Chrysweel
Chrysweel

Reputation: 363

Embedding relations with api platform

My doubt is about Api Platform. (https://api-platform.com) I have two entities. Question and Answer. And I want to have a POST call to create a question with one answer. I show my entities.

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Serializer\Annotation\Groups;

/**
 * @ApiResource(
 *     normalizationContext={"groups"={"question"}},
 *     denormalizationContext={"groups"={"question"}})
 * @ORM\Entity
 */
class Question
{
    /**
     * @Groups({"question"})
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @Groups({"question"})
     * @ORM\Column
     * @Assert\NotBlank
     */
    public $name = '';

    /**
     * @Groups({"question"})
     * @ORM\OneToMany(targetEntity="Answer", mappedBy="question", cascade={"persist"})
     */
    private $answers;

    public function getAnswers()
    {
        return $this->answers;
    }

    public function setAnswers($answers): void
    {
        $this->answers = $answers;
    }


    public function __construct() {
        $this->answers = new ArrayCollection();
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function setName(string $name): void
    {
        $this->name = $name;
    }

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

And a Answer Entity

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Serializer\Annotation\Groups;

/**
 *
 * @ApiResource
 * @ORM\Entity
 */
class Answer
{
    /**
     * @Groups({"question"})
     * @ORM\Id
     * @ORM\Column(type="guid")
     */
    public $id;

    /**
     * @Groups({"question"})
     * @ORM\Column
     * @Assert\NotBlank
     */
    public $name = '';

    /**
     * @ORM\ManyToOne(targetEntity="Question", inversedBy="answers")
     * @ORM\JoinColumn(name="question_id", referencedColumnName="id")
     */
    public $question;

    public function getQuestion()
    {
        return $this->question;
    }

    public function setQuestion($question): void
    {
        $this->question = $question;
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function setName(string $name): void
    {
        $this->name = $name;
    }

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

    public function __toString()
    {
        return $this->getName();
    }
}

Now I can create from dashboard of nelmio a question and into an answer. But in database, my answer doesnt have saved the relation with question.

{
  "name": "my new question number 1",
  "answers": [
    {
          "id": "ddb66b71-5523-4158-9aa3-2691cae9d473",
          "name": "my answer 1 to question number 1"
    }
  ]
}

And other question is... I've changed my id of answer by a guid, because I get and error when I create and answer into question without id. Can I create a question, and answers without to specify an id ?

Thanks in advance

Upvotes: 5

Views: 8372

Answers (2)

Well, I see some errors in your entities... first since the relation between Question and Answer entity is a OneToMany, your Qestion entity should have this implemented, the:

use ApiPlatform\Core\Annotation\ApiProperty;

//..... the rest of your code

/**
 * @ApiProperty(
 *    readableLink=true
 *    writableLink=true
 * )
 * @Groups({"question"})
 * @ORM\OneToMany(targetEntity="Answer", mappedBy="question", cascade={"persist"})
 */
private $answers;


public function __construct()
{
     //....

      $this->answers = new ArrayCollection();

     //...
}

public function addAnswer(Answer $answer): self
{
     if (!$this->answers->contains($answer)) {
           $this->answers[] = $answer;
           $answer->setQuestion($this) 
     }

     return $this;
}

public function removeAnswer(Answer $answer): self
{
    if ($this->answers->contains($answer)) {
        $this->answers->removeElement($answer);
    }
    return $this;
}

the command

PHP bin/console make:entity

allows you to create a field in your entity of type relation and it creates these methods for you just follow the instructions (after both entities are created, use the command for update Question entity...)

the readableLink ApiProperty annotation is for see embedded object on GET request, is the same if you use serialization groups, if you set it on false then the response will look like this:

{
  "name": "my new question number 1",
  "answers": [
    "/api/answers/1",
    "/api/answers/2",
    ....
  ]
}

it is used to make responses smaller (among other things)... and the writableLink is for allowing POST request like this (see this example for more info here):

{
  "name": "my new question number 1",
  "answers": [
    {
          "id": "ddb66b71-5523-4158-9aa3-2691cae9d473",
          "name": "my answer 1 to question number 1"
    }
  ]
}

of course, using the corresponding serialization groups in each entity... in ApiPlatform the embedded objects are persisted through setters and getters method but also add and remove methods for OneToMany relations, and the ORM does the rest of the work. let me know if this helps. Cheers!

Upvotes: 1

AythaNzt
AythaNzt

Reputation: 1057

For the first point, it should persist in database without problem. Anyway, you could create a PostValidateSubscriber for Question entity and check if the relation is there.

<?php /** @noinspection PhpUnhandledExceptionInspection */

namespace App\EventSubscriber;

use ApiPlatform\Core\EventListener\EventPriorities;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;

final class QuestionPostValidateSubscriber implements EventSubscriberInterface
{
    private $tokenStorage;

    public function __construct(
        TokenStorageInterface $tokenStorage
    ) {
        $this->tokenStorage = $tokenStorage;
    }
    /**
     * {@inheritdoc}
     */
    public static function getSubscribedEvents()
    {
        return [
            KernelEvents::VIEW => ['checkQuestionData', EventPriorities::POST_VALIDATE]
        ];
    }

    /**
     * @param GetResponseForControllerResultEvent $event
     */
    public function checkQuestionData(GetResponseForControllerResultEvent $event)
    {
        $bid = $event->getControllerResult();
        $method = $event->getRequest()->getMethod();

        if (!$question instanceof Question || (Request::METHOD_POST !== $method && Request::METHOD_PUT !== $method))
            return;

        $currentUser = $this->tokenStorage->getToken()->getUser();
        if (!$currentUser instanceof User)
            return;
    }
}

And do an echo or use xdebug for check question.

For the second point, you can add these annotations for the id of entities, so the id's will generate theirself.

  • @ORM\GeneratedValue()
  • @ORM\Column(type="integer")

Upvotes: 0

Related Questions