FOSRestBundle update only one field at a time using PUT - rest

While ago I have asked same question Symfony PUT does not contain all entity properties mapped and I've got some #hack solution to solve the problem but when the form is more complex, contains choices (arrays) or mapped entities (objects) the solution is not applicable anymore. So I came up with very dirty ideas to make it work such as
if(is_object($event->getForm()->getData())) $event->setData($event->getForm()->getData()->getId());
if(is_array($event->getData()) && empty($event->getData()))
{
$event->setData([$event->getForm()->getData()]);
}
if($event->getForm()->getData() instanceof \DateTime) $event->setData($event->getForm()->getData()->format('Y-m-d H:i:s'));
if(!is_array($event->getData()) && (is_string($event->getForm()->getData()) || is_integer($event->getForm()->getData())))
{
$event->setData($event->getForm()->getData());
}
but it's not even working perfect. So I must ask one more time if there's a better solution to updatejust one value at the time of sending json response, because if I send {"user":{"firstName":"John"}} all other fields belonging to the User form are empty and I cannot send entire resource. I cannot find any solution to this problem.
And here's the Controller
/**
* This endpoint updates an existing Client entity.
*
* #ApiDoc(
* resource=true,
* description="Updates an existing Client entity",
* )
* #ParamConverter("user", class="Software:User", options={"mapping": {"user": "guid"}})
*/
public function putAction(Request $request, $user)
{
$form = $this->createForm(new UserType(['userType' => 'client', 'manager' => $this->getDoctrine()->getManager()]), $user, ['method' => 'PUT']);
$form->handleRequest($request);
if($form->isValid())
{
$manager = $this->getDoctrine()->getManager();
$manager->flush();
return $this->view([
'user' => $user
]);
}
return $this->view($form);
}

I am going to answer my own question.
The answer is to use PATCH method instead of PUT.
Here's the modified code:
/**
* This endpoint updates an existing Client entity.
*
* #ApiDoc(
* resource=true,
* description="Updates an existing Client entity",
* )
* #ParamConverter("user", class="Software:User", options={"mapping": {"user": "guid"}})
*/
public function patchAction(Request $request, $user)
{
$form = $this->createForm(new UserType(['userType' => 'client', 'manager' => $this->getDoctrine()->getManager()]), $user, ['method' => 'PATCH']);
$form->handleRequest($request);
if($form->isValid())
{
$manager = $this->getDoctrine()->getManager();
$manager->flush();
return $this->view([
'user' => $user
]);
}
return $this->view($form);
}
Please find references:
Symfony2 REST API - Partial update
http://williamdurand.fr/2012/08/02/rest-apis-with-symfony2-the-right-way/
http://williamdurand.fr/2014/02/14/please-do-not-patch-like-an-idiot/
Read carefully PATCH vs. PUT chapters.

Related

Editing a form -> Forced to modify image [Easy to answer i think]

I am trying to modify my profile entity but in it i have a variable for the profile image path ($avatarPath). But when i am trying to modify my entity with a form i am forced to "upload" a new file for validated the form (the value of the path file is at null for default i think, so when i accept the form without an image the tells me that my form is not valid and it lack the image
so my goal is to set by default the image of the profile in the edit form or to not put the button upload but making the form work (and i will put the upload file in an other page)
(that's for sure an idiot error but i don't see where)
My $avatarPath
/**
* #var string
*
* #Assert\NotBlank(message="Please enter an image")
* #Assert\Image()
* #ORM\Column(name="avatar_path", type="string", length=255, nullable=true)
*/
protected $avatarPath;
My controller :
/**
* Creates a new profile entity.
*
* #Route("/edit/{id}", name="profile_edit")
*/
public function editProfileAction(Request $request, User $user, Profile $profile)
{
$loggedAs = $this->getUser();
$form = $this->createForm(ProfileType::class, $profile);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
/**
* #var UploadedFile $file
*/
$file = $form->get('avatar_path')->getData();
$fileName = md5(uniqid()) . '.' . $file->guessExtension();
$file->move($this->getParameter('image_directory'), $fileName);
$profile->setAvatarPath($fileName);
if ($profile->getAvatarPath() == NULL)
$profile->setAvatarPath('NULL');
$em = $this->getDoctrine()->getManager();
$em->persist($profile);
$em->flush();
$user->setIdProfile($profile);
$em2 = $this->getDoctrine()->getManager();
$em2->persist($user);
$em2->flush();
return $this->redirectToRoute('user_list');
}
return $this->render('admin/user/new_profile.html.twig', array(
'profile' => $profile,
'form' => $form->createView(),
'loggedAs' => $loggedAs,
));
}
My form :
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('resume')
->add('is_male')
->add('birth', BirthdayType::class, array(
'years' => range(1930,2018)
))
->add('email', EmailType::class, array(
'label' => 'form.email'
))
->add('phone')
->add('language')
->add('travel')
->add('pets')
->add('avatar_path', FileType::class, array(
'data_class' => null
))
->add('avatar_path', FileType::class, array(
'data_class' => null,
'required' => false
));
}
Thx for anyone who will try to help :p
You could try to set the property mapped => false to the avatar_path field in your Form.
Doing this will cause symfony to do two things. The first is to ignore the avatar_path field when validating the form which happens in your controller when form->isValid() is called. Secondly symfony will not assign any value to the field which happens when you call $form->handleRequest($request).
If you choose to use the mapped => false property you will need to manually set the avatar_path on the Profile entity.
Here is the documentation from symfony http://symfony.com/doc/3.4/reference/forms/types/file.html (these docs are for symfony 3.4)
Hope this helps.
Edit: I assume the form is failing the validation checks because of the two #Assert Statements in your Entity. Looking at your code I think you can safely remove those since you are already allowing the property $avatarPath to be nullable anyway.
I put that :
->add('avatar_path', FileType::class, array(
'data_class' => null,
'mapped' => false,
'required' => false
));
but nothing appens always the same :
That just refresh the page so the form is not valid ...

Symfony3 : multi-steps form and flush after forward

I have a form to add a new prescriber in my database. The first step consists in informing the various informations about the prescriber.
Then, I check if there are similar prescribers before adding it (2nd step with a 2nd form) and if there are, I ask the user to confirm.
In short, I have a 1-step form or a 2-steps form, depending on duplicates.
I tried with CraueFormFlowBundle but I don't know how to implement my conditional second step. My tests were inconclusive. So I decided to use forward method in my controller, and I like it !
But, I can't manage to flush my prescriber at the end of the 2nd step (after forwarding), I have this error : Unable to guess how to get a Doctrine instance from the request information for parameter "prescriber".
addAction (= step 1)
/**
* Add a new prescriber
*
* #Route("/prescribers/add", name="prescriber_add")
*/
public function addAction(Request $request) {
$em = $this->getDoctrine()->getManager();
$rp = $em->getRepository('AppBundle:Prescriber');
$p = new Prescriber();
// build the form
$form = $this->createForm(AddPrescriberType::class, $p);
// handle the submit
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
# search if a prescriber already exists
$qb = $rp->createQueryBuilder('p');
$qb->where($qb->expr()->eq('p.rpps', ':rpps'))
->orWhere($qb->expr()->andX(
$qb->expr()->like('p.lastname', ':name'),
$qb->expr()->like('p.firstname', ':firstname')
))
->setParameter('rpps', $p->getRpps())
->setParameter('name', '%'.$p->getLastname().'%')
->setParameter('firstname', '%'.$p->getFirstname().'%');
$duplicates = $qb->getQuery()->getArrayResult();
# there are duplicates
if (!empty($duplicates)) {
$em->persist($p);
// confirm the addition of the new prescriber
$params = array('prescriber' => $p, 'duplicates' => $duplicates);
$query = $request->query->all();
return $this->forward('AppBundle:Prescriber:addConfirm', $params, $query);
} else {
$em->persist($p); # save the prescriber
$em->flush(); # update database
$this->addFlash('p_success', 'The prescriber has been created successfully');
return $this->redirectToRoute('prescriber');
}
}
// show form
return $this->render('prescribers/form-step1.html.twig', array(
'form' => $form->createView()
));
}
addConfirmAction (= step 2)
/**
* Confirm addition of a new prescriber
*
* #Route("/prescribers/add/confirm", name="prescriber_add_confirm")
*/
public function addConfirmAction(Prescriber $prescriber, $duplicates, Request $request) {
$em = $this->getDoctrine()->getManager();
$form = $this->createFormBuilder()->getForm();
if ($form->handleRequest($request)->isValid()) {
$em->persist($prescriber);
$em->flush();
$this->addFlash('p_success', 'Prescriber has been created successfully');
return $this->redirectToRoute('prescriber');
}
// show confirm page
return $this->render('prescribers/form-step2.html.twig', array(
'h1_title' => 'Ajouter un nouveau prescripteur',
'form' => $form->createView(),
'p' => $prescriber,
'duplicates'=> $duplicates
));
}
I think the problem comes from the fact that I have 2 forms submissions...
I found a solution by using the session.
(I know it's not a perfect way but I didn't find other one)
For Symfony 3.3.*
use Symfony\Component\HttpFoundation\Session\SessionInterface;
public function addAction(Request $request, SessionInterface $session) {
// [...]
# there are duplicates
if (!empty($duplicates)) {
$data = $form->getData();
$session->set('prescriber', $data);
$session->set('duplicates', $duplicates);
return $this->forward('AppBundle:Prescriber:addConfirm');
// [...]
}
public function addConfirmAction(Request $request, SessionInterface $session) {
$em = $this->getDoctrine()->getManager();
$p = $session->get('prescriber');
$duplicates = $session->get('duplicates');
// empty form with only a CSRF field
$form = $this->createFormBuilder()->getForm();
if ($form->handleRequest($request)->isValid()) {
$em->persist($p);
$em->flush();
$this->addFlash('p_success', 'The prescriber has been created successfully');
return $this->redirectToRoute('prescriber');
}
// show confirm page
return $this->render('prescribers/form-step2.html.twig', array(
'form' => $form->createView(),
'prescriber'=> $p,
'duplicates'=> $duplicates
));
}

Symfony2 form setting, unsetting associations

I have Company and Number entity which are related
/**
* #var Comapany
*
* #ORM\ManyToOne(targetEntity="Company", inversedBy="numbers", cascade={"persist", "remove"})
* #ORM\JoinColumn(name="company", referencedColumnName="id", nullable=true, onDelete="RESTRICT")
* #Assert\NotBlank(groups={"client"})
* #Assert\Valid()
*/
private $company;
/**
* #var Number[]
* #ORM\OneToMany(targetEntity="Number", mappedBy="company", fetch="EXTRA_LAZY", cascade={"persist", "remove"})
* #Assert\Count(min="1")
*/
private $numbers;
I have created a form for creating and updating Company entity. This form should allow to set Number entities to it as well as unset them. This is how it looks rendered
And this is how it looks in code:
$builder
->add('name', 'text', [
'required' => false
])
->add('numbers', 'entity', [
'class' => 'AppBundle:Number',
'property' => 'number',
'placeholder' => '',
'required' => false,
'multiple' => true,
'query_builder' => function (EntityRepository $er) use ($builder) {
if ($builder->getData() && $id = $builder->getData()->getId()) {
return $er->createQueryBuilder('n')
->where('n.company is NULL')
->orWhere('n.company = :id')
->setParameter('id', $id);
}
return $er->createQueryBuilder('n')
->where('n.company is NULL');
}
]);
The problem is when creating new Company record, the form assigns Number entities, but the Number entities have property "company" which doesn't get assigned and so no relation is made. I have worked around this with form events:
$builder->addEventListener(FormEvents::SUBMIT, function (FormEvent $event) {
foreach ($event->getData()->getNumbers() as $number) {
$number->setCompany($event->getData());
}
});
Which works for creating record, however when updating I have another issue, since I remove Number associations I have no access to them and thus can't update them in database. I could again select all Number entities assigned to form, and then filter out which were assigned to company and which were not and then manually update them, but this feels dirty and I would like to work it out in a clean way.
Finally found solution, as is turns out it's quite well documented:
http://symfony.com/doc/current/cookbook/form/form_collections.html
The changes I Had to make was:
1.Add by_reference property to entity form field: More information on that with an example: http://symfony.com/doc/current/cookbook/form/form_collections.html#allowing-new-tags-with-the-prototype
Basically what I figured that without this options symfony2 form uses own means of adding associations, and with this option set it calls methods "addNumber" and "removeNumber" inside Entity in which I had to manually add inverse side "number" association which goes to 2nd change I had to make.
$builder
->add('name', 'text', [
'required' => false
])
->add('numbers', 'entity', [
'class' => 'AppBundle:Number',
'property' => 'number',
'placeholder' => '',
'required' => false,
'multiple' => true,
'by_reference' => false, //
'query_builder' => function (EntityRepository $er) use ($builder) {
if ($builder->getData() && $id = $builder->getData()->getId()) {
return $er->createQueryBuilder('n')
->where('n.company is NULL')
->orWhere('n.company = :id')
->setParameter('id', $id);
}
return $er->createQueryBuilder('n')
->where('n.company is NULL');
}
]);
2.I had explicitly set Inverse side association to owning side by calling method setComapany($this) from owning (Company Entity) side.
/**
* Add numbers
*
* #param \AppBundle\Entity\Number $numbers
* #return Company
*/
public function addNumber(\AppBundle\Entity\Number $numbers)
{
$numbers->setCompany($this); //!Important manually set association
$this->numbers[] = $numbers;
return $this;
}
These 2 changes are enough to make form automatically add associations. However with removing associations there's a little bit more.
3.Change I had to make to correctly unset associations was inside controller action itself: I had to save currently set associations inside new ArrayCollection variable, and after form validation manually go through each item in that collection checking if it exists after form was validated. Important note:
I had also manually unset inverse side association to owning side by calling:
"$number->setCompany(null);"
public function editAction(Request $request, Company $company)
{
$originalNumbers = new ArrayCollection();
foreach ($company->getNumbers() as $number) {
$originalNumbers->add($number);
}
$form = $this->createForm(new CompanyType(), $company);
$form->handleRequest($request);
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
foreach ($originalNumbers as $number) {
if (false === $company->getNumbers()->contains($number)) {
$company->getNumbers()->removeElement($number);
$number->setCompany(null); //!Important manually unset association
}
}
$em->persist($company);
$em->flush();
return $this->redirectToRoute('companies');
}
return $this->render('AppBundle:Company:form.html.twig', [
'form' => $form->createView()
]);
}
All of these steps are required to make this kind of logic function properly, luckily I was able to really good documentation for that.
PS. Note that I call
$em->persist($company);
$em->flush();
without persisting each "Number" Entity iterated inside loop which you can be seen in given Symfony2 documentation example, and which in this case would look like this:
$company->getNumbers()->removeElement($number);
$number->setCompany(null);
$em->persist($number);
This is because I setup Cascading Relations options inside my Entity class
/**
* #var Number[]
* #ORM\OneToMany(targetEntity="Number", mappedBy="company", fetch="EXTRA_LAZY", cascade={"persist", "remove"})
* #Assert\Count(min="1")
*/
private $numbers;
My advise for anyone struggling with this is to read whole http://symfony.com/doc/current/cookbook/form/form_collections.html thoroughly especially sections marked by special signs ✎✚❗☀💡

Proper way to update class object in db using symfony2 + doctrine + form?

I have a simple class:
class Type
{
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(type="string", length=15)
*/
private $name;
...
}
And have a some 'type' objects in database.
So, if i want to change one of them, i create new controller rule (like /types/edit/{id}) and new action:
public function typesEditViewAction($id)
{
...
$editedType = new Type();
$form = $this->createFormBuilder($editedType)
->add('name', 'text')
->add('id', 'hidden', array('data' => $id))
->getForm();
// send form to twig template
...
}
After that, i create another controller rule (like /types/do_edit) and action:
public function typesEditAction(Request $request)
{
...
$editedType = new Type();
$form = $this->createFormBuilder($editedType)
->add('name', 'text')
->add('id', 'hidden')
->getForm();
$form->bind($request); // <--- ERROR THERE !!!
// change 'type' object in db
...
}
And i found a small problem there.
Сlass 'Type' doesn't have аuto-generated setter setId() and on binding i got error.
Neither the property "id" nor one of the methods "setId()", "__set()" or "__call()" exist and have public access in class "Lan\CsmBundle\Entity\Type".
Now, i remove 'id' field from symfony2 form object ($form) and transmit it manually to template.
At second controller's action i have $form object and 'id'-field apart.
I don't know a 'proper'-way for doing that (updating 'type' class). Please help.
Symfony has an integrated ParamConverter which automatically fetches your entity from database and throws an Exception ( which you can catch in a listener ) if the entity is not found.
You can easily handle GET and POST requests in one controller method.
make sure you have the public getters and setters for your properties in your entity.
I added annotations to make the routing clearer and still have a working example.
use Vendor\YourBundle\Entity\Type;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
// ...
/**
* #Route("/edit/{id}", requirements={"id" = "\d+"})
* #Method({"GET", "POST"})
*/
public function editAction(Request $request, Type $type)
{
$form = $this->createFormBuilder($type)
->add('name', 'text')
->add('id', 'hidden')
->getForm()
;
if ($request->isMethod('POST')) {
$form->bind($request);
if ($form->isValid())
{
$em = $this->getDoctrine()->getEntityManager();
$em->flush(); // entity is already persisted and managed by doctrine.
// return success response
}
}
// return the form ( will include the errors if validation failed )
}
I strongly suggest you should create a form type to further simplify your controller.
For anyone else stumbling on this where you added the ID field to your FormType because the frontend needed it you can just set the ID column to "not-mapped" like so:
->add('my_field', 'hidden', ['mapped'=>false])
and it prevents the ID value trying to get used by the form processing method.

Symfony2 Form Entity Update

Can anyone please show me a specific example of a Symfony2 form entity update? The book only shows how to create a new entity. I need an example of how to update an existing entity where I initially pass the id of the entity on the query string.
I'm having trouble understanding how to access the form again in the code that checks for a post without re-creating the form.
And if I do recreate the form, it means I have to also query for the entity again, which doesn't seem to make much sense.
Here is what I currently have but it doesn't work because it overwrites the entity when the form gets posted.
public function updateAction($id)
{
$em = $this->getDoctrine()->getEntityManager();
$testimonial = $em->getRepository('MyBundle:Testimonial')->find($id);
$form = $this->createForm(new TestimonialType(), $testimonial);
$request = $this->get('request');
if ($request->getMethod() == 'POST') {
$form->bindRequest($request);
echo $testimonial->getName();
if ($form->isValid()) {
// perform some action, such as save the object to the database
//$testimonial = $form->getData();
echo 'testimonial: ';
echo var_dump($testimonial);
$em->persist($testimonial);
$em->flush();
return $this->redirect($this->generateUrl('MyBundle_list_testimonials'));
}
}
return $this->render('MyBundle:Testimonial:update.html.twig', array(
'form' => $form->createView()
));
}
Working now. Had to tweak a few things:
public function updateAction($id)
{
$request = $this->get('request');
if (is_null($id)) {
$postData = $request->get('testimonial');
$id = $postData['id'];
}
$em = $this->getDoctrine()->getEntityManager();
$testimonial = $em->getRepository('MyBundle:Testimonial')->find($id);
$form = $this->createForm(new TestimonialType(), $testimonial);
if ($request->getMethod() == 'POST') {
$form->bindRequest($request);
if ($form->isValid()) {
// perform some action, such as save the object to the database
$em->flush();
return $this->redirect($this->generateUrl('MyBundle_list_testimonials'));
}
}
return $this->render('MyBundle:Testimonial:update.html.twig', array(
'form' => $form->createView()
));
}
This is actually a native function of Symfony 2 :
You can generate automatically a CRUD controller from the command line (via doctrine:generate:crud) and the reuse the generated code.
Documentation here :
http://symfony.com/doc/current/bundles/SensioGeneratorBundle/commands/generate_doctrine_crud.html
A quick look at the auto-generated CRUD code by the Symfony's command generate:doctrine:crudshows the following source code for the edit action
/**
* Displays a form to edit an existing product entity.
*
* #Route("/{id}/edit", name="product_edit")
* #Method({"GET", "POST"})
*/
public function editAction(Request $request, Product $product)
{
$editForm = $this->createForm('AppBundle\Form\ProductType', $product);
$editForm->handleRequest($request);
if ($editForm->isSubmitted() && $editForm->isValid()) {
$this->getDoctrine()->getManager()->flush();
return $this->redirectToRoute('product_edit', array('id' => $product->getId()));
}
return $this->render('product/edit.html.twig', array(
'product' => $product,
'edit_form' => $editForm->createView(),
));
}
Note that a Doctrine entity is passed to the action instead of an id (string or integer). This will make an implicit parameter conversion and saves you from manually fetching the corresponding entity with the given id.
It is mentioned as best practice in the Symfony's documentation