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.
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é $description
pero 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
¡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.
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 DragonTreasure
aú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 | |
} |
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.
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.
Hey @Jeremy!
For now, I prevent writing in collections like this (when there is ownership-like stuff) and manage to handle it in opposite side.
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!
// 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
}
}
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