Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Relaciones incrustadas

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.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Así 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.

Incrustar Vs IRI mediante Grupos de Normalización

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
#[ApiResource(
normalizationContext: ['groups' => ['user:read']],
... line 19
)]
... lines 21 - 22
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
... lines 25 - 30
#[Groups(['user:read', 'user:write'])]
... lines 32 - 33
private ?string $email = null;
... lines 35 - 46
#[Groups(['user:read', 'user:write'])]
... line 48
private ?string $username = null;
... lines 50 - 51
#[Groups(['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
#[Groups(['treasure:read', 'treasure:write', 'user:read'])]
... lines 61 - 63
private ?string $name = null;
... lines 65 - 75
#[Groups(['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 @idy @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.

Incrustar en la otra direcció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
#[Groups(['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!

Incrustado para una ruta, IRI para otra

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 owneral 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
#[ApiResource(
... 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
#[Groups(['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 ownerque 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.

Leave a comment!

6
Login or Register to join the conversation
Carlos-33 Avatar
Carlos-33 Avatar Carlos-33 | posted hace 10 días | edited

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.

Reply

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!

Reply
Jeremy Avatar
Jeremy Avatar Jeremy | posted hace 13 días | edited

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

Reply

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!

Reply
Jean-tilapin Avatar
Jean-tilapin Avatar Jean-tilapin | posted hace 6 meses

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 :)

Reply

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!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// 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
    }
}
userVoice