Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Configuración automática del "propietario

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

Cada DragonTreasure debe tener un owner... y para establecerlo, cuando POSTpara crear un tesoro, requerimos ese campo. Creo que deberíamos hacerlo opcional. Así que, en la prueba, deja de enviar el campo owner:

... lines 1 - 12
class DragonTreasureResourceTest extends ApiTestCase
{
... lines 15 - 41
public function testPostToCreateTreasure(): void
{
... lines 44 - 45
$this->browser()
... lines 47 - 51
->post('/api/treasures', HttpOptions::json([
... lines 53 - 56
'owner' => '/api/users/'.$user->getId(),
]))
... lines 59 - 60
;
}
... lines 63 - 179
}

Cuando esto ocurra, configurémoslo automáticamente para el usuario autenticado actualmente.

Asegúrate de que la prueba falla. Copia el nombre del método... y ejecútalo:

symfony php bin/phpunit --filter=testPostToCreateTreasure

Falló. Obtuve un 422, 201 esperado. Ese 422 es un error de validación de la propiedad owner: este valor no debe ser nulo.

Eliminar la validación del propietario

Si vamos a hacerlo opcional, tenemos que eliminar ese Assert\NotNull:

... lines 1 - 88
class DragonTreasure
{
... lines 91 - 136
#[Assert\NotNull]
... line 138
private ?User $owner = null;
... lines 140 - 251
}

Y ahora cuando intentemos la prueba

symfony php bin/phpunit --filter=testPostToCreateTreasure

¡Hola magnífico error 500! Probablemente se deba a que el nulo owner_id hace kaboom cuando llega a la base de datos. ¡Yup!

Utilizar los procesadores de estado

Entonces: ¿cómo podemos establecer automáticamente este campo cuando no se envía? En el tutorial anterior de la API Platform 2, lo hice con un oyente de entidad, que es una buena solución. Pero en la API Platform 3, al igual que cuando hicimos hash de la contraseña de usuario, ahora hay un sistema muy bueno para esto: el sistema de procesadores de estado.

Como recordatorio, nuestras rutas POST y PATCH para DragonTreasure ya tienen un procesador de estado que proviene de Doctrine: es el responsable de guardar el objeto en la base de datos. Llegados a este punto, nuestro objetivo te resultará familiar: decorar ese proceso de estado para que podamos ejecutar código adicional antes de guardar.

Como antes, empieza ejecutando:

php bin/console make:state-processor

Llámalo DragonTreasureSetOwnerProcessor:

... lines 1 - 2
namespace App\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
class DragonTreasureSetOwnerProcessor implements ProcessorInterface
{
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): void
{
// Handle the state
}
}

En src/State/, ábrelo. Vale, ¡a decorar! Añade el método construct con private ProcessorInterface $innerProcessor:

... lines 1 - 5
use ApiPlatform\State\ProcessorInterface;
... lines 7 - 9
class DragonTreasureSetOwnerProcessor implements ProcessorInterface
{
public function __construct(private ProcessorInterface $innerProcessor)
{
}
... lines 15 - 19
}

Luego abajo en process(), ¡llama a eso! Este método no devuelve nada - tiene un retorno void - así que sólo necesitamos $this->innerProcessor - no olvides esa parte como estoy haciendo yo - ->process() pasando $data, $operation, $uriVariables y$context:

... lines 1 - 9
class DragonTreasureSetOwnerProcessor implements ProcessorInterface
{
... lines 12 - 15
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): void
{
$this->innerProcessor->process($data, $operation, $uriVariables, $context);
}
}

Ahora, para hacer que Symfony utilice nuestro procesador de estado en lugar del normal de Doctrine, añade #[AsDecorator]... y el id del servicio esapi_platform.doctrine.orm.state.persist_processor:

... lines 1 - 6
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
#[AsDecorator('api_platform.doctrine.orm.state.persist_processor')]
class DragonTreasureSetOwnerProcessor implements ProcessorInterface
{
... lines 12 - 19
}

¡Genial! Ahora, a todo lo que utilice ese servicio en el sistema se le pasará nuestro servicio en su lugar... y luego se nos pasará el original.

¡Decorar varias veces está bien!

Ah, y está pasando algo guay. Mira UserHashPasswordStateProcessor. ¡Estamos decorando lo mismo ahí! Sí, estamos decorando ese servicio dos veces, ¡lo que está totalmente permitido! Internamente, esto creará una especie de cadena de servicios decorados.

Bien, pongámonos a trabajar en la configuración del propietario. Conecta automáticamente nuestro servicio favorito Security para que podamos averiguar quién ha iniciado sesión:

... lines 1 - 7
use Symfony\Bundle\SecurityBundle\Security;
... lines 9 - 11
class DragonTreasureSetOwnerProcessor implements ProcessorInterface
{
public function __construct(private ProcessorInterface $innerProcessor, private Security $security)
{
}
... lines 17 - 25
}

Entonces, antes de que hagamos el guardado, si $data es un instanceof DragonTreasurey $data->getOwner() es nulo y $this->security->getUser() -asegurándonos de que el usuario está conectado- entonces $data->setOwner($this->security->getUser()):

... lines 1 - 11
class DragonTreasureSetOwnerProcessor implements ProcessorInterface
{
... lines 14 - 17
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): void
{
if ($data instanceof DragonTreasure && $data->getOwner() === null && $this->security->getUser()) {
$data->setOwner($this->security->getUser());
}
$this->innerProcessor->process($data, $operation, $uriVariables, $context);
}
}

¡Eso debería bastar! Ejecuta esa prueba:

symfony php bin/phpunit --filter=testPostToCreateTreasure

¡Caramba! Tamaño de memoria permitido agotado. ¡Me huele a recursión! Porque... Me estoy llamando aprocess(): Necesito $this->innerProcessor->process():

... lines 1 - 11
class DragonTreasureSetOwnerProcessor implements ProcessorInterface
{
... lines 14 - 17
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): void
{
... lines 20 - 23
$this->innerProcessor->process($data, $operation, $uriVariables, $context);
}
}

Ahora:

symfony php bin/phpunit --filter=testPostToCreateTreasure

Una prueba superada mola mucho más que la recursividad. ¡Y el campo propietario ahora es opcional!

Siguiente: actualmente devolvemos todos los tesoros de nuestro punto final de colección GET, incluidos los tesoros no publicados. Arreglémoslo modificando la consulta detrás de ese punto final para ocultarlos.

Leave a comment!

5
Login or Register to join the conversation
Rsteuber Avatar
Rsteuber Avatar Rsteuber | posted hace 4 meses

When will this finally be ready?

Reply

Hey @Rsteuber!

I JUST recorded the audio today - it'll probably be a week or two before the video is out, but you refresh, you'll see the final "script" for this chapter (though, code blocks will come later). Also, if you download the code, what we do in this chapter is already in there. I hope that helps!

Cheers!

Reply
Rsteuber Avatar

Hmm, it seems I can't download the source code yet. Guess i have to wait when it is released.

Cheers :)

Reply

Hey @Rsteuber

Yea, the download button it's disabled for non-published chapters, but if you only want to download the course code, you can go to any other chapter and download it there

Cheers!

Reply
Rsteuber Avatar

Hey Ryan, Yes thank you so very much! It really helps :)

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.1.2
        "doctrine/annotations": "^2.0", // 2.0.1
        "doctrine/doctrine-bundle": "^2.8", // 2.8.3
        "doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.2
        "doctrine/orm": "^2.14", // 2.14.1
        "nelmio/cors-bundle": "^2.2", // 2.2.0
        "nesbot/carbon": "^2.64", // 2.66.0
        "phpdocumentor/reflection-docblock": "^5.3", // 5.3.0
        "phpstan/phpdoc-parser": "^1.15", // 1.16.1
        "symfony/asset": "6.2.*", // v6.2.5
        "symfony/console": "6.2.*", // v6.2.5
        "symfony/dotenv": "6.2.*", // v6.2.5
        "symfony/expression-language": "6.2.*", // v6.2.5
        "symfony/flex": "^2", // v2.2.4
        "symfony/framework-bundle": "6.2.*", // v6.2.5
        "symfony/property-access": "6.2.*", // v6.2.5
        "symfony/property-info": "6.2.*", // v6.2.5
        "symfony/runtime": "6.2.*", // v6.2.5
        "symfony/security-bundle": "6.2.*", // v6.2.6
        "symfony/serializer": "6.2.*", // v6.2.5
        "symfony/twig-bundle": "6.2.*", // v6.2.5
        "symfony/ux-react": "^2.6", // v2.7.1
        "symfony/ux-vue": "^2.7", // v2.7.1
        "symfony/validator": "6.2.*", // v6.2.5
        "symfony/webpack-encore-bundle": "^1.16", // v1.16.1
        "symfony/yaml": "6.2.*" // v6.2.5
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.2
        "mtdowling/jmespath.php": "^2.6", // 2.6.1
        "phpunit/phpunit": "^9.5", // 9.6.3
        "symfony/browser-kit": "6.2.*", // v6.2.5
        "symfony/css-selector": "6.2.*", // v6.2.5
        "symfony/debug-bundle": "6.2.*", // v6.2.5
        "symfony/maker-bundle": "^1.48", // v1.48.0
        "symfony/monolog-bundle": "^3.0", // v3.8.0
        "symfony/phpunit-bridge": "^6.2", // v6.2.5
        "symfony/stopwatch": "6.2.*", // v6.2.5
        "symfony/web-profiler-bundle": "6.2.*", // v6.2.5
        "zenstruck/browser": "^1.2", // v1.2.0
        "zenstruck/foundry": "^1.26" // v1.28.0
    }
}
userVoice