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 SubscribeCuando dos recursos están relacionados entre sí, esto puede expresarse de dos maneras diferentes en una API. La primera es con IRIs, básicamente un "enlace" al otro recurso. No podemos ver los datos del CheeseListing
relacionado, pero si los necesitamos, podríamos hacer una segunda petición a esta URL y... ¡boom! Lo tenemos.
Pero, por motivos de rendimiento, podrías decir:
¿Sabes qué? No quiero tener que hacer una petición para obtener los datos del usuario y luego otra petición para obtener los datos de cada lista de quesos que posean: ¡Quiero obtenerlos todos de una vez!
Y eso describe la segunda forma de expresar una relación: en lugar de devolver sólo un enlace a un listado de quesos, ¡puedes incrustar sus datos justo dentro!
Como recordatorio, cuando normalizamos un User
, incluimos todo en el grupouser:read
. Así que eso significa $email
, $username
y $cheeseListings
, que es la razón por la que esa propiedad aparece en absoluto.
Para hacer que esta propiedad devuelva datos, en lugar de sólo un IRI, esto es lo que tienes que hacer: entra en la entidad relacionada -así que CheeseListing
- y añade este grupo user:read
a al menos una propiedad. Por ejemplo, añade user:read
por encima de $title
... y qué tal también por encima de $price
.
... lines 1 - 37 | |
class CheeseListing | |
{ | |
... lines 40 - 46 | |
/** | |
... line 48 | |
* @Groups({"cheese_listing:read", "cheese_listing:write", "user:read"}) | |
... lines 50 - 55 | |
*/ | |
private $title; | |
... lines 58 - 65 | |
/** | |
... lines 67 - 69 | |
* @Groups({"cheese_listing:read", "cheese_listing:write", "user:read"}) | |
... line 71 | |
*/ | |
private $price; | |
... lines 74 - 194 | |
} |
¡A ver qué pasa! Ni siquiera necesitamos actualizar, sólo ejecutar. ¡Vaya! En lugar de una matriz de cadenas, ¡ahora es una matriz de objetos! Bueno, este usuario sólo posee un CheeseListing, pero te haces una idea. Cada elemento tiene los estándares @type
y @id
más las propiedades que hayamos añadido al grupo: title
y price
.
Es muy sencillo: el serializador sabe que debe serializar todos los campos del grupouser:read
. Primero busca en User
y encuentra email
, username
ycheeseListings
. Luego sigue adelante y, dentro de CheeseListing
, encuentra ese grupo en title
y price
.
Esto significa que cada propiedad de la relación puede ser una cadena -el IRI- o un objeto. Y un cliente de la API puede distinguir la diferencia. Si recibe un objeto, sabe que tendrá @id
, @type
y algunas otras propiedades de datos. Si obtienes una cadena, sabes que es un IRI que puedes utilizar para obtener los datos reales.
Podemos hacer lo mismo en el otro lado de la relación. Utiliza los documentos para obtener el CheeseListing
con id = 1
. ¡Sí! La propiedad owner
es una cadena. Pero puede ser conveniente que el JSON de CheeseListing contenga al menos el nombre de usuario del propietario... para que no tengamos que ir a buscar todo el Usuario sólo para mostrar quién es el propietario.
Dentro de CheeseListing
, el proceso de normalización serializará todo en el grupo cheese_listing:read
. Copiadlo. La propiedad owner
, por supuesto, ya tiene este grupo encima, por eso lo vemos en nuestra API. Dentro de User
, busca$username
... y añade cheese_listing:read
a eso.
... lines 1 - 22 | |
class User implements UserInterface | |
{ | |
... lines 25 - 51 | |
/** | |
... line 53 | |
* @Groups({"user:read", "user:write", "cheese_listing:read"}) | |
... line 55 | |
*/ | |
private $username; | |
... lines 58 - 184 | |
} |
¡Vamos a probar esto! Muévete hacia atrás y... ¡Ejecuta! Y... ¡ja! Perfecto! Se expande a un objeto e incluye el username
.
¿Funciona si obtenemos la colección de listados de quesos? ¡Pruébalo! Bueno... vale, ahora mismo sólo hay un CheeseListing
en la base de datos, pero ¡claro! Incrusta el propietario de la misma manera.
Así que... sobre eso... ¡nuevo reto! ¿Qué pasa si queremos incrustar los datos de owner
cuando obtengo un solo CheeseListing
... pero, para que la respuesta no sea gigantesca... no queremos incrustar los datos cuando obtenemos la colección. ¿Es posible?
Totalmente De nuevo, para CheeseListing
, cuando normalizamos, incluimos todo en el grupo cheese_listing:read
. Esto es así independientemente de si estamos obteniendo la colección de listados de quesos o sólo obtenemos un único artículo. Pero, un montón de cosas -incluidos los grupos- pueden cambiarse operación por operación.
Por ejemplo, en itemOperations
, rompe la configuración de la operación get
en varias líneas y añade normalization_context
. Una de las cosas complicadas de la configuración aquí es que las claves de nivel superior son minúsculas, comonormalizationContext
. Pero las claves más profundas suelen ser mayúsculas y minúsculas, comonormalization_context
. Eso... puede ser un poco incoherente, y es fácil estropearlo. Ten cuidado.
En cualquier caso, el objetivo es anular el contexto de normalización, pero sólo para esta operación. Establece esto en la normalidad groups
y en otra matriz. Dentro, vamos a decir:
Cuando se obtiene un único elemento, quiero incluir todas las propiedades que tiene el grupo
cheese_listing:read
como es normal. Pero también quiero incluir todas las propiedades de un nuevo grupocheese_listing:item:get
.
... lines 1 - 16 | |
/** | |
* @ApiResource( | |
... line 19 | |
* itemOperations={ | |
* "get"={ | |
* "normalization_context"={"groups"={"cheese_listing:read", "cheese_listing:item:get"}}, | |
* }, | |
... line 24 | |
* }, | |
... lines 26 - 32 | |
* ) | |
... lines 34 - 38 | |
*/ | |
class CheeseListing | |
... lines 41 - 198 |
Hablaremos de ello más adelante, pero estoy utilizando una convención de nomenclatura específica para este grupo específico de la operación: el "nombre de la entidad", dos puntos, el elemento o la colección, dos puntos, y luego el método HTTP: get
, post
, put
, etc.
Si volvemos a obtener un único CheeseListing
.... no hay ninguna diferencia: estamos incluyendo un nuevo grupo para la serialización - yaaaay - pero no hay nada en el nuevo grupo.
Aquí está la magia. Copia el nombre del nuevo grupo, abre User
, y sobre la propiedad$username
, sustituye cheese_listing:read
por cheese_listing:item:get
.
... lines 1 - 22 | |
class User implements UserInterface | |
{ | |
... lines 25 - 51 | |
/** | |
... line 53 | |
* @Groups({"user:read", "user:write", "cheese_listing:item:get"}) | |
... line 55 | |
*/ | |
private $username; | |
... lines 58 - 184 | |
} |
Ya está Vuelve a la documentación y busca un solo CheeseListing
. Y... perfecto - sigue incorporando al propietario - ahí está el nombre de usuario. Pero ahora, cierra eso y ve al punto final de la colección GET. ¡Ejecuta! ¡Sí! ¡El propietario vuelve a ser un IRI!
Estos grupos de serialización pueden ser un poco complejos de pensar, pero vaya si son potentes.
A continuación... cuando obtenemos un CheeseListing
, algunos de los datos del propietario se incrustan en la respuesta. Así que... tengo una pregunta un poco loca: cuando actualizamos un CheeseListing
... ¿podríamos actualizar también algunos datos del propietario enviando datos incrustados? Um... ¡sí! Eso a continuación.
So it does work, I was just too tunnel vision'ed to see why, I had two @ApiResource annotations.
Remember to check your annotations people, IDE won't help you here hahaha.
Hey Musa
Yeah, that's the problem with annotations, they're hard to validate. If you're on PHP8 you may want to consider migrating to PHP attributes
Cheers!
I have an interesting scenario:
I would like to return some object of class A, but include only IRI of B.
The way I see it would have to mark property in HasTimestamp with @Group
But either way, I get:
`
{
"@type": "A",
"@id": "/api/a/1",
"mtime": "some datetime", <-- This comes from the HasTimetamp
... other simple properties
"b": {
"@type": "B",
"@id": "/api/b/1",
"mtime": "mtime": "some datetime", <-- This also comes from the HasTimetamp
}
}
`
Is there any way to tell the Serializer not to go down the object B, for this property, despite the @Groups?
Thanks! :)
I think I've got the it, but I would really appreciate any thoughts on this. I wouldn't want to go down any hackish solution :)
Just added `@ApiProperty(readableLink=false)` to property `b` of class A. Does that make sense?
But this comes at a cost: What if I wanted to get only B's title (and not just IRI)? This brings me back to square one :(
Hey Jovan P.!
Hmm, interesting problem. You're correct that this should be handled via @Group... but I guess the tricky part is that the properties are shared in the trait, and so you can't give the HasTimestamp properties different groups in each entity. If you were using Yaml or XML config for API Platform this wouldn't be a problem, but I don't think that you can mix them (have most config in annotations, and just one property in Yaml/XML).
About readableLink=false
, from my understanding of that option - https://github.com/api-platform/core/issues/479 - I think your solution is correct/valid. But I see your point that this completely turns things into an IRI... but if you wanted to include an embedded object but avoid the "mtime" field, then that's still not possible. You effectively want to be able to change the "group" of a property in an object at runtime, and I'm not sure that's possible (well, probably possible, but maybe not easy/nice).
The only solution I can think of is to:
A) put no groups into HasTimestamp so that they are not serialized
B) Inside A, add getModifiedTime() that returns mtime and put the A:read group above this. Repeat the same for B and put B:read above that.
The downside is more work :). The upside is that you are explicitly controlling what fields you want serialized.
Let me know what you think!
Cheers!
I actually very much like the idea of having explicit control with methods. Nice! :) The whole time I was chasing the solution by annotating the properties, but that sounds much more elegant even if that means just a bit more of code.
Thanks Ryan! :)
How to increase embedded form depth? because i think default is 2
i added "userQueue:next" in @Groups in all related entities but only first relation is embedded document, rest are IRI
Hey Daniel K.
You can define the depth by using the MaxDepth
annotation. You can check it on the docs here (almost at the bottom of the topic): https://api-platform.com/docs/core/serialization/#using-serialization-groups
Cheers!
Hi, it's possible to use MaxDepth with sorting value ?
Example.. i have user and want also embedded data as last 5 transactions or transaction for 30 days.
Is this possible by annotation or i need to serape api requests to solve this scenario?
Hey @A. Mikolaj!
Hmm. I’m not sure about MaxDepth and how that fits into things. But here is how I’d solve this.
I would create, for example, a getRecentTransactions() method. Inside, filter the collection how you want (last 5 or last 30 days). You can use Doctrine’s criteria system to do this efficiently (without querying for ALL the transactions... only to limit them later). Details at https://symfonycasts.com/sc...
Once you’ve done this, you can expose this as a field via @Groups().
Does this help? I feel like I might be missing a detail - because I can’t think of how MaxDepth fits in here. So let me know :).
Cheers!
Hi all,
I have a many to many relation to itself (via a join table).
And i think this is the challenge.
Relation is working just fine as long as i only show the links to the relation.
As soon as i want to show the related data, i get this error.
<blockquote> "hydra:title": "An error occurred",
"hydra:description": "The total number of joined relations has exceeded the specified maximum. Raise the limit if necessary, or use the \"max_depth\" option of the Symfony serializer.",</blockquote>
Do i need to use 'max depth' and if so where do i put it, in the 'parent' or the 'child'? I've tried, but i keep getting the message.
Thanks in advance.
In the mean time I have made an entity for this join table, because i need some extra fields in this join table.
So now i have a one-to-many and a many-to-one relation to this join table.
This doesn't solve the problem ofcourse.
I think i solved it<spoiler>`
/**
Hey truuslee
That's a tricky one, you made me dig but didn't find anything out of the box. In this thread they recommend to create a custom normalizer for such cases. https://github.com/api-plat...
I hope it helps. 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
}
}
Hey! Going out on a limb here.
In the sorely missed polymorphism I've previously used in laravel projects, I've created a unidirectional relationship with a "price" entity that many products have a manyToOne relationship to (@ORM\ManyToOne(targetEntity=Price::class)).
This works for all intents and purposes, but the embed does not seem to work, with the right groups in the right places, the relation still returns an IRI.
Since I'm not using traditional relationships where everything is bidirectional (although doctrine seems to think uni is fine), any chance someone knows/can confirm, that unidirectional relationships do not possess this functionality?