Search code examples
phpsymfonyapi-platform.com

How to save a Nested Relation with Entity on API Platform


I have two entities, Question and Alternative where Question has a OneToMany relation with Alternative and I'm trying to send a JSON with a nested document of Alternative on it via POST to Question API Platform.

The API Platform returns that error below :

Nested documents for "alternatives" attribute are not allowed. Use IRIs instead.

Searching about it I've found some people saying that is only possible using IRIs and some other people saying that is possible to use Denormalization and Normalization contexts to solve this problem but I can't find some example or tutorial about it.

TL;DR;

Is there a way to send a nested relation into an entity POST on API Platform without using IRIs?

UPDATE:

As asked, please see below the two mappings of Question and Alternative entities.

Question

<?php

namespace App\Entity;

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

/**
 * @ORM\Entity(repositoryClass="App\Repository\QuestionRepository")
 * @ApiResource()
 */
class Question implements CreatedAtEntityInterface, UpdatedAtEntityInterface
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\ManyToOne(targetEntity="App\Entity\Token", inversedBy="questions")
     * @ORM\JoinColumn(nullable=false)
     * @Assert\NotBlank()
     */
    private $token;

    /**
     * @ORM\ManyToOne(targetEntity="App\Entity\Question", inversedBy="question_versions")
     */
    private $question;

    /**
     * @ORM\OneToMany(targetEntity="App\Entity\Question", mappedBy="question")
     */
    private $question_versions;

    /**
     * @ORM\Column(type="integer")
     * @Assert\NotBlank()
     */
    private $version;

    /**
     * @ORM\Column(type="string", length=100)
     * @Assert\Length(max="100")
     * @Assert\NotBlank()
     *
     */
    private $name;

    /**
     * @ORM\Column(type="integer")
     * @Assert\NotBlank()
     */
    private $type;

    /**
     * @ORM\Column(type="text", length=65535)
     * @Assert\NotBlank()
     * @Assert\Length(max="65535")
     */
    private $enunciation;

    /**
     * @ORM\Column(type="string", length=255, nullable=true)
     * @Assert\Length(max="255")
     */
    private $material;

    /**
     * @ORM\Column(type="text", length=65535, nullable=true)
     * @Assert\Length(max="65535")
     */
    private $tags;

    /**
     * @ORM\Column(type="boolean")
     * @Assert\NotNull()
     */
    private $public;

    /**
     * @ORM\Column(type="boolean")
     * @Assert\NotNull()
     */
    private $enabled;

    /**
     * @ORM\Column(type="datetime")
     * @Assert\DateTime()
     */
    private $createdAt;

    /**
     * @ORM\Column(type="datetime")
     * @Assert\DateTime()
     */
    private $updatedAt;

    /**
     * @ORM\OneToMany(targetEntity="App\Entity\Alternative", mappedBy="question")
     */
    private $alternatives;

    /**
     * @ORM\OneToMany(targetEntity="App\Entity\QuestionProperty", mappedBy="question")
     */
    private $properties;

    /**
     * @ORM\OneToMany(targetEntity="App\Entity\QuestionAbility", mappedBy="question")
     */
    private $abilities;

    /**
     * @ORM\OneToMany(targetEntity="QuestionCompetency", mappedBy="question")
     */
    private $competencies;
}

Alternative

<?php

namespace App\Entity;

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

/**
 * @ORM\Entity(repositoryClass="App\Repository\AlternativeRepository")
 * @ORM\Table(name="alternatives")
 * @ApiResource()
 */
class Alternative implements CreatedAtEntityInterface, UpdatedAtEntityInterface
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\ManyToOne(targetEntity="App\Entity\Question", inversedBy="alternatives")
     * @ORM\JoinColumn(nullable=false)
     * @Assert\NotBlank()
     */
    private $question;

    /**
     * @ORM\Column(type="text", length=65535)
     * @Assert\NotBlank()
     * @Assert\Length(max="65535")
     */
    private $enunciation;

    /**
     * @ORM\Column(type="datetime")
     * @Assert\DateTime()
     */
    private $createdAt;

    /**
     * @ORM\Column(type="datetime")
     * @Assert\DateTime()
     */
    private $updatedAt;

    /**
     * @ORM\OneToMany(targetEntity="App\Entity\AlternativeProperty", mappedBy="alternatives")
     */
    private $properties;
}

Solution

  • you can try implement Denormalization

    Question:

    <?php
    
    namespace App\Entity;
    
    use ApiPlatform\Core\Annotation\ApiResource;
    use DateTimeInterface;
    use Doctrine\Common\Collections\ArrayCollection;
    use Doctrine\Common\Collections\Collection;
    use Doctrine\ORM\Mapping as ORM;
    use Symfony\Component\Validator\Constraints as Assert;
    
    /**
     * @ORM\Entity(repositoryClass="App\Repository\QuestionRepository")
     * @ApiResource(
     *     denormalizationContext={"groups"={"post"}}
     * )
     */
    class Question implements CreatedAtEntityInterface, UpdatedAtEntityInterface
    {
        /**
         * @ORM\Id()
         * @ORM\GeneratedValue()
         * @ORM\Column(type="integer")
         */
        private $id;
    
        /**
         * @ORM\ManyToOne(targetEntity="App\Entity\Token", inversedBy="questions")
         * @ORM\JoinColumn(nullable=false)
         * @Assert\NotBlank()
         * @Groups({"post"})
         */
        private $token;
    
        /**
         * @ORM\ManyToOne(targetEntity="App\Entity\Question", inversedBy="question_versions")
         * @Groups({"post"})
         */
        private $question;
    
        /**
         * @ORM\OneToMany(targetEntity="App\Entity\Question", mappedBy="question")
         * @Groups({"post"})
         */
        private $question_versions;
    
        /**
         * @ORM\Column(type="integer")
         * @Assert\NotBlank()
         * @Groups({"post"})
         */
        private $version;
    
        /**
         * @ORM\Column(type="string", length=100)
         * @Assert\Length(max="100")
         * @Assert\NotBlank()
         * @Groups({"post"})
         */
        private $name;
    
        /**
         * @ORM\Column(type="integer")
         * @Assert\NotBlank()
         * @Groups({"post"})
         */
        private $type;
    
        /**
         * @ORM\Column(type="text", length=65535)
         * @Assert\NotBlank()
         * @Assert\Length(max="65535")
         * @Groups({"post"})
         */
        private $enunciation;
    
        /**
         * @ORM\Column(type="string", length=255, nullable=true)
         * @Assert\Length(max="255")
         * @Groups({"post"})
         */
        private $material;
    
        /**
         * @ORM\Column(type="text", length=65535, nullable=true)
         * @Assert\Length(max="65535")
         * @Groups({"post"})
         */
        private $tags;
    
        /**
         * @ORM\Column(type="boolean")
         * @Assert\NotNull()
         * @Groups({"post"})
         */
        private $public;
    
        /**
         * @ORM\Column(type="boolean")
         * @Assert\NotNull()
         * @Groups({"post"})
         */
        private $enabled;
    
        /**
         * @ORM\Column(type="datetime")
         * @Assert\DateTime()
         * @Groups({"post"})
         */
        private $createdAt;
    
        /**
         * @ORM\Column(type="datetime")
         * @Assert\DateTime()
         * @Groups({"post"})
         */
        private $updatedAt;
    
        /**
         * @ORM\OneToMany(targetEntity="App\Entity\Alternative", mappedBy="question")
         * @Groups({"post"})
         */
        private $alternatives;
    
        /**
         * @ORM\OneToMany(targetEntity="App\Entity\QuestionProperty", mappedBy="question")
         * @Groups({"post"})
         */
        private $properties;
    
        /**
         * @ORM\OneToMany(targetEntity="App\Entity\QuestionAbility", mappedBy="question")
         * @Groups({"post"})
         */
        private $abilities;
    
        /**
         * @ORM\OneToMany(targetEntity="QuestionCompetency", mappedBy="question")
         * @Groups({"post"})
         */
        private $competencies;
    }
    

    Alternative:

    <?php
    
    namespace App\Entity;
    
    use ApiPlatform\Core\Annotation\ApiResource;
    use DateTimeInterface;
    use Doctrine\Common\Collections\ArrayCollection;
    use Doctrine\Common\Collections\Collection;
    use Doctrine\ORM\Mapping as ORM;
    use Symfony\Component\Validator\Constraints as Assert;
    
    /**
     * @ORM\Entity(repositoryClass="App\Repository\AlternativeRepository")
     * @ORM\Table(name="alternatives")
     * @ApiResource()
     */
    class Alternative implements CreatedAtEntityInterface, UpdatedAtEntityInterface
    {
        /**
         * @ORM\Id()
         * @ORM\GeneratedValue()
         * @ORM\Column(type="integer")
         */
        private $id;
    
        /**
         * @ORM\ManyToOne(targetEntity="App\Entity\Question", inversedBy="alternatives")
         * @ORM\JoinColumn(nullable=false)
         * @Assert\NotBlank()
         * @Groups({"post"})
         */
        private $question;
    
        /**
         * @ORM\Column(type="text", length=65535)
         * @Assert\NotBlank()
         * @Assert\Length(max="65535")
         * @Groups({"post"})
         */
        private $enunciation;
    
        /**
         * @ORM\Column(type="datetime")
         * @Assert\DateTime()
         * @Groups({"post"})
         */
        private $createdAt;
    
        /**
         * @ORM\Column(type="datetime")
         * @Assert\DateTime()
         * @Groups({"post"})
         */
        private $updatedAt;
    
        /**
         * @ORM\OneToMany(targetEntity="App\Entity\AlternativeProperty", mappedBy="alternatives")
         * @Groups({"post"})
         */
        private $properties;
    }
    

    Now you can this send JSON to POST/PUT without IRI:

    {
        "token": "/api/tokens/1",
        "question": "string",
        "version": "string",
        "name": "string",
        "type": 0,
        "enunciation": "string",
        "public": true,
        "enabled": true,
        "alternatives": [
            {
                "enunciation": "String",
                "createdAt": "2018-01-01 11:11:11",
                "updatedAt": "2018-01-01 11:11:11"
            }
        ]
    }