Symfony 3 FileUpload - php

I'm trying implement file uploading functionality for my app with Symfony 3.
I have a product entiry, that have relation to File entiry.
Part of Product:
/**
* #ORM\OneToMany(targetEntity="AppBundle\Entity\File", mappedBy="product")
* #ORM\OrderBy({"weight" = "DESC"})
*/
protected $files;
and field on form:
->add('files', FileType::class, array('multiple'=> true, 'data_class'=> 'AppBundle\Entity\File'));
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Product',
));
}
As you can see, I'm set data_class.
and in controller I'm trying handle form
public function addAction(Request $request)
{
$em = $this->getDoctrine()->getManager();
$product = new Product();
$product->setAddedBy($this->getUser());
$form = $this->createForm(ProductType::class, null);
$form->handleRequest($request);
...
and I have an error:
Expected argument of type "AppBundle\Entity\File", "Symfony\Component\HttpFoundation\File\UploadedFile" given
If I drop data_class mapping I have no error and no object, just array.
How I can resolve this error, how to transform UploadedFile to File (Entiry). I'm trying to create Transformer, but I just got the ProductEntiry class, and as result can't process it, becouse it's without files.

Before I'll get to the point, just one suggest. In line:
$form = $this->createForm(ProductType::class, null);
I would provide $product variable so it will be automatically filled with data instead of creating new one. So it should be changed to :
$form = $this->createForm(ProductType::class, $product);
Ok, now, the problem occurs, because you probably have in your Product class a setter like:
public function addFile(AppBundle\Entity\File $file) { ... }
Then, after successful validation, the form tries to fill instance of Product class with data from the form, which contains Symfony's UploadedFile class instance. I hope you understand that.
Now, you have (at least) two possible solutions.
You can set "mapped" => false option for the file field. That will stop form from trying to put it's value into underlying object (Product instance).
After doing that you can handle the value on your own, which is handle file upload, create AppBundle/Entity/File instance and put it into $product variable via setter.
That the lazy solution, but if you would like to do the same in other forms, you will have to copy the code to every controller that needs it. So it's easier only for one time usage.
The right solution would be to convert UploadedFile to you File object with a Data Transformer. It's a longer topic to talk about and exact solution depends on your data flow that you want to achieve. Therefore if you want to do this right, read about Data Transformers in Symfony's docs first.
I promise that you will thank yourself later if you do that the right way. I've spent some time on understanding Symfony Form Component including Data Transformers and solved a similar issue that way. Now it pays back. I have reusable image upload form that handles even removing previously uploaded files in edit forms.
P.S.
It's "entity", not "entiry". You've wrote "entiry" twice, so I'm just saying FYI.

Related

Symfony 4 - Forms CollectionType get specific data

I have two kind of entities : "Affaire" and "Pointage". My Affaire Entity has a OneToMany relation with my Pointage Entity :
//App\Entity\Affaire.php
/**
* #ORM\OneToMany(targetEntity="App\Entity\Pointage", mappedBy="numAffaire")
*/
private $pointages;
//App\Entity\Pointage.php
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Affaires", inversedBy="pointages")
* #ORM\JoinColumn(nullable=false)
*/
private $numAffaire;
I created a form for my Affaire Entity which get "Pointages" related to a "Affaire".
//App\Form\AffaireType.php
class AffaireType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('numAffaire');
$builder->add('descAffaire');
$builder->add('pointages', CollectionType::class, [
'entry_type' => PointageType::class,
'entry_options' => ['label' => false, 'pointages' => $options['pointages']],
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setRequired(['pointages']);
$resolver->setDefaults([
'data_class' => Affaires::class,
]);
}
}
//App\Form\PointageType.php
class PointageType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('heurePointage');
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setRequired(['pointages']);
$resolver->setDefaults([
'data_class' => Pointage::class,
]);
}
}
My question is : how can I filter the "Pointages" in my AffaireType class ? For example, I want to get all the pointages associated to my Affaire where the id of my pointages is less than 100. Is it possible to make a query somewhere ?
!!! I would strongly advise against what you're doing there !!!
Reason:
Your form type's data class is set to an entity (as far as I can tell). And the form is designed to modify the object it is given (your entity). Now, if you filter a field for that form type, and the form then tries to set the "new" values, all Pointage objects that are not in the form submission (specifically all those that were filtered out) might be removed. You will lose data, if the form calls setPointages - I'm not quite certain, if this is always called, even despite allow_add/allow_remove set to false. If you ever forget, this can make your life miserable. And I believe this is always called, for the standard CollectionType.
update after some code reading: regarding the calling of setPointages: if addPointage/removePointage exist, they will be called. However, the previous value is read by the PropertyAccessor (property-access component) from the target object(!), and the difference will be calculated and add/remove called accordingly -> removing any entities not in the new collection
Possible workarounds
Make heavy use of data mapper / data transformers / form events to somehow hide the fact that there are more Pointage entities. this might work very well, but it complicates stuff a lot but still might become a clean solution.
the general idea is:
form render: get from entity(affaire, all pointages) -> your transformer/mapper/form event handler (affaire, all -> filtered pointages) -> form (affaire, filtered pointages)
form submit: form (affaire, filtered pointages) -> your transformer/mapper/form event handler (affaire, filtered -> "new all" pointages) -> set on entity (affaire, all pointages)
however, you probably have to spend a lot of time understanding the internals of the form component to do this correctly and safely ... (one pointer would be the MergeCollectionListener, which you might be able to adapt to your needs, you might also take a look at the ResizeFormListener, which adds and remove sub forms depending on the given data. remember if you filter your data, you should probably create your own collection type and add a new form listener that handles everything gracefully)
instead of integrating this into the AffaireType, add another form type AffairePointageType with two fields: affaire (AffaireType without pointages) and pointages (CollectionType) and call it like
$filteredPointages = someFilterFunction($affaire->getPointages());
$this->createForm(AffairePointageType::class, [
'affaire' => $affaire,
'pointages' => $filteredPointages,
], [
'pointages' => ... //the option you provide for the CollectionType
]);
where you obviously can provide any pointages, filtered by any filter you like and will only edit those you provide, leaving anything else associated with $affaire untouched. Note, however, that you have to take care of added or removed pointages yourself. (this might break separation of concerns though)
essentially, this is an easier to understand version of the previous workaround, where all filter logic is external, but since the form doesn't communicate "You can edit pointages of the affaire", but instead "you can simultaneously edit an affaire and a set of pointages, which might or might not be related", it's semantically clear and doesn't surprise future users (you included).
However, I believe you're approach might be flawed... but since it's unclear what you're actually trying to achieve, it's hard to propose a proper solution.
If it's just "there are too many (sub) forms displayed" - then it's more of a display issue, that can and probably should be fixed via javascript or css (display:none) or both. (which are imho nicer approaches, that don't mess with form logic and/mechanics).

Forms with Symfony2 - Doctrine Entity with some immutable constructor parameters and OneToMany association

I have a OneToMany association between a Server entity and Client entities in the database. One server can have many clients. I want to make a form where the user can choose a server from a dropdown, fill in some details for a new client, and submit it.
Goal
To create a form where a user can input data into fields for the Client, choose a Server from the dropdown, then click submit and have this data (and the association) persisted via Doctrine.
Simple, right? Hell no. We'll get to that. Here's the pretty form as it stands:
Things of note:
Server is populated from the Server entities (EntityRepository::findAll())
Client is a dropdown with hardcoded values
Port, endpoint, username and password are all text fields
Client Entity
In my infinite wisdom I have declared that my Client entity has the following constructor signature:
class Client
{
/** -- SNIP -- **/
public function __construct($type, $port, $endPoint, $authPassword, $authUsername);
/** -- SNIP -- **/
}
This will not change. To create a valid Client object, the above constructor parameters exist. They are not optional, and this object cannot be created without the above parameters being given upon object instantiation.
Potential Problems:
The type property is immutable. Once you've created a client, you cannot change the type.
I do not have a setter for type. It is a constructor parameter only. This is because once a client is created, you cannot change the type. Therefore I am enforcing this at the entity level. As a result, there is no setType() or changeType() method.
I do not have the standard setObject naming convention. I state that to change the port, for example, the method name is changePort() not setPort(). This is how I require my object API to function, before the use of an ORM.
Server Entity
I'm using __toString() to concatenate the name and ipAddress members to display in the form dropdown:
class Server
{
/** -- SNIP -- **/
public function __toString()
{
return sprintf('%s - %s', $this->name, $this->ipAddress);
}
/** -- SNIP -- **/
}
Custom Form Type
I used Building Forms with Entities as a baseline for my code.
Here is the ClientType I created to build the form for me:
class ClientType extends AbstractType
{
/**
* #var UrlGenerator
*/
protected $urlGenerator;
/**
* #constructor
*
* #param UrlGenerator $urlGenerator
*/
public function __construct(UrlGenerator $urlGenerator)
{
$this->urlGenerator = $urlGenerator;
}
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
/** Dropdown box containing the server name **/
$builder->add('server', 'entity', [
'class' => 'App\Model\Entity\Server',
'query_builder' => function(ServerRepository $serverRepository) {
return $serverRepository->createQueryBuilder('s');
},
'empty_data' => '--- NO SERVERS ---'
]);
/** Dropdown box containing the client names **/
$builder->add('client', 'choice', [
'choices' => [
'transmission' => 'transmission',
'deluge' => 'deluge'
],
'mapped' => false
]);
/** The rest of the form elements **/
$builder->add('port')
->add('authUsername')
->add('authPassword')
->add('endPoint')
->add('addClient', 'submit');
$builder->setAction($this->urlGenerator->generate('admin_servers_add_client'))->setMethod('POST');
}
/**
* {#inheritdoc}
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults([
'data_class' => 'App\Model\Entity\Client',
'empty_data' => function(FormInterface $form) {
return new Client(
$form->getData()['client'],
$form->getData()['port'],
$form->getData()['endPoint'],
$form->getData()['authPassword'],
$form->getData()['authUsername']
);
}
]);
}
/**
* {#inheritdoc}
*/
public function getName()
{
return 'client';
}
}
The above code is what actually generates the form to be used client-side (via twig).
The Problems
First and foremost, with the above code, submitting the form gives me:
NoSuchPropertyException in PropertyAccessor.php line 456:
Neither the property "port" nor one of the methods "addPort()"/"removePort()", "setPort()", "port()", "__set()" or "__call()" exist and have public access in class "App\Model\Entity\Client".
So it can't find the port method. That's because it's changePort() as I explained earlier. How do I tell it that it should use changePort() instead? According to the docs I would have to use the entity type for port, endPoint etc. But they're just text fields. How do I go about this the right way?
I have tried:
Setting ['mapped' => false] on port, authUsername etc. This gives me null for all the client fields, but it does seem to have the relevant server details with it. Regardless, $form->isValid() return false. Here's what var_dump() shows me:
A combination of other things involving setting each on field to "entity", and more..
Basically, "it's not working". But this is as far as I've got. What am I doing wrong? I am reading the manual over and over but everything is so far apart that I don't know if I should be using a DataTransformer, the Entity Field Type, or otherwise. I'm close to scrapping Symfony/Forms altogether and just writing this myself in a tenth of the time.
Could someone please give me a solid answer on how to get where I want to be? Also this may help future users :-)
There are a few problems with the above solution, so here's how I got it working!
Nulls
It turns out that in setDefaultOptions(), the code: $form->getData['key'] was returning null, hence all the nulls in the screenshot. This needed to be changed to $form->get('key')->getData()
return new Client(
$form->get('client')->getData(),
$form->get('port')->getData(),
$form->get('endPoint')->getData(),
$form->get('authPassword')->getData(),
$form->get('authUsername')->getData()
);
As a result, the data came through as expected, with all the values intact (apart from the id).
Twig Csrf
According to the documentation you can set csrf_protection => false in your form options. If you don't do this, you will need to render the hidden csrf field in your form:
{{ form_rest(form) }}
This renders the rest of the form fields for you, including the hidden _token one:
Symfony2 has a mechanism that helps to prevent cross-site scripting: they generate a CSRF token that have to be used for form validation. Here, in your example, you're not displaying (so not submitting) it with form_rest(form). Basically form_rest(form) will "render" every field that you didn't render before but that is contained into the form object that you've passed to your view. CSRF token is one of those values.
Silex
Here's the error I was getting after solving the above issue:
The CSRF token is invalid. Please try to resubmit the form.
I'm using Silex, and when registering the FormServiceProvider, I had the following:
$app->register(new FormServiceProvider, [
'form.secret' => uniqid(rand(), true)
]);
This Post shows how Silex is giving you some deprecated CsrfProvider code:
Turned out it was not due to my ajax, but because Silex gives you a deprecated DefaultCsrfProvider which uses the session ID itself as part of the token, and I change the ID randomly for security. Instead, explicitly telling it to use the new CsrfTokenManager fixes it, since that one generates a token and stores it in the session, such that the session ID can change without affecting the validity of the token.
As a result, I had to remove the form.secret option and also add the following to my application bootstrap, before registering the form provider:
/** Use a CSRF provider that does not depend on the session ID being constant. We change the session ID randomly */
$app['form.csrf_provider'] = $app->share(function ($app) {
$storage = new Symfony\Component\Security\Csrf\TokenStorage\SessionTokenStorage($app['session']);
return new Symfony\Component\Security\Csrf\CsrfTokenManager(null, $storage);
});
With the above modifications, the form now posts and the data is persisted in the database correctly, including the doctrine association!

Symfony2 Form pre-fill fields with data

Assume for a moment that this form utilizes an imaginary Animal document object class from a ZooCollection that has only two properties ("name" and "color") in symfony2.
I'm looking for a working simple stupid solution, to pre-fill the form fields with the given object auto-magically (eg. for updates ?).
Acme/DemoBundle/Controller/CustomController:
public function updateAnimalAction(Request $request)
{
...
// Create the form and handle the request
$form = $this->createForm(AnimalType(), $animal);
// Set the data again << doesn't work ?
$form->setData($form->getData());
$form->handleRequest($request);
...
}
You should load the animal object, which you want to update. createForm() will use the loaded object for filling up the field in your form.
Assuming you are using annotations to define your routes:
/**
* #Route("/animal/{animal}")
* #Method("PUT")
*/
public function updateAnimalAction(Request $request, Animal $animal) {
$form = $this->createForm(AnimalType(), $animal, array(
'method' => 'PUT', // You have to specify the method, if you are using PUT
// method otherwise handleRequest() can't
// process your request.
));
$form->handleRequest($request);
if ($form->isValid()) {
...
}
...
}
I think its always a good idea to learn from the code generated by Symfony and doctrine console commands (doctrine:generate:crud). You can learn the idea and the way you should handle this type of requests.
Creating your form using the object is the best approach (see #dtengeri's answer). But you could also use $form->setData() with an associative array, and that sounds like what you were asking for. This is helpful when not using an ORM, or if you just need to change a subset of the form's data.
http://api.symfony.com/2.8/Symfony/Component/Form/Form.html#method_setData
The massive gotcha is that any default values in your form builder will not be overridden by setData(). This is counter-intuitive, but it's how Symfony works. Discussion:
https://github.com/symfony/symfony/issues/7141

Symfony Adding and Dealing With Custom Form Fields

I am using Symfony with propel to generate a form called BaseMeetingMeetingsForm.
In MeetingMeetingsForm.class.php I have the following configure method:
public function configure() {
$this->useFields(array('name', 'group_id', 'location', 'start', 'length'));
$this->widgetSchema['invited'] = new myWidgetFormTokenAutocompleter(array("url"=>"/user/json"));
}
In MeetingMeetings.php my save method is simply:
public function save(PropelPDO $con = null) {
$this->setOwnerId(Meeting::getUserId());
return parent::save($con);
}
However propel doesn't know about my custom field and as such doesn't do anything with it. Where and how to I put in a special section that can deal with this form field, please be aware it is not just a simple save to database, I need to deal with the input specially before it is input.
Thanks for your time and advice,
You have to define a validator (and/or create your own). The validator clean() method returns the value that needs to be persisted.
In Doctrine (I don't know Propel) the form then calls the doUpdateObject() on the form, which in turns calls the fromArray($arr) function on the model.
So if it's already a property on your model you'll only need to create the validator. If it's a more complex widget, you'll need to add some logic to the form.

Saving a embed sfForm

I'm making a form which consist in some text and uploading files. The problem is that the the file is not been saved in the db (blob field) because the client didn't want to so I made a UploadFileForm to make it clean and do the logic of uploading and saving into the File Table (which has the path of that file). So i have:
//HistoryForm
class HistoryForm extends BaseHistoryForm
{
public function configure()
{
unset($this['text']);
$this->setWidget('text', new sfWidgetFormTextareaTinyMCE(array(
'width' => 550,
'height' => 350,
'config' => 'theme_advanced_disable: "anchor,image,cleanup,help"',)));
$this->setValidator('text', new sfValidatorString());
$this->embedForm('uploadfile1', new UploadFileForm());
}
}
//UploadFileForm
class UploadFileForm extends sfForm{
public function configure() {
$this->setWidget('file', new sfWidgetFormInputFile());
$this->setValidator('file', new sfValidatorFile(array(
'required' => false,
'path' => sfConfig::get('sf_upload_dir')
)));
$this->setWidget('name', new sfWidgetFormInputText());
$this->setValidator('name', new sfValidatorString(array('required' => true)));
$this->widgetSchema->setNameFormat('uploadfile[%s]');
}
public function save({data_to_be_saved}){
//logic of saving
}
}
The main problem is that embeding a doctrine form works perfectly, but if I want to save a non doctrine form the save method is never called (seems fine because not all sfForms have to be saved) but if I override the saveEmbeddedForms the embed sfForm isn't binded! Reading the symfony's code found out that when embeding a form what really does is appending the fields to the main widgetSchema so using the embed form is practically usless... So, what I do is making a save method in the sfForm which does the saving by getting all needed variables from parameters. This is call is made in the overrided method save of the main doctrine form:
//HistoryForm
class HistoryForm extends BaseHistoryForm
{
...
public function save($con = null) {
$hc = parent::save($con);
foreach ($this->getEmbeddedForms() as $form) {
$values = $this->getValue($form->getName());
$form->save($hc, $values);
}
}
I spent all afternoon thinking and reading symfony's core code and didn't found a proper approach. So... does anyone knows (or thinks) a better approach?
The generally accepted approach to doing what I think you are trying to do is to infact not use an embedded form. Mixing doctrine form and sfForms is troublesome to say the least.
I'm assuming you're trying to associated a database record with a file?
If so, the usually way to do this is to create a filename field on the HistoryForm. make this field a sfWidgetInputFile widget. This widget will save to the file system, then overwrite the forms 'save' method to save the file's name filename field instead of the file contents.
Then add an accessor on the model or an asset helper class to get the record's associated file on the file system
Did you try this plugin? I had problems with embedding relations but after trying this out all of them were solved.

Categories