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 SubscribeAcabo de intentar crear un CheeseListing
poniendo la propiedad owner
a 1: el id de un usuario real en la base de datos. Pero... ¡no le ha gustado! ¿Por qué? Porque en la Plataforma API y, comúnmente, en el desarrollo moderno de APIs en general, no utilizamos ids para referirnos a los recursos: utilizamos IRIs. Para mí, esto fue extraño al principio... pero rápidamente me enamoré de esto. ¿Por qué pasar ids enteros cuando las URLs son mucho más útiles?
Mira la respuesta del usuario que acabamos de crear: como toda respuesta JSON-LD, contiene una propiedad @id
... que no es un id, ¡es un IRI! Y esto es lo que utilizarás siempre que necesites referirte a este recurso.
Vuelve a la operación POST CheeseListing
y establece owner
como/api/users/1
. Ejecuta eso. Esta vez... ¡funciona!
Y fíjate, cuando transforma el nuevo CheeseListing
en JSON, la propiedad owner
es ese mismo IRI. Por eso Swagger lo documenta como una "cadena"... lo cual no es del todo exacto. Claro, en la superficie, owner
es una cadena... y eso es lo que muestra Swagger en el modelo cheeses-Write
.
Pero sabemos... con nuestro cerebro humano, que esta cadena es especial: en realidad representa un "enlace" a un recurso relacionado. Y... aunque Swagger no lo entienda del todo, echa un vistazo a la documentación de JSON-LD: en /api/docs.jsonld
. Veamos, busca propietario. ¡Ja! Esto es un poco más inteligente: JSON-LD sabe que se trata de un Enlace... con algunos metadatos extravagantes para decir básicamente que el enlace es a un recurso deUser
.
La gran conclusión es ésta: una relación es sólo una propiedad normal, excepto que se representa en tu API con su IRI. Muy bueno.
¿Qué pasa con el otro lado de la relación? Utiliza los documentos para ir a buscar elCheeseListing
con id = 1. Sí, aquí está toda la información, incluido el owner
como IRI. ¿Pero qué pasa si queremos ir en la otra dirección?
Actualicemos para cerrar todo. Ve a buscar el recurso User
con id 1. Bastante aburrido: email
y username
. ¿Y si también quieres ver qué quesos ha publicado este usuario?
Eso es igual de fácil. Dentro de User
encuentra la propiedad $username
, copia la anotación@Groups
y pégala encima de la propiedad $cheeseListings
. Pero... por ahora, sólo vamos a hacer esto legible: sólo user:read
. Más adelante hablaremos de cómo puedes modificar las relaciones de colección.
... lines 1 - 22 | |
class User implements UserInterface | |
{ | |
... lines 25 - 58 | |
/** | |
... line 60 | |
* @Groups("user:read") | |
*/ | |
private $cheeseListings; | |
... lines 64 - 184 | |
} |
Bien, actualiza y abre la operación de elemento GET para Usuario. Antes de intentarlo, ya anuncia que ahora devolverá una propiedad cheeseListings
que, curiosamente, será un array de cadenas. Veamos qué aspecto tiene User
id 1. ¡Ejecuta!
Ah... ¡es una matriz! Una matriz de cadenas IRI, por supuesto. Por defecto, cuando relacionas dos recursos, la Plataforma API mostrará el recurso relacionado como un IRI o una matriz de IRIs, lo cual es maravillosamente sencillo. Si el cliente de la API necesita más información, puede hacer otra petición a esa URL.
O... si quieres evitar esa petición adicional, puedes optar por incrustar los datos del listado de quesos directamente en el JSON del recurso del usuario. Hablemos de eso a continuación.
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!
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!
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!
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!
// 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.17.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