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.
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.
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!
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 @Carlos-33,
Interesting, as far as I know there is no limit... is it 3 different entities? Can you re-check if 3rd entity has correct group set on fields you want to read, if it's like a Tree with single entity try #[MaxDepth(2)]
attribute to configure serialisation process
Cheers!
Hi! Thank you for all these awesome courses! I love it, I stopped my netflix subscription as I prefer to chill on SymfonyCast ;)
Aren't serialization groups going whild when you have to add role access to differents fields of the entity, when you have several roles (admin / seller / customer / public...)? How do you deal with this? Isn't it starting to be a mess in Entities? And when your API is growing with 10 or 20 entities with relations that you need to embed? Maybe you don't embed anymore and rely on Vulcain in this case?
Cheers
Edit : Oh sorry I was in too much of a hurry, all my questions seems to have an answer in the next part : https://symfonycasts.com/screencast/api-platform-security
Thank you very much
Hey Jeremy!
Haha, I hope SymfonyCasts is more interesting for you ;)
So it seems you found the answers in the next chapters - great, I'm happy to hear it :)
Cheers!
Hello there!
Quick question when you have some extra time.
In your exact configuration, when you make a PUT request on the USER, to change for example its username, what do you get in the response, specifically in the embedded collection? Do you get the extra fields you configured or just the IRIs of the treasures owned by the User?
In my very similar case, when I GET the parent, it works just fine, the extra fields of the Children are there. But when I update the parent entity, the configurated fields of the children are systemically replaced by the unique IRIs, breaking all the UI using those fields (for example an Image I display using its name).
I really don't get what I'm doing wrong and don't find where to look for my mistake.
Thanks :)
Hi Jean!
I might know the problem you're referring to! Are you, like we do in this chapter, adding an extra (de)normalization group for one specific operation? Like, in this chapter, we add treasure:item:get
to just the Get
operation. Are you doing something like that?
If so, the problem is that when you make a GET
request, it will (of course) normalize with the extra group - e.g. treasure:item:get
. But when you make a PUT
request, it will first look for the denormalization groups to "read" the JSON you're sending. Then it will look on the Put
operation to see what normalization groups it should use. If you've set things up like we did in this chapter, then the Put
request will NOT have the extra treasure:item:get
. The solution would be to add that extra group also to the PUT
operation. Heck, if you care enough, you might even add it to the Post
operation so that the extra fields are returned even after you "create" a resource.
Let me know if that's the problem - I wondered while I was recording this if that would trick some people.
Cheers!
// composer.json
{
"require": {
"php": ">=8.1",
"ext-ctype": "*",
"ext-iconv": "*",
"api-platform/core": "^3.0", // v3.0.8
"doctrine/annotations": "^1.0", // 1.14.2
"doctrine/doctrine-bundle": "^2.8", // 2.8.0
"doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.2
"doctrine/orm": "^2.14", // 2.14.0
"nelmio/cors-bundle": "^2.2", // 2.2.0
"nesbot/carbon": "^2.64", // 2.64.1
"phpdocumentor/reflection-docblock": "^5.3", // 5.3.0
"phpstan/phpdoc-parser": "^1.15", // 1.15.3
"symfony/asset": "6.2.*", // v6.2.0
"symfony/console": "6.2.*", // v6.2.3
"symfony/dotenv": "6.2.*", // v6.2.0
"symfony/expression-language": "6.2.*", // v6.2.2
"symfony/flex": "^2", // v2.2.4
"symfony/framework-bundle": "6.2.*", // v6.2.3
"symfony/property-access": "6.2.*", // v6.2.3
"symfony/property-info": "6.2.*", // v6.2.3
"symfony/runtime": "6.2.*", // v6.2.0
"symfony/security-bundle": "6.2.*", // v6.2.3
"symfony/serializer": "6.2.*", // v6.2.3
"symfony/twig-bundle": "6.2.*", // v6.2.3
"symfony/ux-react": "^2.6", // v2.6.1
"symfony/validator": "6.2.*", // v6.2.3
"symfony/webpack-encore-bundle": "^1.16", // v1.16.0
"symfony/yaml": "6.2.*" // v6.2.2
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.2
"symfony/debug-bundle": "6.2.*", // v6.2.1
"symfony/maker-bundle": "^1.48", // v1.48.0
"symfony/monolog-bundle": "^3.0", // v3.8.0
"symfony/stopwatch": "6.2.*", // v6.2.0
"symfony/web-profiler-bundle": "6.2.*", // v6.2.4
"zenstruck/foundry": "^1.26" // v1.26.0
}
}
Hi everyone! I'm currently using the API Platform for my personal project. However, I'm facing an issue. I have three entity relations, which means three levels of embedded relations with normalizationContext groups. The problem is that I can only retrieve data object from the 2 stages. The format of the third stage is a URI and not object.