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 SubscribeHe 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.
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 CheeseListing
es 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!
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á!
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 User
completamente 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.
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
?
// 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
}
}