Creating Embedded Objects

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Instead of assigning an existing CheeseListing to the user, could we create a totally new one by embedding its data? Let's find out!

This time, we won't send an IRI string, we'll send an object of data. Let's see... we need a title and... I'll cheat and look at the POST endpoint for cheeses. Right: we need title, price owner and description. Set price to 20 bucks and pass a description. But I'm not going to send an owner property. Why? Well... forget about API Platform and just imagine you're using this API. If we're sending a POST request to /api/users to create a new user... isn't it pretty obvious that we want the new cheese listing to be owned by this new user? Of course, it's our job to actually make this work, but this is how I would want it to work.

Oh, and before we try this, change the email and username to make sure they're unique in the database.

Ready? Execute! It works! No no, I'm totally lying - it's not that easy. We've got a familiar error:

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

Allowing Embedded cheeseListings to be Denormalized

Ok, let's back up. The cheeseListings field is writable in our API because the cheeseListings property has the user:write group above it. But if we did nothing else, this would mean that we can pass an array of IRIs to this property, but not a JSON object of embedded data.

To allow that, we need to go into CheeseListing and add that user:write group to all the properties that we want to allow to be passed. For example, we know that, in order to create a CheeseListing, we need to be able to set title, description and price. So, let's add that group! user:write above title, price and... down here, look for setTextDescription()... and add it there.

... lines 1 - 39
class CheeseListing
{
... lines 42 - 48
/**
... line 50
* @Groups({"cheese_listing:read", "cheese_listing:write", "user:read", "user:write"})
... lines 52 - 57
*/
private $title;
... lines 60 - 67
/**
... lines 69 - 71
* @Groups({"cheese_listing:read", "cheese_listing:write", "user:read", "user:write"})
... line 73
*/
private $price;
... lines 76 - 134
/**
... lines 136 - 137
* @Groups({"cheese_listing:write", "user:write"})
... line 139
*/
public function setTextDescription(string $description): self
... lines 142 - 197
}

I love how clean it is to choose which fields you want to allow to be embedded... but life is getting more complicated. Just keep that "complexity" cost in mind if you decide to support this kind of stuff in your API

Cascade Persist

Anyways, let's try it! Ooh - a 500 error. We're closer! And we know this error too!

A new entity was found through the User.cheeseListings relation that was not configured to cascade persist.

Excellent! This tells me that API Platform is creating a new CheeseListing and it is setting it onto the cheeseListings property of the new User. But nothing ever calls $entityManager->persist() on that new CheeseListing, which is why Doctrine isn't sure what to do when trying to save the User.

If this were a traditional Symfony app where I'm personally writing the code to create and save these objects, I'd probably just find where that CheeseListing is being created and call $entityManager->persist() on it. But because API Platform is handling all of that for us, we can use a different solution.

Open User, find the $cheeseListings property, and add cascade={"persist"}. Thanks to this, whenever a User is persisted, Doctrine will automatically persist any CheeseListing objects in this collection.

... lines 1 - 22
class User implements UserInterface
{
... lines 25 - 58
/**
* @ORM\OneToMany(targetEntity="App\Entity\CheeseListing", mappedBy="owner", cascade={"persist"})
... line 61
*/
private $cheeseListings;
... lines 64 - 184
}

Ok, let's see what happens. Execute! Woh, it worked! This created a new User, a new CheeseListing and linked them together in the database.

But who set CheeseListing.owner?

But... how did Doctrine... or API Platform know to set the owner property on the new CheeseListing to the new User... if we didn't pass an owner key in the JSON? If you create a CheeseListing the normal way, that's totally required!

This works... not because of any API Platform or Doctrine magic, but thanks to some good, old-fashioned, well-written code in our entity. Internally, the serializer instantiated a new CheeseListing, set data on it and then called $user->addCheeseListing(), passing that new object as the argument. And that code takes care of calling$cheeseListing->setOwner() and setting it to $this User. I love that: our generated code from make:entity and the serializer are working together. What's gonna work? Team work!

Embedded Validation

But, like when we embedded the owner data while editing a CheeseListing, when you allow embedded resources to be changed or created like this, you need to pay special attention to validation. For example, change the email and username so they're unique again. This is now a valid user. But set the title of the CheeseListing to an empty string. Will validation stop this?

Nope! It allowed the CheeseListing to save with no title, even though we have validation to prevent that! That's because, as we talked about earlier, when the validator processes the User object, it doesn't automatically cascade down into the cheeseListings array and also validate those objects. You can force that by adding @Assert\Valid().

... lines 1 - 22
class User implements UserInterface
{
... lines 25 - 58
/**
... lines 60 - 61
* @Assert\Valid()
*/
private $cheeseListings;
... lines 65 - 185
}

Let's make sure that did the trick: go back up, bump the email and username to be unique again and... Execute! Perfect! A 400 status code because:

the cheeseListings[0].title field should not be blank.

Ok, we've talked about how to add new cheese listings to an user - either by passing the IRI of an existing CheeseListing or embedding data to create a new CheeseListing. But what would happen if a user had 2 cheese listings... and we made a request to edit that User... and only included the IRI of one of those listings? That should... remove the missing CheeseListing from the user, right? Does that work? And if so, does it set that CheeseListing's owner to null? Or does it delete it entirely? Let's find some answers next!

Leave a comment!

  • 2019-08-26 weaverryan

    Hey Christopher Hoyos!

    Ha! Nice question. Tough question :). I think the answer is... yes! Um, maybe :P.

    So, I tried this using this tutorial. Specifically, a made a PUT request to /api/users/8 and tried to update the "price" field on an existing cheeseListing with this body


    {
    "cheeseListings": [
    {
    "@id": "/api/cheeses/1",
    "price": 500
    }
    ]
    }

    This DOES work. Assuming you've got all of your serialization groups set up (in this case, a user:write group is used when deserializing a User object and I've also added that same group to the CheeseListing.price property so that it can be updated), then it works just fine. This is a bit of a different result than you were getting... and I'm not sure why (well, your request and response body didn't look quite right to me - there should be an extra { } around each item inside the collection [] but I wasn't sure if that was a typo).

    But, apart from needing the groups to be set up correctly, there is one catch: if the User has 2 cheese listings and you only want to edit one of them, you'll need to make sure you include ALL the cheese listings in the request, else the others will be removed. Something like this:


    {
    "cheeseListings": [
    {
    "@id": "/api/cheeses/1",
    "price": 500
    },
    "/api/cheeses/4"
    ]
    }

    Personally, to manage complexity, I'd *prefer* to update these individual cheese listings by making a PUT request to /api/cheeses/1 instead of trying to do it all inside on request to update the user. But, I also understand that if you're refactoring a giant form... it may be more natural to combine it all at once. But, it's something to think about :). And now that your form is submitting via JavaScript, you could even start updating each CheeseListing (or whatever your other resource really is) on "blur" - i.e. when the user clicks off a field, send a PUT request right then to update just that one item.

    Cheers!

  • 2019-08-22 Christopher Hoyos

    Is it possible to update individual items in a collection during a PUT operation? Similar to how the CollectionType Field would updated enities if the id was present.

    The body of my request (PUT /resource/uri/1) follows this structure:

    {
    "a_prop": "some value",
    "collection": [
    "@id": "/another_resources/uri/1",
    "another_prop": "new value"
    ]
    }

    The response will always return new uri for the item in the collection that is been updated (the previous one is getting deleted form db).
    Resonse would look like this:

    {
    ...
    "collection": [
    "@id": "/another_resources/uri/2", // new uri
    "another_prop": "new value" // Updated value
    ]
    }

    I know the scenerario might be a little odd, but its all part of a old big form which the user may need to go back to and edit after it was already persisted to db. I'm considering splitting up the form to handle edits on a diferent view, but if possible i would like to avoid it. I wonder if its a configuration that i am missing (similar to the allow_add, allow_remove on the CollectionType), or is just not possible.