Crear objetos incrustados
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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login Subscribe¿Es posible crear un DragonTreasure totalmente nuevo cuando creamos un usuario? Como... ¿en lugar de enviar el IRI de un tesoro existente, enviamos un objeto?
¡Vamos a intentarlo! Primero, cambiaré esto por un correo electrónico y un nombre de usuario únicos. Después, paradragonTreasures, borra esos IRI y, en su lugar, pasa un objeto JSON con los campos que sabemos que son obligatorios. ¡Nuestro nuevo usuario dragón acaba de conseguir una copia de GoldenEye para N64! Legendario. Añade un description... y un value.
En teoría, ¡este cuerpo JSON tiene sentido! ¿Pero funciona? Pulsa "Ejecutar" y... ¡no! Bueno, todavía no. ¡Pero conocemos este error!
No se permiten documentos anidados para el atributo
dragonTreasures. Utiliza IRI en su lugar.
Cómo hacer que dragonTreasures acepte objetos JSON
Dentro de User, si nos desplazamos hacia arriba, la propiedad $dragonTreasures es escribible porque tiene user:write.
| // ... lines 1 - 22 | |
| class User implements UserInterface, PasswordAuthenticatedUserInterface | |
| { | |
| // ... lines 25 - 51 | |
| (['user:read', 'user:write']) | |
| private Collection $dragonTreasures; | |
| // ... lines 54 - 170 | |
| } |
Pero no podemos enviar un objeto para esta propiedad porque no hemos añadido user:write a ninguno de los campos dentro de DragonTreasure. Arreglemos eso.
Queremos poder enviar $name, así que añade user:write... Me saltaré $descriptionpero haré lo mismo con $value. Ahora busca setTextDescription() que es la descripción real. Añade user:write aquí también.
| // ... lines 1 - 55 | |
| class DragonTreasure | |
| { | |
| // ... lines 58 - 63 | |
| (['treasure:read', 'treasure:write', 'user:read', 'user:write']) | |
| // ... lines 65 - 67 | |
| private ?string $name = null; | |
| // ... lines 69 - 79 | |
| (['treasure:read', 'treasure:write', 'user:read', 'user:write']) | |
| // ... lines 81 - 82 | |
| private ?int $value = 0; | |
| // ... lines 84 - 138 | |
| (['treasure:write', 'user:write']) | |
| public function setTextDescription(string $description): self | |
| { | |
| // ... lines 142 - 144 | |
| } | |
| // ... lines 146 - 214 | |
| } |
Vale, en teoría, ahora deberíamos poder enviar un objeto incrustado. Si nos dirigimos y lo intentamos de nuevo... ¡obtenemos un error 500!
Se ha encontrado una nueva entidad a través de la relación
User#dragonTreasures
Persistencia de una relación de entidad en cascada
¡Esto es genial! Ya sabemos que cuando envías un objeto incrustado, si incluyes@id, el serializador recuperará primero ese objeto y luego lo actualizará. Pero si no tienes @id, creará un objeto totalmente nuevo. Ahora mismo, está creando un objeto nuevo,... pero nada le ha dicho al gestor de entidades que lo persista. Por eso obtenemos este error.
Para solucionarlo, necesitamos persistir en cascada esta propiedad. En User, en la opciónOneToMany para $dragonTreasures, añade una opción cascade establecida en ['persist'].
| // ... lines 1 - 22 | |
| class User implements UserInterface, PasswordAuthenticatedUserInterface | |
| { | |
| // ... lines 25 - 50 | |
| #[ORM\OneToMany(mappedBy: 'owner', targetEntity: DragonTreasure::class, cascade: ['persist'])] | |
| // ... line 52 | |
| private Collection $dragonTreasures; | |
| // ... lines 54 - 170 | |
| } |
Esto significa que si estamos guardando un objeto User, debería persistir mágicamente cualquier $dragonTreasures que haya dentro. Y si lo probamos ahora... ¡funciona! Es increíble! Y aparentemente, nuestro nuevo tesoro id es 43.
Abramos una nueva pestaña del navegador y naveguemos hasta esa URL... más .json... en realidad, hagamos .jsonld. ¡Estupendo! Vemos que el owner está establecido para el nuevo usuario que acabamos de crear.
¿Cómo se estableció el propietario? De nuevo: Los métodos inteligentes
Pero... ¡aguanta! No enviamos el campo owner en los datos del tesoro... entonces, ¿cómo se estableció ese campo? Bueno, en primer lugar, tiene sentido que no enviáramos un campo owner para el nuevo DragonTreasure... ¡ya que el usuario que será su propietario ni siquiera existía todavía! Vale, entonces, ¿pero quién estableció el owner?
Entre bastidores, el serializador crea primero un nuevo objeto User. Después, crea un nuevo objeto DragonTreasure. Finalmente, ve que el nuevo DragonTreasureaún no está asignado al User, y llama a addDragonTreasure(). Cuando lo hace, el código de aquí abajo establece el owner: tal y como vimos antes. Así que nuestro código bien escrito se está ocupando de todos esos detalles por nosotros.
| // ... lines 1 - 22 | |
| class User implements UserInterface, PasswordAuthenticatedUserInterface | |
| { | |
| // ... lines 25 - 149 | |
| public function addDragonTreasure(DragonTreasure $treasure): self | |
| { | |
| if (!$this->dragonTreasures->contains($treasure)) { | |
| $this->dragonTreasures->add($treasure); | |
| $treasure->setOwner($this); | |
| } | |
| return $this; | |
| } | |
| // ... lines 159 - 170 | |
| } |
Añadir la restricción válida
De todos modos, quizá recuerdes de antes que en cuanto permitimos que un campo de relación envíe datos incrustados... tenemos que añadir una cosita. No lo haré, pero si enviáramos un campo name vacío, se crearía un DragonTreasure... con unname vacío, aunque, por aquí, si nos desplazamos hasta la propiedad name, ¡es obligatorio! Recuerda: cuando el sistema valide el objeto User, se detendrá en$dragonTreasures. No validará también esos objetos. Si quieres validarlos, añade #[Assert\Valid].
| // ... lines 1 - 22 | |
| class User implements UserInterface, PasswordAuthenticatedUserInterface | |
| { | |
| // ... lines 25 - 52 | |
| private Collection $dragonTreasures; | |
| // ... lines 55 - 171 | |
| } |
Ahora que tengo esto, para comprobar que funciona, pulsa "Ejecutar" y... ¡genial! Obtenemos un código de estado 422 que nos indica que name no debería estar vacío. Voy a volver a ponerlo.
Enviar objetos incrustados y cadenas IRI al mismo tiempo
Ahora sabemos que podemos enviar cadenas IRI u objetos incrustados para una propiedad de relación, suponiendo que hayamos configurado los grupos de serialización para permitirlo. E incluso podemos mezclarlos.
Digamos que queremos crear un nuevo objeto DragonTreasure, pero también vamos a robar, tomar prestado, un tesoro de otro dragón. Esto está totalmente permitido. ¡Mira! Cuando pulsamos "Ejecutar"... obtenemos un código de estado 201. Esto devuelve los identificadores de tesoro 44 (que es el nuevo) y 7, que es el que acabamos de robar.
Vale, ya sólo nos queda un capítulo sobre el manejo de las relaciones. Veamos cómo podemos quitar un tesoro a un usuario para eliminar ese tesoro. Eso a continuación.
14 Comments
Help me with
"hydra:description": "The total number of joined relations has exceeded the specified maximum. Raise the limit if necessary with the \"api_platform.eager_loading.max_joins\" configuration key (https://api-platform.com/docs/core/performance/#eager-loading), or limit the maximum serialization depth using the \"enable_max_depth\" option of the Symfony serializer (https://symfony.com/doc/current/components/serializer.html#handling-serialization-depth).",
Hey @GM
That's an unexpected error. Did you change the default config? Did you download the course code from this page?
Cheers!
Do I get it right that i'ts not possible to update an embedded resource?
I tried to call patch with this:
but instead it removed the old one and created the new one completly ignoring @id. As there was no video on updating related entities I get you can only add, assign, delete embedded objects?
Hey @Fireball!
Sorry for the slow reply! I think you’re correct and I think it’s by design.
If you think of the dragonTreasures property like any other property, like an array of “favorite sports”, it starts to make sense. You are updating a user by setting a new value for the dragonTreasures property. For simple values like a string, it’s obvious what should happen to the old value: it’s replaced. For array properties, it’s the same (the new value replaces the old value), but it’s less obvious that this should be the case. Actually, I believe that different PATCH content types can be used to control this behavior, though this is theoretical as API Platform, I believe, doesn’t support any wilder PATCH types.
tl;dr Pretty sure you’re right: this is just an explanation into the “why”. Hopefully it’s at least interesting ;).
Cheers!
Hi! Great work on the courses!
Regarding the coding challenge for this video, asking us which of the options are not required to make the code work, I would argue that this
is not necessarily required for the code to work, it's just good practice to have it. Did I understand it wrong?
Thank you!
Hey @Iulian-L
That's a very good point. Technically you're correct, if we remove validations, the code will still run and will work but only if you submit valid data. If you submit invalid data it will eventually fail when trying to save it into the database, so in my opinion and from the point of view of the feature, validations are required. What do you think?
Cheers!
On the latest version of API Platform (3.2) it looks like Virtual Properties don't seem to work correctly when creating embedded objects. I've added group "user:write" to the "name" and "value" properties in DragonTreasure and they show up in the API Docs correctly and update as expected, however after adding that group to the "setTextDescription" virtual property (with the SerializedName set to "description") it doesn't show up in the docs for the nested object which means a null value error is thrown for the "description" column even if it's added to the payload.
Not sure if it's a bug or configuration issue.
Edit: After further testing it looks like there's just some general weirdness going on with the description property; deleting the virtual property and moving the write groups to it still doesn't allow any User endpoints to update the field. Also adding the "user:read" group to the property doesn't allow the User Get endpoints to see the description. It doesn't make any sense; adding all other fields works exactly as expected so what's so wrong with "description" that it can't be exposed in the nested object?
Edit 2: Turns out that the "description" field is only recognised as writable by if the only group set on the property is "user:write". This makes absolutely no sense and would greatly appreciate any insight into why this would be the case.
Hey @opalint!
So using
#[Groups(['user:write'])]works, right? But#[Groups(['user:write', 'foo:bar'])]suddenly does not work? That, indeed, does not make sense. As soon as you have even 1 group, adding additional groups should only make the field MORE readable or MORE writable... not less-so (unless I'm completely mis-thinking at the moment). So if this is what you are experiencing, that is not the behavior I would expect either and I can't explain it :/Cheers!
Hello,
Is it allowed to update embedded objects in ManyToMany relation?
For now if I do it. There is new object created and swapped to relation. Im not sure if it's correct result, or I'm doing something wrong.
I think same thing happen to me in OneToMany relation. even I add "@id": it's creating new object with new Id and remove old one.
I think there is some different in PUT and PATCH working, seems like PUT works fine for it.
Hey @Lukasz-W!
You're right that
ManyToManyshould work the same asOneToMany: setting the "many" side of the relationship is really the same for both of these cases.Hmm, yea, that should not be happening.
It's possible. So, you have no problems with
PUT? Things work as expected? But as soon as you change toPATCHit always creates a new object? That's possible - but it would be surprising. The logic for that is in the normalizer - https://github.com/api-platform/core/blob/main/src/JsonLd/Serializer/ItemNormalizer.php#L143-L151 - which doesn't, generally-speaking, operate differently based on which operation is being used.So.... I'm not sure what's going on here to be honest - it looks like the wrong behavior...
Cheers!
Oh it's looks like api platform known error
https://github.com/api-platform/core/issues/4293
Adding PatchAwareItemNormalizer seems to fix the problem.
Thanks for answer :)
Ah, good find! So this is a "this is how PATCH is meant to work" situation... which is probably not what you want, but at least we know now!
Hi!
How can you prevent from stealing while allowing to edit collections like you did here?
For now, I prevent writing in collections like this (when there is ownership-like stuff) and manage to handle it in opposite side.
Cheers
Hey @Jeremy!
First, good job spotting this potential issue! Honestly, what you said is the safest and simplest way. We CAN prevent stealing, but it adds complexity (both to the code and... just to my brain, lol). We talk about how to prevent stealing in the next tutorial - https://symfonycasts.com/screencast/api-platform-security/unit-of-work-validator
Cheers!
"Houston: no signs of life"
Start the conversation!