Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Relación incrustada

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

Cuando 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!

Incrustar el listado de quesos en el usuario

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:reada 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 @typey @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.

Cadenas de relación frente a objetos

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.

Incrustar el usuario en CheeseListing

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.

Incrustar datos sólo cuando se obtiene un único artículo

¿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 grupo cheese_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.

Leave a comment!

Este tutorial funciona muy bien para Symfony 5 y la Plataforma API 2.5/2.6.

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.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
    }
}