3
votes

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!

1
Can you provide Api Platform entity annotation of your Submission class? - S.LT
The error message seems to explain that you pass by the Entity Manager to interact with the database. - S.LT
It's cascade persist error, since system doesn't know what surveyData to update. There are two ways to fix it, one is to prefetch this object in Deserializer rather than in denormalizer whenever request comes or pass IRI of surveryData along the put / patch request so api platform knows which embeded object you are talking about. At the end persister requires an object managed by doctrine. Better way of doing this is to write DTOs and manage persister of that DTO manually, that allows you to implement transformation logic without going in depth of API core logic and skip all the complexity. - Maulik Parmar
@S.LT: I've added the Submission entity annotations. - BrianV

1 Answers

1
votes

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.