BrianV
BrianV

Reputation: 416

API Platform: Normalizing and Denormalizing inheritance-mapped Doctrine entities

I have an entity type called a Submission. A Submission has an OneToOne relationship to a SurveyData entity type.

The SurveyData entity is actually a mapped superclass. It will eventually have several dozen subclasses for Entities that store the data from different surveys.

As per the documentation, I created a custom Normalizer that handles denormalization based on a type key:

  public function denormalize($data, string $type, string $format = null, array $context = [])
  {
    if ($type === 'App\Entity\SurveyData\SurveyData') {
      $class = 'App\Entity\SurveyData\\' . $data['type'];
      $context['resource_class'] = $class;
    }

    $context[self::ALREADY_CALLED] = true;

    return $this->denormalizer->denormalize($data, 'App\Entity\SurveyData\\' . $data['type'], $format, $context);
  }

With this in place, I can create a new Submission with embedded SurveyData perfectly. Here's an example of the JSON I sent to the POST request:

{
    "facility": "/api/facilities/1",
    "survey": "/api/surveys/monthly_totals",
    "dateDetail": "Q1 2020",
    "surveyData": {
      "type": "MonthlyTotals",
      "num_deliveries": 50,
      "num_cesarean": 30,
      "num_epidural_anesthesia": 15
    },
    "created": "2020-08-14T18:59:49.218Z",
    "updated": "2020-08-14T18:59:49.218Z",
    "user": "brian",
    "status": "complete"
}

When I fetch the collection, or a single Submission entity via GET, however, the response returned by API Platform neglects to add the @id property to the embedded survey response. I'm not sure if this is because it's an OneToOne that can't be blank, so it's internally tracked:

{
            "@id": "/api/submissions/2",
            "@type": "Submission",
            "id": 2,
            "facility": "/api/facilities/1",
            "survey": "/api/surveys/monthly_totals",
            "dateDetail": "Q1 2020",
            "created": "2020-08-14T18:59:49+00:00",
            "updated": "2020-08-14T18:59:49+00:00",
            "user": "brian",
            "status": "complete",
            "surveyData": {
                "num_deliveries": 50,
                "num_cesarean": 30,
                "num_epidural_anesthesia": 15
            }
        }

The real problem is that PUT and PATCH requests fail.

For a PATCH request, I can update fields in the parent Submission entity. However, if I send the below request, the Submission and SurveyData entities get removed from the database, and I get the following error from the API:

"Entity App\\Entity\\Submission@000000002116ebc30000000012ca4827 is not managed. An entity is managed if its fetched from the database or registered as new through EntityManager#persist",

Gist with the entire response including a trace: https://gist.github.com/brianV/c32661186c91b49b013017dde77d5d4a

Here's an example of a PATCH request that triggers the error:

{
    "user": "brian",
    "surveyData": {
        "type": "MonthlyTotals",
        "num_deliveries": 100
    }
}

This happens with every PUT request as well (in which I include the entire replacement Submission entity).

In plain Symfony & Doctrine, this solution would work great, but it appears to break API Platform.

As per a comment request, here is the Submission entity annotations:

/**
 * @ApiResource(
 *   normalizationContext={"groups"={"submission"}},
 *   denormalizationContext={"groups"={"submission"}},
 *   itemOperations={
 *     "get"={
 *       "method"="GET",
 *       "access_control"="is_granted('view', object)",
 *     },
 *     "put", "patch", "delete",
 *   },
 * )
 * @ORM\Entity(repositoryClass="App\Repository\SubmissionRepository")
 * @CustomAssert\SubmissionDataIsValid
 */
class Submission
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     * @Groups({"submission"})
     */
    private $id;

    /**
     * @ORM\ManyToOne(targetEntity="App\Entity\Facility")
     * @ORM\JoinColumn(nullable=false)
     * @Groups({"submission"})
     */
    private $facility;

    /**
     * @ORM\ManyToOne(targetEntity="App\Entity\Patient", inversedBy="submissions")
     * @Groups({"submission"})
     */
    private $patient;

    /**
     * @ORM\Column(type="string", length=255)
     * @Groups({"submission"})
     */
    private $survey;

    /**
     * @ORM\Column(type="string", length=255, nullable=true)
     * @Groups({"submission"})
     */
    private $dateDetail;

    /**
     * @ORM\Column(type="datetime")
     * @Assert\Type("\DateTimeInterface")
     * @Groups({"submission"})
     */
    private $created;

    /**
     * @ORM\Column(type="datetime")
     * @Assert\Type("\DateTimeInterface")
     * @Groups({"submission"})
     */
    private $updated;

    /**
     * @ORM\Column(type="string", length=255)
     * @Groups({"submission"})
     */
    private $user;

    /**
     * @ORM\Column(type="string", length=255)
     * @Groups({"submission"})
     */
    private $status;

    /**
     * @ORM\OneToOne(targetEntity="App\Entity\SurveyData\SurveyData", inversedBy="submission", cascade={"persist", "remove"}, orphanRemoval=true, fetch="EAGER")
     * @Groups({"submission"})
     */
    private $surveyData;

Thanks in advance for any assistance!

Upvotes: 3

Views: 4128

Answers (1)

rugolinifr
rugolinifr

Reputation: 1281

Make sure that your SurveyData property is normalized with the api_platform.jsonld.normalizer.item service when you're working with Submission URLs.

I assume you have followed those steps to embedd your object ? Well, as described here, since you do not provide an @id property within your embedded object, Api-Platform considers that you're pushing a new object instead of editing the current one, and that's why doctrine cries (your error message): this new object is not registered.

The easiest way to register automatically this new object is by adding a cascade={"persist"} annotation property onto your Submission::$surveyData property:

class Submission
{
    /**
     * @OneToOne(targetEntity="App\Entity\SurveyData", cascade={"persist"})
     */
    private $surveyData,
}

But you may also use the EntityManagerInterface::persist() method.

Note: I'm not sure that PATCH methods are fully compatibles with embedded object, I remember some issues on github about that.

Upvotes: 1

Related Questions