Procesador de estado más sencillo
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.
| // ... lines 1 - 90 | |
| class DragonTreasure | |
| { | |
| // ... lines 93 - 130 | |
| (['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.
| // ... lines 1 - 30 | |
| ( | |
| // ... 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.
| // ... lines 1 - 10 | |
| ('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.
| // ... 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.
| // ... lines 1 - 19 | |
| use App\State\DragonTreasureStateProcessor; | |
| // ... lines 21 - 31 | |
| ( | |
| // ... 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 .
| // ... 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.