Aprovechar el procesador central
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 Subscribe¡Mira cómo vamos! En nuestro procesador de estados, hemos transformado con éxito el UserApi
en una entidad User
. Así que ¡vamos a guardarla! Podríamos inyectar el gestor de entidades, persistir y vaciar... y darlo por terminado. Pero prefiero descargar ese trabajo al núcleoPersistProcessor
. Busca ese archivo y ábrelo.
Hace la persistencia y el vaciado sencillos... pero también tiene una lógica bastante compleja para las operaciones de PUT
. En realidad, no las vamos a utilizar, pero la cuestión es que es mejor reutilizar esta clase que intentar desarrollar nuestra propia lógica.
Llamar al Core PersistProcessor
A estas alturas, ya debería resultarte familiar cómo lo hacemos. Añade unprivate ProcessorInterface $persistProcessor
... y para que Symfony sepa exactamente qué servicio queremos, incluye el atributo #[Autowire()]
, con service
establecido en PersistProcessor
(en este caso, sólo hay uno para elegir) ::class
.
// ... lines 1 - 4 | |
use ApiPlatform\Doctrine\Common\State\PersistProcessor; | |
// ... line 6 | |
use ApiPlatform\State\ProcessorInterface; | |
// ... lines 8 - 10 | |
use Symfony\Component\DependencyInjection\Attribute\Autowire; | |
class EntityClassDtoStateProcessor implements ProcessorInterface | |
{ | |
public function __construct( | |
private UserRepository $userRepository, | |
#[Autowire(service: PersistProcessor::class)] private ProcessorInterface $persistProcessor, | |
) | |
{ | |
} | |
// ... lines 22 - 53 | |
} |
¡Muy bonito! A continuación, guarda con $this->persistProcessor->process()
pasando$entity
, $operation
, $uriVariables
, y $context
... que son todos los mismos argumentos que tenemos aquí arriba.
// ... lines 1 - 4 | |
use ApiPlatform\Doctrine\Common\State\PersistProcessor; | |
// ... line 6 | |
use ApiPlatform\State\ProcessorInterface; | |
// ... lines 8 - 10 | |
use Symfony\Component\DependencyInjection\Attribute\Autowire; | |
class EntityClassDtoStateProcessor implements ProcessorInterface | |
{ | |
public function __construct( | |
private UserRepository $userRepository, | |
#[Autowire(service: PersistProcessor::class)] private ProcessorInterface $persistProcessor, | |
) | |
{ | |
} | |
// ... line 22 | |
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []) | |
{ | |
// ... lines 25 - 28 | |
$this->persistProcessor->process($entity, $operation, $uriVariables, $context); | |
// ... lines 30 - 31 | |
} | |
// ... lines 33 - 53 | |
} |
Ah, y como antes, cuando generamos esta clase, generó process()
con un tipo de retorno void
. Eso no es exactamente correcto. No tienes que devolver nada de los procesadores de estado, pero puedes hacerlo. Y lo que devuelvas -en este caso, devolveremos $data
- se convertirá en última instancia en la "cosa" que se serializa y se devuelve al usuario. Si no devuelves nada, se utilizará$data
.
// ... lines 1 - 4 | |
use ApiPlatform\Doctrine\Common\State\PersistProcessor; | |
// ... line 6 | |
use ApiPlatform\State\ProcessorInterface; | |
// ... lines 8 - 10 | |
use Symfony\Component\DependencyInjection\Attribute\Autowire; | |
class EntityClassDtoStateProcessor implements ProcessorInterface | |
{ | |
public function __construct( | |
private UserRepository $userRepository, | |
#[Autowire(service: PersistProcessor::class)] private ProcessorInterface $persistProcessor, | |
) | |
{ | |
} | |
// ... line 22 | |
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []) | |
{ | |
// ... lines 25 - 28 | |
$this->persistProcessor->process($entity, $operation, $uriVariables, $context); | |
return $data; | |
} | |
// ... lines 33 - 53 | |
} |
Establecer el id en el DTO
Vale, creo que esto debería funcionar (Famosas últimas palabras...).
symfony php bin/phpunit --filter=testPostToCreateUser
Y... falla. Seguimos recibiendo un error 400, y sigue siendoUnable to generate an IRI for the item
.
Entonces... ¿qué pasa? Mapeamos el UserApi
a un nuevo objeto User
y guardamos el nuevoUser
... lo que hace que Doctrine asigne el nuevo id
a ese objeto entidad. Pero nunca cogemos ese nuevo id y lo volvemos a poner en nuestro UserApi
.
Para solucionarlo, después de guardar, añade $data->id = $entity->getId()
.
// ... lines 1 - 4 | |
use ApiPlatform\Doctrine\Common\State\PersistProcessor; | |
// ... line 6 | |
use ApiPlatform\State\ProcessorInterface; | |
// ... lines 8 - 10 | |
use Symfony\Component\DependencyInjection\Attribute\Autowire; | |
class EntityClassDtoStateProcessor implements ProcessorInterface | |
{ | |
public function __construct( | |
private UserRepository $userRepository, | |
#[Autowire(service: PersistProcessor::class)] private ProcessorInterface $persistProcessor, | |
) | |
{ | |
} | |
// ... line 22 | |
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []) | |
{ | |
// ... lines 25 - 28 | |
$this->persistProcessor->process($entity, $operation, $uriVariables, $context); | |
$data->id = $entity->getId(); | |
return $data; | |
} | |
// ... lines 34 - 54 | |
} |
Y si lo intentamos ahora...
symfony php bin/phpunit --filter=testPostToCreateUser
sigue fallando... ¡pero esta vez hemos llegado más lejos! La respuesta parece buena. Devuelve un código de estado 201 con la nueva información del usuario. Falla en la parte de la prueba en la que intenta utilizar la contraseña para iniciar sesión. Esto se debe a que nuestra contraseña está actualmente configurada como... TODO
. Lo arreglaremos en un minuto.
Manejo de la operación de borrado
Pero primero, cuando establecimos el processor
en el nivel superior #[ApiResource]
, éste se convirtió en el procesador de todas las operaciones: POST
, PUT
, PATCH
, yDELETE
. POST
, PUT
, y PATCH
son todas prácticamente iguales: guardar el objeto en la base de datos. Pero DELETE
es diferente: no estamos guardando, sino eliminando.
Para ello, consulta if ($operation instanceof DeleteOperationInterface)
.
// ... lines 1 - 6 | |
use ApiPlatform\Metadata\DeleteOperationInterface; | |
// ... lines 8 - 14 | |
class EntityClassDtoStateProcessor implements ProcessorInterface | |
{ | |
// ... lines 17 - 25 | |
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []) | |
{ | |
// ... lines 28 - 31 | |
if ($operation instanceof DeleteOperationInterface) { | |
// ... lines 33 - 35 | |
} | |
// ... lines 37 - 41 | |
} | |
// ... lines 43 - 63 | |
} |
Al igual que guardar, eliminar no es difícil... pero sigue siendo mejor descargar este trabajo al procesador de eliminación del núcleo de Doctrine. Así que, aquí arriba, copia el argumento... e inyecta otro procesador: RemoveProcessor
... y cámbiale el nombre a$removeProcessor
.
// ... lines 1 - 5 | |
use ApiPlatform\Doctrine\Common\State\RemoveProcessor; | |
use ApiPlatform\Metadata\DeleteOperationInterface; | |
// ... lines 8 - 14 | |
class EntityClassDtoStateProcessor implements ProcessorInterface | |
{ | |
public function __construct( | |
// ... lines 18 - 19 | |
#[Autowire(service: RemoveProcessor::class)] private ProcessorInterface $removeProcessor, | |
) | |
{ | |
} | |
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []) | |
{ | |
// ... lines 28 - 31 | |
if ($operation instanceof DeleteOperationInterface) { | |
// ... lines 33 - 35 | |
} | |
// ... lines 37 - 41 | |
} | |
// ... lines 43 - 63 | |
} |
Aquí abajo, di $this->removeProcessor->process()
y pásale $entity
,$operation
, $uriVariables
, y $context
igual que al otro procesador.
// ... lines 1 - 5 | |
use ApiPlatform\Doctrine\Common\State\RemoveProcessor; | |
use ApiPlatform\Metadata\DeleteOperationInterface; | |
// ... lines 8 - 14 | |
class EntityClassDtoStateProcessor implements ProcessorInterface | |
{ | |
public function __construct( | |
// ... lines 18 - 19 | |
#[Autowire(service: RemoveProcessor::class)] private ProcessorInterface $removeProcessor, | |
) | |
{ | |
} | |
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []) | |
{ | |
// ... lines 28 - 31 | |
if ($operation instanceof DeleteOperationInterface) { | |
$this->removeProcessor->process($entity, $operation, $uriVariables, $context); | |
// ... lines 34 - 35 | |
} | |
// ... lines 37 - 41 | |
} | |
// ... lines 43 - 63 | |
} |
Una cosa clave a tener en cuenta es que vamos a return null
. En el caso de una operación DELETE
, no devolvemos nada en la respuesta... lo que conseguimos devolviendo null
desde aquí. No tengo una prueba preparada para esto, pero haremos un acto de fe y supondremos que funciona. ¡Envíalo!
// ... lines 1 - 5 | |
use ApiPlatform\Doctrine\Common\State\RemoveProcessor; | |
use ApiPlatform\Metadata\DeleteOperationInterface; | |
// ... lines 8 - 14 | |
class EntityClassDtoStateProcessor implements ProcessorInterface | |
{ | |
public function __construct( | |
// ... lines 18 - 19 | |
#[Autowire(service: RemoveProcessor::class)] private ProcessorInterface $removeProcessor, | |
) | |
{ | |
} | |
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []) | |
{ | |
// ... lines 28 - 31 | |
if ($operation instanceof DeleteOperationInterface) { | |
$this->removeProcessor->process($entity, $operation, $uriVariables, $context); | |
return null; | |
} | |
// ... lines 37 - 41 | |
} | |
// ... lines 43 - 63 | |
} |
Cifrar la contraseña
Sólo nos queda un problema por resolver: cifrar la contraseña. Ya lo hemos hecho antes, así que no pasa nada. Antes de hacer demasiado aquí, abre UserApi
... y añade unpublic ?string $password = null
... con un comentario. Esto siempre contendrá null o la contraseña "en texto plano" si el usuario envía una. Nunca vamos a necesitar manejar la contraseña "hash" en nuestra API, así que no necesitamos espacio para ello... ¡lo cual está muy bien!
De vuelta en el procesador, if ($dto->password)
, entonces sabemos que tenemos que aplicar el hash y establecerlo en el usuario. Si se está creando un nuevo usuario, siempre se establecerá... pero al actualizar un usuario, haremos que este campo sea opcional. Si no se establece, no haremos nada, de modo que se mantendrá la contraseña actual del usuario.
Para hacer el hash, arriba, añade un argumento más:private UserPasswordHasherInterface $userPasswordHasher
. Luego, abajo,$entity->setPassword()
se establece en $this->userPasswordHasher->hashPassword()
, pasando a$entity
(el objeto User
) y la contraseña simple: $dto->password
.
// ... lines 1 - 13 | |
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; | |
// ... line 15 | |
class EntityClassDtoStateProcessor implements ProcessorInterface | |
{ | |
public function __construct( | |
// ... lines 19 - 21 | |
private UserPasswordHasherInterface $userPasswordHasher, | |
) | |
{ | |
} | |
// ... lines 27 - 45 | |
private function mapDtoToEntity(object $dto): object | |
{ | |
// ... lines 48 - 60 | |
if ($dto->password) { | |
$entity->setPassword($this->userPasswordHasher->hashPassword($entity, $dto->password)); | |
} | |
// ... lines 64 - 66 | |
} | |
} |
Uf. Intentemos de nuevo la prueba. Y... falla... con
La anotación "@El" de la propiedad
UserApi::$password
nunca se importó.
Así que... me he tropezado con el teclado y he añadido un @
de más. Elimínalo... e inténtalo de nuevo:
symfony php bin/phpunit --filter=testPostToCreateUser
¡Pasa! ¡Lo que significa que se ha registrado completamente utilizando esa contraseña! Aunque, oh oh, mira la respuesta JSON volcada: esto es después de que POST
creara el usuario. En la respuesta JSON, se incluye la propiedad password
en texto plano que el usuario acaba de establecer. ¡Vaya!
El flujo de una petición de escritura
Desglosemos esto. Nuestro proveedor de estado se utiliza para todas las operaciones GET
, así como para la operación PATCH
. Y fíjate, no vamos a establecer nunca la propiedad password
. No queremos devolver ese campo en el JSON, así que, correctamente, no lo estamos mapeando desde nuestra entidad a nuestro DTO. ¡Eso está bien!
Pero la operación POST
es la única situación en la que nunca se llama al proveedor. Estos datos se deserializan directamente en un nuevo objeto UserApi
y se pasan a nuestro procesador. Esto significa que nuestro DTO sí tiene establecida la contraseña simple... Y, en última instancia, ese objeto DTO es lo que se serializa y se devuelve al usuario.
Esto es una forma larga de decir que, en UserApi
, esta contraseña debe ser un campo de sólo escritura. El usuario nunca debería poder leerla. A continuación: hablemos de cómo podemos hacer personalizaciones como ésta dentro deUserApi
, evitando la complejidad de los grupos de serialización.
Hello dear SymfonyCast,
I have created a Processor where I want to check if the is a new User entity being posted.
I want to create a additional resource/ new entity UserProfile when this happens.
But I keep getting an error: Unable to generate an IRI for the item of type "App\ApiResource\UserProfileApi"
I have dealt with this error before, but know I cant really figure out how to deal with in this situation.
I tried setting it up within the mapper, where I get the same issue, but it does create a UserProfile after creating the User entity.
I thought maybe if I create the user First within the StateProcessor with the $this->persistProcessor and then proceed with creating the UserProfile but it did not work out as I thought.
I have AppFixtures where I can succesfully create userProfile after creating a User with no problems, so I am a little stuck.
I was thinking about creating a eventSubscriber/ Listener that check if a new User has a UserProfile, if not then create one and store it
I hope to get some pointers how to resolve this issue.
State Processor:
UserProfileApi:
UserAPi Resource: