Symfony CRUD controller - how does it works? - php

I'm trying to understand how is Symfoy CRUD controllers works, i've googled a lot and can't find any answers.
So the question is, how does controller knows, which entity is passed to route?
For example:
In this index route, we are calling doctrine manager and then pulling all the comments from database.
/**
* Lists all Comment entities.
*
* #Route("/", name="admin_comment_index")
* #Method("GET")
*/
public function indexAction()
{
$em = $this->getDoctrine()->getManager();
$comments = $em->getRepository('AppBundle:Comment')->findAll();
return $this->render('comment/index.html.twig', array(
'comments' => $comments,
));
}
but at next "new" action we are not calling any doctrine instances.Controller seems alredy knows which entity is operating.
/**
* Creates a new Comment entity.
*
* #Route("/new", name="admin_comment_new")
* #Method({"GET", "POST"})
*/
public function newAction(Request $request)
{
$comment = new Comment();
$form = $this->createForm('AppBundle\Form\CommentType', $comment);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($comment);
$em->flush();
return $this->redirectToRoute('admin_comment_show', array('id' => $comment->getId()));
}
return $this->render('comment/new.html.twig', array(
'comment' => $comment,
'form' => $form->createView(),
));
}
I guess it's because second route gets "Request" object, is entity stored in it? I'd like to have some deeper explanation.
UPDATE: "new" action seems clear to me now,it was a bad example of what i'm trying to figure, but let's see the "edit" action:
public function editAction(Request $request, Comment $comment)
{
$deleteForm = $this->createDeleteForm($comment);
$editForm = $this->createForm('AppBundle\Form\CommentType', $comment);
$editForm->handleRequest($request);
if ($editForm->isSubmitted() && $editForm->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($comment);
$em->flush();
return $this->redirectToRoute('admin_comment_edit', array('id' => $comment->getId()));
}
return $this->render('comment/edit.html.twig', array(
'comment' => $comment,
'edit_form' => $editForm->createView(),
'delete_form' => $deleteForm->createView(),
));
}
This time, form is already rendered with data in it, but we are only passing 'id" in request
edit
From where data comes this time? Seems like from comment object,which is passed into controller, but i dont see where it come from.
Sorry for my noobish questions and bad english!

I think you are making it complicated assuming controller knows what to do.
It's actually what code is written inside a controller defines what controller does. For example :
indexAction supposed to get you list of comments from database, hence you need to get EntityManager first to fetch the data. this controller doesn't deal with form as its not required.
newAction supposed to create a new instance of the Comment entity and generate a form to be filled up by user, when submitted Request parameter catches all data and saves to the database. hence, unless you get data from a submitted form, you don't need entity manager to deal with database.
Also, don't assume these are limited to what controller can do, You can customise any controller as per you requirement.
Hope it make sense.

newAction does get an EntityManager instance inside the if statement.
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->getDoctrine()->getManager();
//...
And then it uses this manager to persist the object and flush.
When you load the newAction page, it created a new comment object, and sends that to the formbuilder. After that, it makes sure that the form data is placed onto the new Comment object, allowing it to be persisted.

In newAction function:
First, your form is mapped with your entity :
$comment = new Comment();
$form = $this->createForm('AppBundle\Form\CommentType', $comment);
So Symfony knows that you deal with Comment objects in your form
Then when you submit the form, handleRequest() recognizes this and immediately writes the submitted data back into the Comment object
Finally, if object is valid, you just have to save it in the database thanks to entityManager
So between the request and your formType Symfony knows what he wants

I was wondering the same when I created the first controller via generate:doctrine:crud.
As I'm new to symfony as well, what I state is still based on some assumptions. If I'm wrong, I'm thankful for any correction as it helps me learning.
The Kernel seems to recognises what your controller function accepts based on type hinting. By this, it determines whether your controller function accepts the Request object or not.
Further more, the arguments from the route are resolved and parsed with respect to the hinted types. This should
The magic is then done via Sensio\Bundle\FrameworkExtraBundle\Request\ParamConverter Class.
By type hinting your desired object Comment, the id parameter is converted to the associated object and loaded from your database.
By the way, what I couldn't figure out so far: is it possible to use a different order of parameters in my route than in my function?
e.g. is it possible to do
/user/{user_id}/{comment_id}
with
public function funWithUserComment( Request $request, Comment $comment, User $user)
{
//..
}
Hope, it helps you though.

Related

Symfony 3 FileUpload

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.

How to apply only diff changes from request object to form

I'm implementing patch method using fosrestbundle and I want to create proper patch method.
To do so I've created controler and there is patchAction which takes an argument Entity, Entity is created passed via ParamConverter which I wrote myself. The Entity is passed to EntityType and here's the problem. I want to update only fields that changed and when I pass Entity to form it set nulls to object that comes from request. Entity is POPO
Here's the flow
User sends PATCH request to /entity/{Entity} let's say /entity/12
Param converter converts 12 to proper Entity asking DB for the data
EntityFormType takes Entity as argument and sets data from request to entity.
Entity is stored to DB
The problem is that form after it takes whole Entity object it sets null for fields that are null on form. I'd prefer if it took these values and set it for example as defaults.
I don't and can't use doctrine ORM.
The code:
/**
* #ParamConverter("Entity", class="Entity")
*/
public function patchAction(Entity $entity, Request $request)
{
var_dump($entity); // object mapped from DB
$form = $this->createForm(new EntityType(), $entity);
$form->handleRequest($request);
$form->submit($request);
var_dump($entity);exit; //here I get only values that i passed through patch method, rest of them is set to null
}
I was thinking about form events or creating something like diff method but probably there is better solution?
You need to create your form with method option set.
$form = $this->createForm(new EntityType(), $entity, array(
'method' => $request->getMethod(),
));
If request is send with PATH method then Symfony will update only sent fields.
How to fake PATCH method in Symfony: http://symfony.com/doc/current/cookbook/routing/method_parameters.html#faking-the-method-with-method

How could Symfony2 forms be used to validate data loaded from a file?

From the Symfony2 manual, here is the way a form is validated:
use Symfony\Component\HttpFoundation\Request;
public function createAction(Request $request)
{
$em = $this->getDoctrine()->getManager();
$form = $this->createForm(new RegistrationType(), new Registration());
$form->handleRequest($request);
if ($form->isValid()) {
$registration = $form->getData();
$em->persist($registration->getUser());
$em->flush();
return $this->redirect(...);
}
return $this->render(
'AcmeAccountBundle:Account:register.html.twig',
array('form' => $form->createView())
);
}
The bottom line is we have an entity Registration and a corresponding form RegistrationType. When we want to validate the data in the $request we use $form->handleRequest($request);
I wanna use the same approach, but when my entities are loaded from a file not posted via a form.
For example I have a CSV file that has a list of 100 users that I want to create accounts for, but the data needs to be validated the same way the form was validated and an array of errors can be extracted from the form class.
Is there a way to do this automatically using Symfony2 in a similar way?
Hi in a recent project we did that. But we read the entire file CSV and created some arrays wich hold the values. Then we made the objects with all the values from the file and each object was validated with the Symfony Validator. If you wanted to do it automaticly it's not possible, you have to do it by yourself.

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

Zend Framework 2 Form, created with the AnnotationReader, is valid even when the data is invalid

I have a Form, which I create using the annotation builder like this:
$builder = new AnnotationBuilder();
$fieldset = $builder->createForm(new \Application\Entity\Example());
$this->add($fieldset);
$this->setBaseFieldset($fieldset);
In the controller everything is standard:
$entity = new \Application\Entity\Example();
$form = new \Application\Form\Example();
$form->bind($entity);
if($this->getRequest()->isPost()) {
$form->setData($this->getRequest()->getPost());
if($form->isValid()) {
// save ....
}
}
The problem is, that $form->isValid() always returns true, even when empty or invalid form is submitted. What is even more weird is that the form element error messages are all set, hinting that they are not valid.
I have looked into the ZF2 Form / InputFilter / Input classes and found out that:
Input->isValid() is called twice: once in the Form->isValid() and once in Form->bindValues()
In the first call the validator chain in Input->isValid() ($this->getValidatorChain) is empty and in the second call (from bindValues) it is correct.
What may got wrong?
PS. Using devel version 2.1
I found out what was causing it.
It turns out, that the annotation builder was never intended to work this way. The annotation builder creates a \Zend\Form\Form instance, which I placed in as a fieldset in my base form. I am not sure why, but this was causing the base form not to validate. So in order to make the above code work, there should be no extra Form class and in controller we should have:
$entity = new \Application\Entity\Example();
$builder = new AnnotationBuilder();
$form = $builder->createForm($entity);
$form->bind($entity);
if($this->getRequest()->isPost()) {
$form->setData($this->getRequest()->getPost());
if($form->isValid()) {
// save ....
}
}
Maybe there will be a createFieldset function in the AnnotationBuilder in the future, but for now this seems to be the only way. Hope this helps someone. :)
I am also experiencing the same problem. When I use Annotations to create Fieldsets #Annotation\Type("fieldset") in a form, isValid() always returns true.
Looking at the code for Zend\Form\Factory, when we are creating a Fieldset the configureFieldset() function does not call prepareAndInjectInputFilter(), even where there is an input_filter as part of the form specification.
Only when we are creating a Form, does the Zend\Form\Factory::configureForm() function call prepareAndInjectInputFilter().
So it seems that input filters, and validation groups are only created by the AnnotationBuilder when its type is set to create a form.
I created an input filter myself, from the annotations by adding the code below to my form:
$fspec = ArrayUtils::iteratorToArray($builder->getFormSpecification($entity));
$outerfilter = new InputFilter();
$iffactory = new \Zend\InputFilter\Factory ();
$filter = $iffactory->createInputFilter($fspec['input_filter']);
$outerfilter->add($filter, 'shop'); // Use the name of your fieldset here.
$this->setInputFilter($outerfilter);

Categories