Buy Access to Course
05.

Procesador de estado más sencillo

|

Share this awesome video!

|

Publicar un DragonTreasure es fácil: haz una petición Patch a la ruta del tesoro con isPublished establecido en true y... ¡celebración! Pero... ¿qué pasa si, cuando se publica unDragonTreasure, necesitamos ejecutar algún código personalizado - tal vez activar algunas notificaciones en el sitio.

Una opción es crear una operación personalizada, como POST /api/treasures/5/publish. Puedes hacerlo, y puede ser divertido verlo en un futuro tutorial. Pero, ¿quién quiere trabajo extra? Podemos mantener esa simple petición a Patch y seguir ejecutando el código que queramos. ¿Cómo? Utilizando un procesador de estado y detectando el cambio.

Empecemos creando una prueba que publique un tesoro. En la parte inferior, copia esta última prueba, pégala y cámbiale el nombre a testPublishTreasure. Comenzamos con un usuario que posee un tesoro con isPublished false . A continuación, iniciamos sesión como ese usuario, hacemos una petición->patch() a /api/treasures/ utilizando el id... y enviamosisPublished: true. Esto debería ser un código de estado 200... y luego->assertJsonMatches() que isPublished es true.

// ... lines 1 - 13
class DragonTreasureResourceTest extends ApiTestCase
{
// ... lines 16 - 219
public function testPublishTreasure(): void
{
$user = UserFactory::createOne();
$treasure = DragonTreasureFactory::createOne([
'owner' => $user,
'isPublished' => false,
]);
$this->browser()
->actingAs($user)
->patch('/api/treasures/'.$treasure->getId(), [
'json' => [
'isPublished' => true,
],
])
->assertStatus(200)
->assertJsonMatches('isPublished', true)
;
}
}

¡Bastante sencillo! Copia el nombre de la prueba, gira y ejecútala:

symfony php bin/phpunit --filter=testPublishTreasure

¡Uy! Falla: esperaba que false fuera lo mismo que true. Eso es de la última línea: el JSON sigue teniendo isPublished false. Quizá... ¿el campo no es escribible? Comprueba los grupos que hay sobre esa propiedad. Ah: en un tutorial anterior, hicimos que este campo fuera escribible por los usuarios administradores, pero no por los usuarios normales. Añade treasure:write.

275 lines | src/Entity/DragonTreasure.php
// ... lines 1 - 90
class DragonTreasure
{
// ... lines 93 - 130
#[Groups(['admin:read', 'admin:write', 'owner:read', 'treasure:write'])]
private bool $isPublished = false;
// ... lines 133 - 273
}

Eso significa que cualquiera con acceso a la operación Patch puede escribir en este campo... que en realidad, gracias al security de esa operación... y a un votador personalizado que creamos... son sólo los usuarios administradores y el propietario.

275 lines | src/Entity/DragonTreasure.php
// ... lines 1 - 30
#[ApiResource(
// ... lines 32 - 33
operations: [
// ... lines 35 - 43
new Patch(
security: 'is_granted("EDIT", object)',
),
// ... lines 47 - 49
],
// ... lines 51 - 68
)]
// ... lines 70 - 90
class DragonTreasure
// ... lines 92 - 275

Haz la prueba ahora:

symfony php bin/phpunit --filter=testPublishTreasure

¡Ya está! Para ejecutar algún código cuando se publique el tesoro, necesitamos un procesador de estado. Y ya tenemos uno para `¡TesoroDragón! Lo creamos originalmente para establecer el propietario en el usuario autenticado en ese momento. Así que... ¿deberíamos meter el nuevo código aquí o crear un segundo procesador?

Tú decides, pero a mí me gusta tener un procesador por clase de recurso. Me simplifica la vida. Pero cambiemos el nombre de esta clase para que quede más claro: DragonTreasureStateProcessor.

Cambiar la decoración de nuestro procesador de estado

En el último tutorial, aprendimos que hay dos formas de añadir un proveedor o procesador de estado personalizado al sistema. El primer método lo hemos utilizado hace unos minutos con el proveedor de estado: crear un servicio aburrido normal... utilizar #[Autowire] para inyectar los servicios centrales... luego establecer la opción provideren DragonTreasure para que apunte a él.

La otra forma -que hicimos en el último tutorial de esta clase- es decorar el procesador central. Aquí, decoramos el PersistProcessorde Doctrine... lo que significa que siempre que se guarde cualquier recurso de la API, cuando intente utilizar el núcleo PersistProcessor, se llamará a nuestro servicio en su lugar. Esto fue fácil de configurar porque todo lo que necesitábamos era #[AsDecorator] y... ¡bam! Nuestro servicio empezó a ser llamado para todos nuestros recursos. Pero también por eso necesitamos este código adicional que comprueba qué objeto se está guardando.

31 lines | src/State/DragonTreasureStateProcessor.php
// ... lines 1 - 10
#[AsDecorator('api_platform.doctrine.orm.state.persist_processor')]
class DragonTreasureStateProcessor 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());
}
// ... lines 23 - 28
}
}

Ambas formas están bien. Pero por coherencia con el proveedor, vamos a refactorizar esto para utilizar el otro método. Esto consta de 3 pasos. En primer lugar, elimina #[AsDecorator]. De repente, en lugar de sobrescribir un servicio central, se trata de un servicio normal y aburrido que nadie utiliza en este momento. En segundo lugar, como ya no estamos decorando un servicio del núcleo, Symfony no sabrá qué pasar por $innerProcessor. Divide esto en varias líneas... y luego utiliza el truco #[Autowire] para apuntar al núcleo PersistProcessor. Y yo limpiaré la antigua declaración use.

35 lines | src/State/DragonTreasureStateProcessor.php
// ... lines 1 - 11
class DragonTreasureStateProcessor implements ProcessorInterface
{
public function __construct(
#[Autowire(service: PersistProcessor::class)]
private ProcessorInterface $innerProcessor,
private Security $security
)
{
}
// ... lines 21 - 33
}

El paso 3 es decirle a API Platform cuándo utilizar este procesador. En DragonTreasure, queremos que se utilice para nuestras operaciones Post y Patch. Estableceprocessor en DragonTreasureStateProcessor::class... y repite eso hacia abajo paraPatch.

278 lines | src/Entity/DragonTreasure.php
// ... lines 1 - 19
use App\State\DragonTreasureStateProcessor;
// ... lines 21 - 31
#[ApiResource(
// ... lines 33 - 34
operations: [
// ... lines 36 - 41
new Post(
// ... line 43
processor: DragonTreasureStateProcessor::class,
),
new Patch(
// ... line 47
processor: DragonTreasureStateProcessor::class,
),
// ... lines 50 - 71
)]
// ... lines 73 - 93
class DragonTreasure
// ... lines 95 - 278

¡Listo! API Platform llamará a nuestro procesador... y contiene el núcleo PersistProcessorpara que podamos hacer que haga el trabajo real. Vuelve a ejecutar la prueba para darnos una confianza infinita:

symfony php bin/phpunit --filter=testPublishTreasure

Me parece estupendo.

Y lo bueno de hacer el procesador con este método es que no necesitas este código condicional: esto siempre será un DragonTreasure. Para ayudar a mi editor y demostrarlo, assert() que $data es un instanceofDragonTreasure .

32 lines | src/State/DragonTreasureStateProcessor.php
// ... lines 1 - 11
class DragonTreasureStateProcessor implements ProcessorInterface
{
// ... lines 14 - 21
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): void
{
assert($data instanceof DragonTreasure);
// ... lines 25 - 29
}
}

Y mi editor ya está gritando

¡Eh, este código de aquí abajo ya no es necesario, tío!

Así que elimínalo también. Ahora que hemos refactorizado nuestro procesador de estados, volvamos a la tarea que nos ocupa: ejecutar código personalizado cuando se publica un tesoro.