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 SubscribeAsí que cuando dos recursos están relacionados en nuestra API, aparecen como una cadena IRI, o colección de cadenas. Pero podrías preguntarte:
Oye, ¿podríamos incluir los datos de
DragonTreasure
aquí mismo en lugar del IRI para que no tenga que hacer una segunda, tercera o cuarta petición para obtener esos datos?
Por supuesto Y, de nuevo, también puedes hacer algo realmente genial con Vulcain... pero aprendamos a incrustar datos.
Cuando se serializa el objeto User
, utiliza los grupos de normalización para determinar qué campos incluir. En este caso, tenemos un grupo llamadouser:read
. Por eso se devuelven email
, username
y dragonTreasures
.
... 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 | |
} |
Para transformar la propiedad dragonTreasures
en datos incrustados, tenemos que ir aDragonTreasure
y añadir este mismo grupo user:read
al menos a un campo. Observa: encima de name
, añade user:read
. Luego... ve hacia abajo y añade también esto para 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 | |
} |
Sí, en cuanto tengamos al menos una propiedad dentro de DragonTreasure
que esté en el grupo de normalización user:read
, el aspecto del campo dragonTreasures
cambiará totalmente.
Observa: cuando lo ejecutemos... ¡impresionante! En lugar de una matriz de cadenas IRI, es una matriz de objetos, con name
y value
... y, por supuesto, los campos normales @id
y @type
.
Así que: cuando tengas un campo de relación, se representará como una cadena IRI o como un objeto... y esto depende totalmente de tus grupos de normalización.
Intentemos esto mismo en la otra dirección. Tenemos un treasure
cuyo id es 2. Dirígete a la ruta GET un único tesoro... pruébalo... e introduce 2 para el id.
Sin sorpresa, vemos owner
como una cadena IRI. ¿Podríamos convertirla en un objeto incrustado? ¡Por supuesto! Sabemos que DragonTreasure
utiliza el grupo de normalización treasure:read
. Así que, entra en User
y añádelo a la propiedad username
: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 | |
} |
Sólo con ese cambio... cuando lo probemos... ¡sí! ¡El campo owner
acaba de transformarse en un objeto incrustado!
Vale, vamos a buscar también una colección de treasures
: sólo hay que pedirlos todos. Gracias al cambio que acabamos de hacer, la propiedad owner
de cada tesoro es ahora un objeto.
Esto me da una idea descabellada. ¿Y si disponer de toda la información de owner
cuando obtengo un único DragonTreasure
está bien... pero tal vez resulte exagerado que esos datos se devuelvan desde la ruta de recogida? ¿Podríamos incrustar owner
al obtener un único treasure
... pero utilizar la cadena IRI al obtener una colección?
La respuesta es... ¡no! Estoy bromeando, ¡por supuesto! ¡Podemos hacer las locuras que queramos! Aunque, cuantas más cosas raras añadas a tu API, más complicada se vuelve la vida. ¡Así que elige bien tus aventuras!
Hacer esto es un proceso de dos pasos. Primero, en DragonTreasure
, busca la operación Get
, que es la operación para obtener un único tesoro. Una de las opciones que puedes pasar a una operación es normalizationContext
... que anulará la predeterminada. Añade normalizationContext
, luego groups
ajustado al estándar treasure:read
. A continuación, añade un segundo grupo específico para esta operación: 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 | |
} |
Puedes llamarlo como quieras... pero a mí me gusta esta convención: nombre del recurso seguido de item
o collection
y luego el método HTTP, como get
o post
.
Y sí, olvidé la clave groups
: lo arreglaré en un minuto.
En cualquier caso, si hubiera codificado esto correctamente, significaría que cuando se utilice esta operación, el serializador incluirá todos los campos que estén al menos en uno de estos dos grupos.
Ahora podemos aprovechar eso. Copia el nuevo nombre del grupo. Luego, en User
, encima deusername
, en lugar de treasure:read
, pega ese nuevo grupo.
... 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 | |
} |
¡Vamos a comprobarlo! Prueba de nuevo con la ruta GET. ¡Sí! Volvemos a owner
que es una cadena IRI. Y si probamos con el punto final GET uno... oh, el propietario es... ¿también un IRI aquí? Culpa mía. Volviendo a normalization_context
olvidé decir groups
. Básicamente estaba poniendo dos opciones sin sentido ennormalization_context
.
Intentémoslo de nuevo. Esta vez... ¡lo tengo!
Cuando te pones así, es un poco más difícil saber qué grupos de serialización se están utilizando y cuándo. Aunque puedes utilizar el Perfilador para ayudarte con eso. Por ejemplo, ésta es nuestra petición más reciente para el tesoro único.
Si abrimos el perfilador para esa petición... y bajamos a la sección Serializador, vemos los datos que se están serializando... pero, lo que es más importante, el contexto de normalización... incluido groups
establecido en los dos que esperamos.
Esto también es genial porque puedes ver otras opciones de contexto que establece la API Platform. Éstas controlan ciertos comportamientos internos.
Siguiente: vamos a volvernos locos con nuestras relaciones utilizando una ruta DragonTreasure
para cambiar el campo username
del propietario de ese tesoro. 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.