Embedded Relations
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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeSo when two resources are related in our API, they show up as an IRI string, or collection of strings. But you might wonder:
Hey, could we include the
DragonTreasure
data right here instead of the IRI so that I don't need to make a second, third or fourth request to get that data?
Absolutely! And, again, you can also do something really cool with Vulcain... but let's learn how to embed data.
Embedding Vs IRI via Normalization Groups
When the User
object is being serialized, it uses the normalization groups to determine which fields to include. In this case, we have one group called user:read
. That's why email
, username
and dragonTreasures
are all returned.
// ... lines 1 - 16 | |
( | |
normalizationContext: ['groups' => ['user:read']], | |
// ... line 19 | |
) | |
// ... lines 21 - 22 | |
class User implements UserInterface, PasswordAuthenticatedUserInterface | |
{ | |
// ... lines 25 - 30 | |
'user:read', 'user:write']) | ([|
// ... lines 32 - 33 | |
private ?string $email = null; | |
// ... lines 35 - 46 | |
'user:read', 'user:write']) | ([|
// ... line 48 | |
private ?string $username = null; | |
// ... lines 50 - 51 | |
'user:read']) | ([|
private Collection $dragonTreasures; | |
// ... lines 54 - 170 | |
} |
To transform the dragonTreasures
property into embedded data, we need to go into DragonTreasure
and add this same user:read
group to at least one field. Watch: above name
, add user:read
. Then... go down and also add this for value
.
// ... lines 1 - 51 | |
class DragonTreasure | |
{ | |
// ... lines 54 - 59 | |
'treasure:read', 'treasure:write', 'user:read']) | ([|
// ... lines 61 - 63 | |
private ?string $name = null; | |
// ... lines 65 - 75 | |
'treasure:read', 'treasure:write', 'user:read']) | ([|
// ... lines 77 - 78 | |
private ?int $value = 0; | |
// ... lines 80 - 209 | |
} |
Yup, as soon as we have even one property inside of DragonTreasure
that's in the user:read
normalization group, the way the dragonTreasures
field looks will totally change.
Watch: when we execute that... awesome! Instead of an array of IRI strings, it's an array of objects, with name
and value
... and of course the normal @id
and @type
fields.
So: when you have a relation field, it will either be represented as an IRI string or an object... and this depends entirely on your normalization groups.
Embedding the Other Direction
Let's try this same thing in the other direction. We have a treasure
whose id is 2. Head up to the GET a single treasure endpoint... try it... and enter 2 for the id.
No surprise, we see owner
as an IRI string. Could we turn that into an embedded object instead? Of course! We know that DragonTreasure
uses the treasure:read
normalization group. So, go into User
and add that to the username
property: treasure:read
.
// ... lines 1 - 22 | |
class User implements UserInterface, PasswordAuthenticatedUserInterface | |
{ | |
// ... lines 25 - 46 | |
'user:read', 'user:write', 'treasure:read']) | ([|
// ... line 48 | |
private ?string $username = null; | |
// ... lines 50 - 170 | |
} |
With just that change... when we try it... yes! The owner
field just got transformed into an embedded object!
Embedded for One Endpoint, IRI for Another
Ok, let's also fetch a collection of treasures
: just request all of them. Thanks to the change we just made, every single treasure's owner
property is now an object.
That gives me a wild, hare-brained idea. What if having all the owner
information when I fetch a single DragonTreasure
is cool... but maybe it feels like overkill to have that data returned from the collection endpoint. Could we embed the owner
when fetching a single treasure
... but then use the IRI string when fetching a collection?
The answer is... no! I'm kidding - of course! We can do whatever crazy things we want! Though, the more weird things you add to your API, the trickier life gets. So choose your adventures wisely!
Doing this is a two-step process. First in DragonTreasure
, find the Get
operation, which is the operation for fetching a single treasure. One of the options that you can pass into an operation is the normalizationContext
... which will override the default. Add normalizationContext
, then groups
set to the standard treasure:read
. Then add a second group that's specific to this operation: treasure:item:get
.
// ... lines 1 - 25 | |
( | |
// ... lines 27 - 28 | |
operations: [ | |
new Get( | |
normalizationContext: [ | |
'groups' => ['treasure:read', 'treasure:item:get'], | |
], | |
), | |
// ... lines 35 - 38 | |
], | |
// ... lines 40 - 53 | |
) | |
// ... line 55 | |
class DragonTreasure | |
{ | |
// ... lines 58 - 213 | |
} |
You can call this whatever you want... but I like this convention: resource name followed by item
or collection
then the HTTP method, like get
or post
.
And yes, I did forget the groups
key: I'll fix that in a minute.
Anyways, if I had coded this correctly, it would mean that when this operation is used, the serializer will include all fields that are in at least one of these two groups.
Now we can leverage that. Copy the new group name. Then, over in User
, above username
, instead of treasure:read
, paste that new group.
// ... lines 1 - 22 | |
class User implements UserInterface, PasswordAuthenticatedUserInterface | |
{ | |
// ... lines 25 - 46 | |
'user:read', 'user:write', 'treasure:item:get']) | ([|
// ... line 48 | |
private ?string $username = null; | |
// ... lines 50 - 170 | |
} |
Let's check it out! Try the GET collection endpoint again. Yes! We're back to owner
being an IRI string. And if we try the GET one endpoint.. oh, the owner is... also an IRI here too? That's my bad. Back on normalization_context
I forgot to say groups
. I was basically setting two meaningless options into normalization_context
.
Let's try that again. This time... got it!
When you get fancy like this, it does get a bit harder to keep track of what serialization groups are being used and when. Though you can use the Profiler to help with that. For example, this is our most recent request for the single treasure.
If we open the profiler for that request... and go down to the Serializer section, we see the data that's being serialized... but more importantly the normalization context... including groups
set to the two we expect.
This is also cool because you can see other context options that are set by API Platform. These control certain internal behavior.
Next: let's get crazy with our relationships by using a DragonTreasure
endpoint to change the username
field of that treasure's owner. Woh.
Hi, someone else know why normalizationContext & denormalizationContext don't work with Symfony 7 et API Plateform 3.2 ?
When i add this :
API Platform return to me :
If i remove normalization i have :
My entity :
Thanks a lot !