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

Voy a probar la ruta GET one treasure endpoint... utilizando un identificador real. Perfecto. Debido a los cambios que acabamos de hacer, el campo owner está incrustado.

¿Y si cambiamos el propietario? Pan comido: siempre que el campo sea escribible... y el nuestro lo es. Ahora mismo el owner es el id 1. Utiliza la ruta PUT para actualizar el id 2. Para la carga útil, establece owner en /api/users/3.

Y... ¡a ejecutar! ¡Bah! Error de sintaxis. JSON está malhumorado. Elimina la coma, inténtalo de nuevo y... ¡sí! El owner vuelve como el IRI /api/users/3.

Enviar datos incrustados a Update

¡Pero ahora quiero hacer algo salvaje! Este tesoro pertenece al usuario 3. Vamos a obtener sus datos. Abre la ruta GET un usuario, pruébala, introduce 3 y... ¡ahí está! El nombre de usuario es burnout400.

Éste es el objetivo: al actualizar un DragonTreasure -por tanto, al utilizar la ruta PUT a /api/treasures/{id} -, en lugar de cambiar de un propietario a otro, quiero cambiar el username del propietario existente. Algo así: en lugar de establecerowner a la cadena IRI, establecerla a un objeto con username asignado a algo nuevo.

¿Funcionaría? ¡Experimentemos! Pulsa Ejecutar y no funciona. Dice

No se permiten documentos anidados para el atributo owner, utiliza en su lugar IRI.

Permitir la incrustación de propiedades escribibles

Así que, a primera vista, parece que esto no está permitido: parece que aquí sólo puedes utilizar una cadena IRI. Pero, en realidad, sí está permitido. El problema es que el campousername no es escribible mediante esta operación.

Pensemos en esto. Estamos actualizando un DragonTreasure. Esto significa que API Platform está utilizando el grupo de serialización treasure:write. Ese grupo está por encima de la propiedad owner, por lo que podemos modificar el owner.

... lines 1 - 25
#[ApiResource(
... lines 27 - 49
denormalizationContext: [
'groups' => ['treasure:write'],
],
... line 53
)]
... line 55
class DragonTreasure
{
... lines 58 - 99
#[Groups(['treasure:read', 'treasure:write'])]
private ?User $owner = null;
... lines 102 - 213
}

Pero si queremos poder cambiar el username del propietario, entonces también tenemos que entrar en User y añadir ese grupo aquí.

... lines 1 - 22
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
... lines 25 - 46
#[Groups(['user:read', 'user:write', 'treasure:item:get', 'treasure:write'])]
... line 48
private ?string $username = null;
... lines 50 - 170
}

Esto funciona exactamente igual que los campos incrustados cuando los leemos. Básicamente, como al menos un campo de User tiene el grupo treasure:write, ahora podemos enviar un objeto al campo owner.

Objetos nuevos vs existentes en datos incrustados

Observa: enciéndelo de nuevo. Funciona... casi. Obtenemos un error 500:

Se ha encontrado una nueva entidad a través de la relación DragonTreasure.owner, pero no se ha configurado para persistir en cascade.

Woh. Esto significa que el serializador vio nuestros datos, creó un nuevo objeto User y luego configuró el username en él. Doctrine falló porque nunca le dijimos que persistiera el nuevo objeto User.

Aunque... esa no es la cuestión: ¡la cuestión es que no queremos un nuevo User! Queremos coger al propietario existente y actualizar su username.

Por cierto, para que este ejemplo sea más realista, añadamos también un name a la carga útil para que podamos fingir que estamos actualizando realmente el tesoro... y decidamos actualizar también el username del propietario mientras estamos en el vecindario.

En cualquier caso: ¿cómo le decimos al serializador que utilice el propietario existente en lugar de crear uno nuevo? Añadiendo un campo @id configurado con el IRI del usuario: /api/users/3.

Ya está Cuando el serializador ve un objeto, si no tiene un @id, crea un objeto nuevo. Si tiene @id, encuentra ese objeto y le asigna cualquier dato.

Así pues, llega el momento de la verdad. Cuando lo probamos... por supuesto, otro error de sintaxis. ¡Ponte las pilas, Ryan! Después de arreglarlo... ¡perfecto! ¡Un código de estado 200! Aunque... realmente no podemos ver si actualizó el username aquí... ya que sólo muestra el propietario.

Utiliza la ruta GET one User... busca al usuario 3... ¡y comprueba esos dulces datos! Sí cambió el username.

Vale, me doy cuenta de que este ejemplo puede no haber sido el más realista, pero poder actualizar objetos relacionados tiene muchos casos de uso reales.

Utilizar Persistir en cascada para crear un nuevo objeto

Volviendo a la petición de PUT, ¿qué pasaría si quisiéramos crear y guardar un nuevo objetoUser? ¿Es posible? Pues sí

En primer lugar, tendríamos que añadir un cascade: ['persist'] al atributo treasure.ownerORM\Column . Esto es algo que veremos más adelante. Y en segundo lugar, tendríamos que asegurarnos de exponer todos los campos obligatorios como escribibles. Ahora mismo sólo username es escribible... así que no podríamos enviar password o email.

La restricción válida

Antes de continuar, nos falta un pequeño, pero importante, detalle. Intentemos esta actualización una vez más con el @id. Pero establece username en una cadena vacía.

Recuerda que el campo username tiene un NotBlank encima, por lo que debería fallar la validación. Y sin embargo, cuando lo intentamos, ¡obtenemos un código de estado 200! Y si vamos a la ruta GET de un usuario... sí, ¡el username ahora está vacío! Eso es... un problema.

¿Cómo ha ocurrido? Por cómo funciona el sistema de validación de Symfony.

La entidad de nivel superior -el objeto que estamos modificando directamente- es DragonTreasure. Así que el sistema de validación mira DragonTreasure y ejecuta todas las restricciones de validación. Sin embargo, cuando llega a un objeto como la propiedad owner, se detiene. No sigue validando también ese objeto.

Si quieres que eso ocurra, tienes que añadir una restricción a esto llamadaAssert\Valid.

... lines 1 - 55
class DragonTreasure
{
... lines 58 - 100
#[Assert\Valid]
private ?User $owner = null;
... lines 103 - 214
}

Ahora... en nuestra ruta PUT... si lo intentamos de nuevo, ¡sí! 422: owner.username, este valor no debe estar en blanco.

La posibilidad de actualizar un objeto incrustado es muy útil y potente. Pero el coste de esto es hacer que tu API sea cada vez más compleja. Así que, aunque puedes optar por hacer esto -y deberías hacerlo si es lo que quieres-, también podrías optar por obligar al cliente de la API a actualizar primero el tesoro... y luego hacer una segunda petición para actualizar el nombre de usuario del usuario... en lugar de permitirle que lo haga todo de lujo al mismo tiempo.

A continuación: veamos esta relación desde el otro lado. Cuando estamos actualizando un User, ¿podríamos actualizar también los tesoros que pertenecen a ese usuario? ¡Averigüémoslo!

Leave a comment!

8
Login or Register to join the conversation
triemli Avatar
triemli Avatar triemli | posted hace 1 mes | edited

Hi guys but what's about the case when we have one-one relation, for example User-Profile.
We create a user with a profile, But the profile scope requires iri of the user /api/user/??? It works only if I have IRI user id. All relations has cascade: ['persist'] idea how to solve it?

{
  "username": "userOne",
  "profile": {
       "info": "info about the user in profile",
     "user": "/api/user/???"
   }
} 
Reply

Hey,

IIRC if you are persisting a new object you don't need to set back relation to it, I mean you User payload should be like

{
  "username": "userOne",
  "profile": {
       "info": "info about the user in profile",
   }
} 

That should be enough to create related objects at once.

Cheers!

Reply
seshy Avatar
seshy Avatar seshy | posted hace 2 meses | edited

Hey. Cool series.

I noticed a small issue

After successful update of username, property nameof Treasure remained the same.

{
  "name": "Brand new treasure!",
  "owner": {
    "@id": "/api/users/3",
    "username": "Tom"
  }
}

I can say even more. PUT request

{
  "name": "Brand new treasure111!"
}

has response code:200, but nameis not changed.

Reply
seshy Avatar

For some reason I was missing setName in DragonTreasure
Mystery solved ))

1 Reply

Hey @seshy,

Awesome news! Such things happens sometimes! Keep going!

Cheers!

Reply
Szymon Avatar

When I want to change username and name of treasure I receive error:
"hydra:description": "name: Describe your loot in 50 chars or less\ncoolFactor: This value should be less than or equal to 10.",

I know that is setted #[Assert\NotBlank] but it's just the same like you have code?

Reply

Hey @Szymon!

It looks like the data on that treasure is STARTING invalid - like the coolFactor in the database is somehow already greater than 10. And so, even though you’re not changing that field, it fails validation. I think for the treasure’s name I may have a bug on the fixtures that randomly sets a name that’s longer than 50 - so that is likely the cause of that error. I might (?) have some problem also with coolFactor!

So, just a data error on the database - and probably my fault. On production, thanks to validation, that data would never be able to get into this invalid state.

Cheers!

1 Reply
CarlosReyes Avatar

Yeah, that is the reason, changing the getDefaults() in DragonTreasureFactory.php to something like:

`protected function getDefaults(): array

{
    return [
        'coolFactor' => self::faker()->numberBetween(1, 10),
        'description' => self::faker()->text(255),
        'isPublished' => self::faker()->boolean(),
        'name' => self::faker()->text(50),
        'plunderedAt' => \DateTimeImmutable::createFromMutable(self::faker()->dateTime()),
        'value' => self::faker()->randomNumber(),
        'owner' => UserFactory::new(),
    ];
}`

will fix it.

Regards!

1 Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

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