Procesador de estado de recursos personalizado
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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeNo hemos configurado la clave operations
en nuestro #[ApiResource]
. Y así, obtenemos todas las operaciones por defecto. Pero en realidad sólo necesitamos unas pocas. Añadeoperations
con un new GetCollection()
, new Get()
para obtener una única búsqueda y new Patch()
para que los usuarios puedan actualizar el estado de una búsqueda existente cuando la completen.
// ... lines 1 - 6 | |
use ApiPlatform\Metadata\Get; | |
use ApiPlatform\Metadata\GetCollection; | |
use ApiPlatform\Metadata\Patch; | |
// ... lines 10 - 13 | |
( | |
// ... line 15 | |
operations: [ | |
new GetCollection(), | |
new Get(), | |
new Patch(), | |
], | |
// ... line 21 | |
) | |
class DailyQuest | |
// ... lines 24 - 43 |
Al actualizar... ¡Me encanta!
Hablando de esa operación Patch
, cuando se utilice, API Platform llamará al procesador de estado, para que podamos guardar... o hacer lo que queramos. Aún no tenemos uno, así que ése será nuestro próximo trabajo.
Añadir una prueba de parcheo
Pero empecemos con una prueba. Abajo, en tests/Functional/
, crea una nueva clase llamada DailyQuestResourceTest
. Haz que ésta extienda la ApiTestCase
que creamos en el último tutorial y la use ResetDatabase
de Foundry para asegurarnos de que nuestra base de datos está vacía al inicio de cada prueba. También use Factories
.
// ... lines 1 - 4 | |
use Zenstruck\Foundry\Test\Factories; | |
use Zenstruck\Foundry\Test\ResetDatabase; | |
class DailyQuestResourceTest extends ApiTestCase | |
{ | |
use ResetDatabase; | |
use Factories; | |
} |
Vale, no los necesitamos... ya que no vamos a hablar con la base de datos... pero si decidimos hacerlo más adelante, ya estamos listos.
Aquí abajo, añade public function testPatchCanUpdateStatus()
. Lo primero que necesitamos es un new \DateTime()
que represente $yesterday
: -1 day
.
// ... lines 1 - 12 | |
public function testPatchCanUpdateStatus() | |
{ | |
$yesterday = new \DateTime('-1 day'); | |
// ... lines 16 - 26 | |
} | |
// ... lines 28 - 29 |
Recuerda: en nuestro proveedor, estamos creando búsquedas diarias desde hoy hasta los últimos 50 días. Cuando hacemos una petición a PATCH
, se llama a nuestro proveedor de objetos para que "cargue" el objeto. Así que tenemos que utilizar una fecha que sepamos que se va a encontrar.
Ahora digamos $this->browser()
, ->patch()
... y la URL:/api/quests/
con $yesterday->format('Y-m-d')
. Pasa un segundo argumento de opciones con json
y un array con 'status' => 'completed'
.
El campo status
es un enum... pero como está respaldado por una cadena, el serializador lo deserializará a partir de la cadena active
o completed
. Termina con->assertStatus(200)
, ->dump()
(que será útil en un segundo), y luego->assertJsonMatches()
para comprobar que status
cambió a completed
.
// ... lines 1 - 12 | |
public function testPatchCanUpdateStatus() | |
{ | |
// ... line 15 | |
$this->browser() | |
->patch('/api/quests/'.$yesterday->format('Y-m-d'), [ | |
'json' => [ | |
'status' => 'completed', | |
], | |
// ... line 21 | |
]) | |
->assertStatus(200) | |
->dump() | |
->assertJsonMatches('status', 'completed') | |
; | |
} | |
// ... lines 28 - 29 |
¡Maravilloso! En realidad no vamos a guardar el estado actualizado... pero al menos deberíamos ver que el JSON final tiene status
completed
. Copia este nombre de prueba... y por aquí, ejecuta: symfony php bin/phpunit --filter=
y pega ese nombre:
symfony php bin/phpunit --filter=testPatchCanUpdateStatus
Y... ¡ups! Obtenemos un 415. El error dice
El tipo de contenido
application/json
no es compatible.
Ah... olvidé añadir una cabecera a mi petición PATCH
. Añade headers
en una matriz con Content-Type
, application/merge-patch+json
.
// ... lines 1 - 12 | |
public function testPatchCanUpdateStatus() | |
{ | |
// ... line 15 | |
$this->browser() | |
->patch('/api/quests/'.$yesterday->format('Y-m-d'), [ | |
// ... lines 18 - 20 | |
'headers' => ['Content-Type' => 'application/merge-patch+json'] | |
]) | |
// ... lines 23 - 25 | |
; | |
} | |
// ... lines 28 - 29 |
Ya hablamos de esto en el último tutorial: esto indica al sistema qué tipo de parche tenemos. Este es el único que se admite ahora mismo, pero sigue siendo necesario.
Si probamos esto... ¡pasa! Pero espera, ¡creo que me he engañado a mí mismo! Comenta el status
y luego la prueba... ¿aún pasa? Sí, cámbialo por -2 days
... y $yesterday
por sólo $day
.
En nuestro proveedor, hacemos que todas las demás búsquedas estén activas o completas: y la de ayer empieza como completa. ¡Ups! Cuando intentamos la prueba ahora... falla. Volvemos a añadirstatus
al JSON y... ¡ya está! ¡La prueba pasa!
Entre bastidores, éste es el proceso. Uno: la API Platform llama a nuestro proveedor para obtener el DailyQuest
de esta fecha. Dos: el serializador actualiza eseDailyQuest
utilizando el JSON enviado en la petición. Tres: se llama al procesador de estado. Y cuatro: el DailyQuest
se serializa de nuevo en JSON.
Creación del procesador de estado
Excepto que... en nuestro caso, no hay paso tres... ¡porque aún no hemos creado un procesador de estado! ¡Añadamos uno!
php bin/console make:state-processor
y llamémoslo DailyQuestStateProcessor
.
Otro nombre chispeante de genialidad. Ve a comprobarlo: está vacío y lleno de potencial.
// ... lines 1 - 7 | |
class DailyQuestStateProcessor implements ProcessorInterface | |
{ | |
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): void | |
{ | |
// Handle the state | |
} | |
} |
En DailyQuest
, el procesador debe utilizarse para la operación Patch
, así que añade processor: DailyQuestStateProcessor::class
.
// ... lines 1 - 14 | |
( | |
// ... line 16 | |
operations: [ | |
// ... lines 18 - 19 | |
new Patch( | |
processor: DailyQuestStateProcessor::class, | |
), | |
], | |
// ... line 24 | |
) | |
class DailyQuest | |
// ... lines 27 - 46 |
Para demostrar que esto funciona, dd($data)
.
// ... lines 1 - 7 | |
class DailyQuestStateProcessor implements ProcessorInterface | |
{ | |
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): void | |
{ | |
dd($data); | |
} | |
} |
De acuerdo Vuelve a hacer la prueba:
symfony php bin/phpunit --filter=testPatchCanUpdateStatus
Y... ¡boom! El status
se establece en completed
.
Por cierto, hemos añadido la opción processor
directamente a la operación Patch()
, pero también podemos ponerla aquí abajo, en el atributo #[ApiResource()]
directamente.
// ... lines 1 - 14 | |
( | |
// ... line 16 | |
operations: [ | |
// ... lines 18 - 19 | |
new Patch(), | |
// ... line 21 | |
], | |
// ... line 23 | |
processor: DailyQuestStateProcessor::class, | |
) | |
class DailyQuest | |
// ... lines 27 - 46 |
Eso no cambia nada... porque ésta es la única operación que tenemos que utiliza siquiera un procesador: Las operaciones del método GET nunca llaman a un procesador.
Lógica del procesador de estado
De todos modos, aquí es donde normalmente guardaríamos los datos o... haríamos algo, como enviar un correo electrónico si se tratara de un recurso de la API "restablecer contraseña".
Para hacer las cosas un poco realistas, añadamos una propiedad $lastUpdated
aDailyQuest
y actualicémosla aquí. Añadepublic \DateTimeInterface $lastUpdated
.
// ... lines 1 - 25 | |
class DailyQuest | |
{ | |
// ... lines 28 - 33 | |
public \DateTimeInterface $lastUpdated; | |
// ... lines 35 - 45 | |
} |
Luego rellénalo dentro del proveedor de estado:$quest->lastUpdated
es igual a new \DateTimeImmutable()
... con algo de aleatoriedad: entre 10 y 100 días atrás.
// ... lines 1 - 10 | |
class DailyQuestStateProvider implements ProviderInterface | |
{ | |
// ... lines 13 - 23 | |
private function createQuests(): array | |
{ | |
// ... line 26 | |
for ($i = 0; $i < 50; $i++) { | |
// ... lines 28 - 32 | |
$quest->lastUpdated = new \DateTimeImmutable(sprintf('- %d days', rand(10, 100))); | |
// ... lines 34 - 35 | |
} | |
// ... lines 37 - 38 | |
} | |
} |
Por último, dirígete al procesador de estado. Sabemos que sólo se utiliza para los objetos DailyQuest
... así que $data
será uno de ellos. Ayuda a tu editor con assert($data instanceof DailyQuest)
y, más abajo,$data->lastUpdated = new \DateTimeImmutable('now')
.
// ... lines 1 - 8 | |
class DailyQuestStateProcessor implements ProcessorInterface | |
{ | |
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): void | |
{ | |
assert($data instanceof DailyQuest); | |
$data->lastUpdated = new \DateTimeImmutable('now'); | |
} | |
} |
¡Genial! No tenemos una aserción de prueba para ese campo, pero seguimos volcando la respuesta... y podemos verla aquí. Estoy mirando mi reloj y... es la hora correcta en mi pequeño rincón del mundo. ¡Nuestro procesador estatal está vivo!
Celébralo volviendo a la prueba y eliminando ese volcado.
A continuación: Hagamos nuestro recurso más interesante añadiendo una relación con otro recurso de la API: una relación con el tesoro del dragón.