0

I'm making a REST API with Symfony 4.4. The API largely revolves around putting data into a database, using Doctrine. I have figured out how to add rows to the database, but now I'm stuck on changing data. I know how I can take a row from the database and that, in theory, I can change fields by calling the setter of a property, but right now, I seem to be getting an array instead of the desired entity and, seemingly more difficult, I want to be able to dynamically change the properties of the existing row, so that I don't have to include every field of the object of the row I'm changing and call every setter.

Here is my code:

// PersonController.php
/**
 * @IsGranted("ROLE_USER")
 * @Rest\Post("/addperson")
 * @param Request $request
 * @return Response
 */
public function addOrUpdatePerson(Request $request)
{
    $data = json_decode($request->getContent(), true);
    $em = $this->getDoctrine()->getManager();
    $person = new Person();
    $form = $this->createForm(PersonType::class, $person);
    $form->submit($data);
    if (!$form->isSubmitted() || !$form->isValid())
    {
        return $this->handleView($this->view($form->getErrors()));
    }
    if (isset($data['id']))
    {
        // This person exists, change the row
        // What to do?
    }
    // This person is new, insert a new row
    $em->persist($person);
    $em->flush();
    return $this->handleView($this->view(['status' => 'ok'], Response::HTTP_CREATED));
}
// PersonType.php
public function buildForm(FormBuilderInterface $builder, array $options)
{
    $builder
        ->add('id', IntegerType::class, ['mapped' => false])
        ->add('inits')
        ->add('firstname')
        ->add('lastname')
        ->add('email')
        ->add('dateofbirth', DateTimeType::class, [
            'widget' => 'single_text',
            // this is actually the default format for single_text
            'format' => 'yyyy-MM-dd',
        ])
        // Some other stuff
        ->add('save', SubmitType::class);
}

public function configureOptions(OptionsResolver $resolver)
{
    $resolver->setDefaults(array(
        'data_class' => Person::class,
        'csrf_protection' => false
    ));
}

I doubt the Person entity is relevant here, but if it is, please let me know and I'll include it ASAP!

As a response to the suggestion of the other question from Symfony 2; it doesn't seem to fix my problem (entirely). As a result of this question, I have changed my function to this (which doesn't work, but doesn't throw any errors):

public function addOrUpdatePerson(Request $request)
{
    $data = json_decode($request->getContent(), true);
    $em = $this->getDoctrine()->getManager();
    if (isset($data['id'])) {
        // This person exists
        $existing = $em->getRepository(Person::class)->find(['id' => $data['id']]);
        $this->getDoctrine()->getManager()->flush();
        $form = $this->createForm(PersonType::class, $existing);
        $form->handleRequest($request);
        // this doesn't seem to do anything
        // $em->persist($existing);
        $em->flush();
        return $this->handleView($this->view($existing));
    }
}

I think I'm still missing some info, like what to do at // perform some action, such as save the object to the database. I also notice a lot has changed since Symfony 2, and as a result it is not obvious to me what I should do.

Luctia
  • 322
  • 1
  • 5
  • 17

2 Answers2

0

1.) You don't have to use json_decode directly. You can use the following code instead:

// Person controller
/**
 * @Route("/person", name="api.person.add", methods={"POST"})
 * @Security("is_granted('ROLE_USER')")
 */
public function addPerson(Request $request)
{
    $person = new Person();
    $form = $this->createForm(PersonType::class, $person);
    $form->submit($request->request->all());
    if (!$form->isSubmitted() || !$form->isValid()) {
        throw new \Exception((string) $form->getErrors(true));
    }

    $em = $this->getDoctrine()->getManager();
    $em->persist($person);
    $em->flush();

    ...

}

2.) When you're updating entity you need to load it first and skip the $em->persist($entity); part. In this case, we provide the ID of the entity via the path variable (there are various ways to provide it but this one is quite common). NOTE: I've set $id parameter as mixed because it can be integer or string if you're using UUID type of IDs.

// Person controller
/**
 * @Route("/person/{id}", name=api.person.patch", methods={"PATCH"})
 * @Security("is_granted('ROLE_USER')")
 */
public function patchPerson(Request $request, mixed $id)
{
    // Load person
    $personRepository = $this->getDoctrine()->getRepository(Person::class);
    $person = $personRepository->find($id);
    if (!$person) { throw new \Exception('Entity not found'); }

    $form = $this->createForm(PersonType::class, $person);
    $form->submit($request->request->all());
    if (!$form->isSubmitted() || !$form->isValid()) {
        throw new \Exception((string) $form->getErrors(true));
    }

    $em = $this->getDoctrine()->getManager();
    $em->flush();

    ...

}

3.) In general usage, we don't set the ID property via posted data (unless it is required). We rather use generated value instead. When you insert new entity you gen use its ID to address it for modifications. Sample:

<?php

namespace App\Entity;

use Ramsey\Uuid\Uuid;
use Doctrine\ORM\Mapping as ORM;

class Person
{
    /**
     * @var Uuid
     *
     * @ORM\Id
     * @ORM\Column(type="uuid", unique=true)
     * @ORM\GeneratedValue(strategy="CUSTOM")
     * @ORM\CustomIdGenerator(class="Ramsey\Uuid\Doctrine\UuidGenerator")
     * @Groups({"public"})
     */
    protected $id;

    // Other entity properties ...

    public function getId(): ?string
    {
        return $this->id;
    }

    public function setId(string $id): self
    {
        $this->id = $id;

        return $this;
    }

    // Setters and getters for other entity properties ...
}

4.) Entity class in FormType (PersonType.php) is very relevant. After form submission and validation you access properties of the entity itself within the controller - not the decoded payload data from the request directly. Symfony's form system will make sure that the input data is valid and matches the requirements and constraints set in the entity model or form type specification.

// Person controller
/**
 * @Route("/person", name="api.person.add", methods={"POST"})
 * @Security("is_granted('ROLE_USER')")
 */
public function addPerson(Request $request)
{
    $person = new Person();
    $form = $this->createForm(PersonType::class, $person);
    $form->submit($request->request->all());
    if (!$form->isSubmitted() || !$form->isValid()) {
        throw new \Exception((string) $form->getErrors(true));
    }

    $em = $this->getDoctrine()->getManager();
    $em->persist($person);
    $em->flush();

    $id = $person->getId();
    $firstName = $person->getFirstname();
    $lastName = $person->getLastname();
    // etc

    ...
}

5.) If you want to use the same method/endpoint for adding and updating entity you can do something like @lasouze mentioned.

// Person controller
/**
 * @Route("/person", name=api.person.add_or_update", methods={"POST", "PATCH"})
 * @Security("is_granted('ROLE_USER')")
 */
public function patchPerson(Request $request)
{
    $id = $request->request->get('id', null);
    if (!$id) {
        $person = new Person();
    } else {
        // Load person
        $personRepository = $this->getDoctrine()->getRepository(Person::class);
        $person = $personRepository->find($id);
        if (!$person) { throw new \Exception('Entity not found'); }
    }

    $form = $this->createForm(PersonType::class, $person);
    $form->submit($request->request->all());
    if (!$form->isSubmitted() || !$form->isValid()) {
        throw new \Exception((string) $form->getErrors(true));
    }

    $em = $this->getDoctrine()->getManager();
    $em->flush();

    ...

}

PS: $form->submit($request->request->all()); will not work for file uploads because $request->request->all() does not contain parameters provided by $_FILES. In my case I ended up merging data like $form->submit(array_merge($request->request->all(), $request->files->all())); but this is probably not the best solution. I'll update my answer if I'll figure out anything better.

Boštjan Biber
  • 444
  • 3
  • 12
0

After '$person = new Person()' juste add :

If (isset($data['id']) && 0 < $data['id']) {
    $person=$em->getRepository(Person::class)->find($data['id']);
}
If (!$person) {
    Throw new \Exception('Person not found');
}
Lasouze
  • 112
  • 3