Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Escritura 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

He aquí una cuestión interesante: si recuperamos un solo CheeseListing, podemos ver que el username aparece en la propiedad owner. Y, obviamente, si nosotros, editamos un CheeseListing concreto, podemos cambiar totalmente el propietario por otro distinto. En realidad, probemos esto: establezcamos owner a /api/users/2. Ejecuta y... ¡sí! ¡Se ha actualizado!

Eso es genial, y funciona más o menos como una propiedad escalar normal. Pero... volviendo a mirar los resultados de la operación GET... aquí está, si podemos leer la propiedad username del propietario relacionado, en lugar de cambiar el propietario por completo, ¿podríamos actualizar el nombre de usuario del propietario actual mientras actualizamos un CheeseListing?

Es un ejemplo un poco raro, pero la edición de datos a través de una relación incrustada es posible... y, como mínimo, es una forma impresionante de entender realmente cómo funciona el serializador.

Intentando actualizar el propietario incrustado

De todos modos... ¡probemos! En lugar de establecer el propietario a un IRI, establécelo a un objeto e intenta actualizar el username a cultured_cheese_head. ¡Vamos, vamos, vamos!

Y... no funciona:

No se permiten documentos anidados para el atributo "owner". Utiliza en su lugar IRIs.

Entonces... ¿es esto posible, o no?

Bueno, la razón por la que username se incrusta al serializar un CheeseListinges que, por encima de username, hemos añadido el grupo cheese_listing:item:get, que es uno de los grupos que se utilizan en la operación "obtener" el elemento.

La misma lógica se utiliza cuando se escribe un campo, o se desnormaliza. Si queremos queusername se pueda escribir mientras se desnormaliza un CheeseListing, tenemos que ponerlo en un grupo que se utilice durante la desnormalización. En este caso, escheese_listing:write.

Cópialo y pégalo encima de username.

... lines 1 - 22
class User implements UserInterface
{
... lines 25 - 51
/**
... line 53
* @Groups({"user:read", "user:write", "cheese_listing:item:get", "cheese_listing:write"})
... line 55
*/
private $username;
... lines 58 - 184
}

En cuanto lo hagamos -porque la propiedad owner ya tiene este grupo- ¡se podrá escribir la propiedad username incrustada! Volvamos a intentarlo: seguimos intentando pasar un objeto con username. ¡Ejecuta!

Envío de nuevos objetos frente a referencias en JSON

Y... oh... ¡sigue sin funcionar! ¡Pero el error es fascinante!

Se ha encontrado una nueva entidad a través de la relación CheeseListing.owner que no estaba no estaba configurada para realizar operaciones de persistencia en cascada para la entidad Usuario.

Si llevas un tiempo en Doctrine, puede que reconozcas este extraño error. Ignorando por un momento la Plataforma API, significa que algo creó un objeto User totalmente nuevo, lo estableció en la propiedad CheeseListing.owner y luego intentó guardar. Pero como nadie llamó a $entityManager->persist() en el nuevo objetoUser, ¡Doctrine entró en pánico!

Así que... ¡sí! ¡En lugar de consultar el propietario existente y actualizarlo, la Plataforma API tomó nuestros datos y los utilizó para crear un objeto User totalmente nuevo! ¡Eso no es en absoluto lo que queríamos! ¿Cómo podemos decirle que actualice el objeto User existente en su lugar?

Aquí está la respuesta, o en realidad, aquí está la regla simple: si enviamos una matriz de datos, o en realidad, un "objeto" en JSON, la Plataforma API asume que se trata de un nuevo objeto y así... crea un nuevo objeto. Si quieres indicar que, en cambio, quieres actualizar un objeto existente, sólo tienes que añadir la propiedad @id. Establécela como/api/users/2. Gracias a esto, la Plataforma API consultará ese usuario y lo modificará.

Vamos a probarlo de nuevo. ¡Funciona! Bueno... probablemente ha funcionado: parece que ha tenido éxito, pero no podemos ver el nombre de usuario aquí. Desplázate hacia abajo y busca el usuario con id 2.

¡Ahí está!

¿Crear nuevos usuarios?

Así pues, ahora sabemos que, al actualizar... o realmente crear... un CheeseListing, podemos enviar los datos de owner incrustados y señalar a la Plataforma API que debe actualizar un owner existente a través de la propiedad @id.

Y cuando no añadimos @id, intenta crear un nuevo objeto User... que no funciona por ese error de persistencia. Pero, podemos arreglar totalmente ese problema con un persist en cascada... que mostraré en unos minutos para resolver un problema diferente.

Entonces, espera... ¿significa esto que, en teoría, podríamos crear un Usercompletamente nuevo mientras editamos un CheeseListing? La respuesta es.... ¡sí! Bueno... casi. Hay dos cosas que lo impiden ahora mismo: primero, la falta de persistencia de la cascada, que nos dio ese gran error de Doctrine. Y en segundo lugar, en User, también tendríamos que exponer los campos $password y $email porque ambos son necesarios en la base de datos. Cuando empiezas a hacer que las cosas incrustadas sean escribibles, sinceramente se añade complejidad. Asegúrate de llevar un registro de lo que es posible y lo que no es posible en tu API. No quiero que se creen usuarios accidentalmente al actualizar un CheeseListing, así que esto es perfecto.

Validación incrustada

Pero queda una cosa rara. Establece username como una cadena vacía. Eso no debería funcionar porque tenemos un @NotBlank() por encima de $username.

Intenta actualizar de todos modos. Por supuesto Me sale el error 500 en cascada - déjame volver a poner la propiedad @id. Inténtalo de nuevo.

¡Woh! ¡Un código de estado 200! ¡Parece que ha funcionado! Baja y recupera este usuario... con id=2. ¡No tiene nombre de usuario! ¡No te preocupes!

Esto... es un poco de gotcha. Cuando modificamos el CheeseListing, se ejecutan las reglas de validación: @Assert\NotBlank(), @Assert\Length(), etc. Pero cuando el validador ve el objeto owner incrustado, no continúa hacia abajo en ese objeto para validarlo. Eso es normalmente lo que queremos: si sólo estábamos actualizando un CheeseListing, ¿por qué debería intentar validar también un objeto User relacionado que ni siquiera hemos modificado? No debería

Pero cuando haces actualizaciones de objetos incrustados como nosotros, eso cambia: sí queremos que la validación continúe hasta este objeto. Para forzar eso, encima de la propiedad owner, añade @Assert\Valid().

... lines 1 - 39
class CheeseListing
{
... lines 42 - 86
/**
... lines 88 - 90
* @Assert\Valid()
*/
private $owner;
... lines 94 - 197
}

Bien, vuelve atrás y... intenta de nuevo nuestra ruta de edición. Ejecuta. ¡Lo tengo!

owner.username: Este valor no debe estar en blanco

¡Muy bien! Volvamos atrás y démosle un nombre de usuario válido... para no tener un usuario malo en nuestra base de datos. ¡Perfecto!

Poder hacer modificaciones en las propiedades incrustadas está muy bien... pero añade complejidad. Hazlo si lo necesitas, pero recuerda también que podemos actualizar un CheeseListing y un User de forma más sencilla haciendo dos peticiones a dos rutas.

A continuación, vamos a ponernos aún más locos y a hablar de la actualización de colecciones: ¿qué ocurre si intentamos modificar la propiedad cheeseListings directamente en un User?

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