Chapters
-
Course Code
Subscribe to download the code!Compatible PHP versions: ^7.1.3
Subscribe to download the code!Compatible PHP versions: ^7.1.3
-
This Video
Subscribe to download the video!
Subscribe to download the video!
-
Subtitles
Subscribe to download the subtitles!
Subscribe to download the subtitles!
-
Course Script
Subscribe to download the script!
Subscribe to download the script!
Scroll down to the script below, click on any sentence (including terminal blocks) to jump to that spot in the video!
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 SubscribeI just tried to create a CheeseListing
by setting the owner
property to 1: the id of a real user in the database. But... it didn't like it! Why? Because in API Platform and, commonly, in modern API development in general, we do not use ids to refer to resources: we use IRIs. For me, this was strange at first... but I quickly fell in love with this. Why pass around integer ids when URLs are so much more useful?
Check out the response of the user we just created: like every JSON-LD response, it contains an @id
property... that isn't an id, it's an IRI! And this is what you'll use whenever you need to refer to this resource.
Head back up to the CheeseListing
POST operation and set owner
to /api/users/1
. Execute that. This time... it works!
And check it out, when it transforms the new CheeseListing
into JSON, the owner
property is that same IRI. That is why Swagger documents this as a "string"... which isn't totally accurate. Sure, on the surface, owner
is a string... and that's what Swagger is showing in the cheeses-Write
model.
But we know... with our human brains, that this string is special: it actually represents a "link" to a related resource. And... even though Swagger doesn't quite understand this, check out the JSON-LD documentation: at /api/docs.jsonld
. Let's see, search for owner. Ha! This is a bit smarter: JSON-LD knows that this is a Link... with some fancy metadata to basically say that the link is to a User
resource.
The big takeaway is this: a relation is just a normal property, except that it's represented in your API with its IRI. Pretty cool.
Adding cheesesListings to User
What about the other side of the relationship? Use the docs to go fetch the CheeseListing
with id = 1. Yep, here's all the info, including the owner
as an IRI. But what if we want to go the other direction?
Let's refresh to close everything up. Go fetch the User
resource with id 1. Pretty boring: email
and username
. What if you also want to see what cheeses this user has posted?
That's just as easy. Inside User
find the $username
property, copy the @Groups
annotation, then paste above the $cheeseListings
property. But... for now, let's only make this readable: just user:read
. We're going to talk about how you can modify collection relationships later.
Show Lines
|
// ... lines 1 - 22 |
class User implements UserInterface | |
{ | |
Show Lines
|
// ... lines 25 - 58 |
/** | |
Show Lines
|
// ... line 60 |
* @Groups("user:read") | |
*/ | |
private $cheeseListings; | |
Show Lines
|
// ... lines 64 - 184 |
} |
Ok, refresh and open the GET item operation for User. Before even trying this, it's already advertising that it will now return a cheeseListings
property, which, interesting, will be an array of strings. Let's see what User
id 1 looks like. Execute!
Ah.. it is an array! An array of IRI strings - of course. By default, when you relate two resources, API Platform will output the related resource as an IRI or an array of IRIs, which is beautifully simple. If the API client needs more info, they can make another request to that URL.
Or... if you want to avoid that extra request, you could choose instead to embed the cheese listing data right into the user resource's JSON. Let's chat about that next.
28 Comments
Hi @sidi!
Excellent question! When make a request for the user data, to get the comments data, API Platform simply calls $user->getComments()
, which returns all 50 results. To limit things, you can use this trick: https://symfonycasts.com/screencast/api-platform-security/filtered-collection#adding-getpublishedcheeselistings
Except that for performance (so you don't query for all 50 comments... only then to reutrn 3), you should use the Criteria system explained here - https://symfonycasts.com/screencast/doctrine-relations/collection-criteria
Let me know if that helps!
Cheers!
Thanks Ryan, But where should I place the Criteria? in getComments function?
Hey @sidi!
Yes, exactly! Or, sometimes, to keep the getComments() method pure (and returning all comments) in case I need to call it somewhere else in my code, I will create another method - like getMostRecentComments() - and then use the Groups and SerializedName annotations to expose this method as the “comments” field in your api.
Cheers!
Great! Thank you very much weaverryan it's very clear ;)
Hi. One question.
How do you force return an IRI in a relation if the relation target is the same class of the current object(a recursive relation).
Like a "parent" or "child" if you have for example a "Folder" entity that have a parent "Folder" and several child "Folders" how do you tell for example that for the parent return the IRI and for the childs, return only the "name".
Hey David R.!
Wow, that's an excellent question, and not one that I've thought of before! It should be simple, but unless I'm completely missing something, it is not simple. To accomplish this, I needed to create a custom normalizer.
To test this, I created a parent->child relationship with the CheeseListing from this tutorial - CheeseListing.parentCheese is ManyToOne to CheeseListing.childCheeses. Here is the final CheeseNormalizer:
<?php
namespace App\Serializer\Normalizer;
use App\Entity\CheeseListing;
use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface;
use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
class CheeseNormalizer implements ContextAwareNormalizerInterface, CacheableSupportsMethodInterface, NormalizerAwareInterface
{
use NormalizerAwareTrait;
private const ALREADY_CALLED = 'USER_NORMALIZER_ALREADY_CALLED';
public function normalize($object, $format = null, array $context = array())
{
$context[self::ALREADY_CALLED] = true;
// set the groups to nothing so that no data is serialized from
// the child object
$context['groups'] = [];
// not sure why this is needed. This is normally added
// by the JSON-LD ObjectNormalizer, but that doesn't seem to
// be used... and the logic here is quite hard to follow.
// so, I added it myself - it's the flag that converting to an
// IRI is ok
$context['api_empty_resource_as_iri'] = true;
$data = $this->normalizer->normalize($object, $format, $context);
$context[self::ALREADY_CALLED] = false;
return $data;
}
public function supportsNormalization($data, $format = null, array $context = [])
{
// avoid recursion: only call once per object
if (isset($context[self::ALREADY_CALLED])) {
return false;
}
// api_attribute is a context key set to the property being normalized
return $data instanceof CheeseListing
&& isset($context['api_attribute'])
&& in_array($context['api_attribute'], ['parentCheese', 'childCheeses']);
}
public function hasCacheableSupportsMethod(): bool
{
return false;
}
}
With this, parentCheese is an IRI and childCheeses is an array of IRI's. This really should be simpler (and maybe it is somehow?) but this is the only way I could sort it out.
Let me know if that helps!
Cheers!
Hi Ryan, I think that there is there some code missing in your answer :S
Hey David R.
Yeah, looks like Ryan's copy-paste function is eating some bytes :p
I believe this is the piece of code he's missing
class CheeseNormalizer implements ContextAwareNormalizerInterface, NormalizerAwareInterface
{
public function normalize($object, $format = null, array $context = [])
{
$data = $this->normalizer->normalize($object, $format, $context);
$context[self::ALREADY_CALLED] = false;
return $data;
}
...
}
You can read more about Serializers/Normilizers here: https://api-platform.com/docs/core/serialization/#changing-the-serialization-context-dynamically
Cheers!
Hi, I am trying to use this code, but it doesn't work, the result is the same, and also I don't understand it, for me as I read it this code "does nothing", it returns true only when the fields to normalize are parentCheese and childCheeses, but does nothing on normailze. Am I missing something?
Bah! Sorry about that, let me try posting the code again, but on a gist this time: https://gist.github.com/wea...
The key thing is that supports() returns true ONLY when are normalizing a CheeseListing under a parentCheese or childCheeses property (supports() will return false when normalizing the main CheeseListing object). Then, when normalize() is called, we change the serialization groups to an empty array so that *nothing* on those children CheeseListing objects is serialized.
Hopefully now that the full code is showing, it'll make more sense. Sorry about missing the code for you - I think Disqus may have swallowed it :/
Cheers!
Hi Ryan, Tried your solution, now with the full code and now I get your point, but still no luck.
Now I receive the parent as an empty array, and the childs as an array of empty arrays. Emptying the context groups is working, as nothing gets normalized when these properties are being normalized, but the "api_empty_resource_as_iri" seems not to be working.
I've added some dumps, and I see that ApiPlatform AbstractItemNormalized (where the api_empty_resource_as_iri logic is located) is being called before my custom normalizer, so the context in AbstractItemNormalized doesn't have this property.
Hey David R.!
Well... darn it! Let's see :). I just tried the code again in my app, and it's working perfectly - you can see screenshots of the correct behavior for parent and children here: https://imgur.com/a/KiWfTAu
So, I'm not sure what's different in your case. I also upgraded to the latest api-platform/core version (2.5.) and it still worked. You mentioned:
I've added some dumps, and I see that ApiPlatform AbstractItemNormalized (where the api_empty_resource_as_iri logic is located)
is being called before my custom normalizer, so the context in AbstractItemNormalized doesn't have this property
I would double-check this. I don't doubt what you're saying, but there is are multiple levels of recursion - iirc, AbstractItemNormalizer will be called once for the top level CheeseListing, then again for the user property (for my example) and THEN for the parentCheese or childCheeses properties.
In general, because we're decorating the normalizer, our normalizer should be an "outer" normalizer (with the rest of the normalization system inside the $this->normalizer
property. That means that our normalizer (assuming supports returns true) should always be called first and that WE are in fact calling (indirectly) the AbstractItemNormalizer via the $data = $this->normalizer->normalize($object, $format, $context);
To verify that things are working as expected, I might comment-out that line and replace it with $data = ['testing' => true];
. If everything is working correctly, then YOUR normalizer should be called for parentCheese and childCheese, and you should see data that looks like this:
"childCheeses": [
{
"testing": true
}
],
The recursive & decorated nature of the normalizers is, honestly, one of my least favorite features of API Platform - it's confusing. I'd prefer if I could "hook into" the normalizing process to "tweak" something, but not be responsible for calling the "inner" normalizer and managing the self::ALREADY_CALLED
flag to avoid infinite recursion. Hopefully that's something we can clean up in the serializer component at some point.
Let me know if that helps!
Cheers!
Finally I've found the issue, after checking that adding $data = ['testing' => true];
was working, I've copied all your code from gist, and just replaced, the class name and it worked. After that, comparing line by line, the error was that I hadn't added the NormalizerAwareInterface
to my Normalizer
, and as I had the constructor with the ObjectNormalizer
, it wasn't working properly.
Now it works, I removed the constructor and used the NormalizerAwareTrait
with the NormalizerAwareInterface
and the api is returning the IRIs properly.
Also in my function i had public function normalize(...): array
, I had to remove the :array
part that was added on creating the normalizer with make:serlializer:normalizer,
so PHP doesn't complaint when returning a string.
Thank you very much!! :)
Dear Ryan!
I dare say that the solution offered <a href="https://gist.github.com/weaverryan/3b2da11198e3bb012c7c9698ef9248ef">in the mentioned gist</a> is now not working (at least for me). For children and the parent the $this->normalizer
is null
.
When I introduced this code at the very beginning of the normalize
method, all parents and children were marked with the "no normalizer" string instead of the IRI.
if (is_null($this->normalizer)) {
return 'no normalizer';
}
On the side note, I understand that entity is generally expected to have one Serializer per entity. To confirm this my guess, I cannot have several `@SerializedName
` annotations for one entity property. I use Serializer not only for API Platform, so I'd like to have a more granulated control for a serialized property name for different serialization groups. Is there a case to expect this?
Could you please confirm that the example in the gist doesn't need a correction now?
PS. I was given a piece of advice (actually 2 pieces): to use service decoration for my custom normalizer <a href="#https://api-platform.com/docs/core/serialization/#decorating-a-serializer-and-adding-extra-data">as described here</a> and to introduce some annotations for the "children" and "parent" properties, i.e. [ApiProperty(readableLink: false, writableLink: false)]
or @ApiProperty(readableLink = false, writableLink = false)
. After experimenting, I factored out some methods in a separate trait. Here is <a href="#https://gist.github.com/voltel/ac2820fc7f97892999162774452a97fa">the gist that you might find helpful</a>.
Hey Volodymyr T.!
Sorry for the slow reply - busy week :).
Decoration is probably a good idea - I can't remember exactly why I chose or didn't choose to do that with the gist that you linked to above. I'm effectively using decoration, because by using the NormalizerAwareTrait to "do some work then call the normalizer system", but at this moment, the decoration looks simpler. In the 2nd part of the series we use decoration for context builders, but not for normalizers. I may have missed a "simpler solution" for normalizers.
> I dare say that the solution offered in the mentioned gist is now not working (at least for me). For children and the parent the
> $this->normalizer is null.
> When I introduced this code at the very beginning of the normalize method, all parents and children were marked with the "no
> normalizer" string instead of the IRI.
I'm not sure about this part. The NormalizerAwareTrait should cause the serializer system to "set" the normalizer before it's executed. I don't know why that wouldn't happen. But the decoration strategy doesn't need to rely on this - so it seems better to me.
Anyways, it sounds like you've got it working now? I've just posted a link to your gist from my gist. As I'm replying late, let me know if you still have any problems or questions :).
Cheers!
I'm using a postgres database and I have a table called `stations` with a column called `markets`. This column type is an array and contains a collection of market ID's. I also have a market table. I've created an entity for the station and market tables. One station can be associated with many markets. How do I create a relationship in the station entity to markets? I'm assuming this can be achieved using some annotation magic but I haven't been able to figure it out. I can't change the structure of the DB as it was created before using the API Platform.
I should also include that the returned values from the station.markets column looks something like this...{12,14,...}
I want to be able to get a markets object as well as post/put/patch market values to the stations endpoint.
Hey Ben B.!
Hmm, so it sounds like you have a bit of a messy database structure. I don't mean that to sound bad - you mentioned that you can't change the DB structure - that's a reality that we often need to work in :). In a more perfect world, the "stations" table would have a true relation to the "markets" table through a join table. That would then all be mapped correctly on Doctrine (as a ManyToMany relationship) and API Platform would be happy.
> I want to be able to get a markets object
Because you have a Markets entity, this part should already be ok. I'm guessing this is not a problem ;)
> as well as post/put/patch market values to the stations endpoint.
This IS a problem... probably. Questions:
A) For your Station resource, do you want a markets JSON field to be returned? If so, do you want it to be the array of ids? Array of IRIs? Embedded Market objects?
B) For post/put/patch of the Station resource... if your markets property is an array of ids... then you should be able to simply send an array of ids on the markets property and it will work. There is no referential integrity in the database or anything... but I think it would be that simple. But... I think I may be missing something - let me know if I am ;).
Cheers!
And here is another question from me on a sunday. I hope you don't mind. I'm pretty exited about api platform and can't wait to dazzle people with it.
Case:
A user has many cheeselistings.
Api platform gives me the user with all the data of all the cheelistings
url : /api/user/2
`
"cheeseListings": [
{
"@id": "/api/cheeses/9",
"@type": "cheeses",
"title": "nice cheddar cheese",
"price": 1000
},
...
]
`
But what url can i use to get a specific cheeselist of this user. With a filter? /api/user/2?cheeselist_id =....
And will i still get all the data of this cheese list?
Oke, that's all folks! Have a good sunday Cheers !
Hey truuslee
If you know the CheeseListing id, I think you can just do a get request to "/api/cheeselist/{id}".
Cheers!
Hey Diego, thanks for the reply. 👌
What I mean is: when you do /api/user/2
the response contains the user data and if you want, all the data of all the related 'cheeselistings'. My question is if i can filter on the cheeselistings while using the /api/user/2
call
Thanks for your help in advance.
Ah, I get it now and yes, you can add a filter to your User resource based on its CheeseListing field. Try something like this:
// User.php
/**
* @ApiFilter(SearchFilter::class, properties={
* "cheeseListing": "exact",
* ...
* })
*/
class User
Hi guys, great work as usual !
I have a product owner that wants the api to have urls like this:
/api/v1/user/{cust_id}/orders/{order_id}
/api/v1/user/{cust_id}/products/{product_id}/specs
There are relations between all data.
My gut feeling says it's way better to do it like this:
/api/v1/orders/{order_id}?customer_id={customer_id}
/api/v1/products/{product_id}?customer_id={customer_id}
So the api is useful for many other projects.
What is your opinion about this? I look forward to your answer.
Thank you and please keep up the good work !!!
Hey truuslee
Could you tell me why this structure is better /api/v1/orders/{order_id}?customer_id={customer_id}
than the other one? Other projects could just follow that structure.
What I know about the first structure is that that's the standard structure of RESTful APIs, that might be the reason of your boss but you may want to ask him, so you know the real reason behind it and act accordingly.
Cheers!
Hi Diego, my feeling was right. I just watched the chapter about subresources. And it is recommended to keep things simple and not use subresources. You can easily do the same with the filter options.
I purchased a sub to symfonycasts to learn about api-platform... because we are considering using api-platform for an upcoming project we have. Unforunately we won't be able to use IRIs for relations on this project, so I'm hoping this is configurable so I can use regular plain ids. Is this a configurable option for api-platform yet? I found some issues related to this on github but was confused about whether a solution was ever found. Also I would like a supported solution and not a workaround where I have to do something hacky/fragile.
Hey Isaac E.
Welcome to SymfonyCasts! About your question, you made me dug and looks like it's a topic that have been active for quite long. The latest info I could find is this comment: https://github.com/api-plat...
Seems like that guy find a solution. Give it a try and let us know if it worked for you
Cheers!
"Houston: no signs of life"
Start the conversation!
What PHP libraries does this tutorial use?
// composer.json
{
"require": {
"php": "^7.1.3",
"ext-ctype": "*",
"ext-iconv": "*",
"api-platform/core": "^2.1", // v2.4.3
"composer/package-versions-deprecated": "^1.11", // 1.11.99
"doctrine/annotations": "^1.0", // 1.10.2
"doctrine/doctrine-bundle": "^1.6", // 1.11.2
"doctrine/doctrine-migrations-bundle": "^2.0", // v2.0.0
"doctrine/orm": "^2.4.5", // v2.7.2
"nelmio/cors-bundle": "^1.5", // 1.5.5
"nesbot/carbon": "^2.17", // 2.19.2
"phpdocumentor/reflection-docblock": "^3.0 || ^4.0", // 4.3.1
"symfony/asset": "4.2.*|4.3.*|4.4.*", // v4.3.11
"symfony/console": "4.2.*", // v4.2.12
"symfony/dotenv": "4.2.*", // v4.2.12
"symfony/expression-language": "4.2.*|4.3.*|4.4.*", // v4.3.11
"symfony/flex": "^1.1", // v1.21.6
"symfony/framework-bundle": "4.2.*", // v4.2.12
"symfony/security-bundle": "4.2.*|4.3.*", // v4.3.3
"symfony/twig-bundle": "4.2.*|4.3.*", // v4.2.12
"symfony/validator": "4.2.*|4.3.*", // v4.3.11
"symfony/yaml": "4.2.*" // v4.2.12
},
"require-dev": {
"symfony/maker-bundle": "^1.11", // v1.11.6
"symfony/stopwatch": "4.2.*|4.3.*", // v4.2.9
"symfony/web-profiler-bundle": "4.2.*|4.3.*" // v4.2.9
}
}
Hi!
I can't limit the results get from a relationship.
I Explain my problem, I have a relationship like this:
Let's say that the user whose Id 199 has 50 comments.
I want when I call a this uri : http://localhost/api/user/199
I get a maximum 3 comments and no more per request,
I don't know how to do that someone has an idea?
Thanks