DTO -> Procesador del Estado de la Entidad
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 SubscribeYa hemos comprobado el aspecto "proveedor" de nuestra nueva clase UserApi
. Así que vamos a centrarnos en el procesador para poder guardar las cosas. Y tenemos algunas pruebas bastante encantadoras para nuestras rutas User
. AbrirUserResourceTest
.
Anatomía del procesador de peticiones y estados
Vale, testPostToCreateUser()
, publica algunos datos, crea el usuario y, a continuación, comprueba que la contraseña que hemos publicado funciona al iniciar sesión. Añade->dump()
para ayudarnos a ver lo que ocurre. A continuación, copia el nombre del método y ejecútalo:
symfony php bin/phpunit --filter=testPostToCreateUser
No te sorprendas... falla:
El código de estado de respuesta actual es 400, pero se esperaba 201.
El volcado es realmente útil. ¡Es nuestro error favorito!
No se ha podido generar un IRI para el elemento de tipo
UserApi
.
Ya hemos hablado de lo que ocurre: el JSON se deserializa en un objeto UserApi
. ¡Bien! Entonces se llama al núcleo de la Doctrine PersistProcessor
porque ése es el processor
por defecto cuando se utiliza stateOptions
. Pero... como nuestroUserApi
no es una entidad, PersistProcessor
no hace nada. Por último, la API Platform vuelve a serializar el UserApi
en JSON... pero sin el id
poblado, no consigue generar el IRI.
¡Fíjate! En UserApi
, por defecto temporalmente $id
a 5
. Cuando intentamos la prueba ahora...
symfony php bin/phpunit --filter=testPostToCreateUser
Parece que funciona. Vale, falla... pero sólo después... aquí abajo enUserResourceTest
línea 33. El POST se realiza correctamente.
Creación del procesador de estado
Mira la respuesta de arriba, está devolviendo este JSON de usuario. Pero, aún así, no se está guardando nada. Vuelve a cambiar el id a null. Tenemos que solucionar esta falta de guardado creando un nuevo procesador de estado. Así que gíralo y ejecútalo:
php bin/console make:state-processor
Llámalo EntityClassDtoStateProcessor
porque, de nuevo, vamos a hacer que esta clase sea genérica para que funcione con cualquier clase de recurso de la API que esté vinculada a una entidad Doctrine. La utilizaremos más adelante para DragonTreasure
.
// ... lines 1 - 2 | |
namespace App\State; | |
use ApiPlatform\Metadata\Operation; | |
use ApiPlatform\State\ProcessorInterface; | |
class EntityClassDtoStateProcessor implements ProcessorInterface | |
{ | |
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): void | |
{ | |
// Handle the state | |
} | |
} |
Con el procesador vacío generado, ve a conectarlo en UserApi
conprocessor: EntityClassDtoStateProcessor::class
.
// ... lines 1 - 9 | |
use App\State\EntityClassDtoStateProcessor; | |
// ... lines 11 - 13 | |
( | |
// ... lines 15 - 17 | |
processor: EntityClassDtoStateProcessor::class, | |
// ... line 19 | |
) | |
// ... lines 21 - 23 | |
class UserApi | |
{ | |
// ... lines 26 - 37 | |
} |
A partir de ahora, cada vez que hagamos POST, PATCH o DELETE de este recurso, se llamará a este procesador.
Volver a asignar el DTO a una entidad
Pero, ¿qué es exactamente esta variable $data
? Puede que lo adivines, pero por si acaso, vamos a dd($data)
... y volvamos a ejecutar la prueba.
// ... lines 1 - 9 | |
use App\State\EntityClassDtoStateProcessor; | |
// ... lines 11 - 13 | |
( | |
// ... lines 15 - 17 | |
processor: EntityClassDtoStateProcessor::class, | |
// ... line 19 | |
) | |
// ... lines 21 - 23 | |
class UserApi | |
{ | |
// ... lines 26 - 37 | |
} |
symfony php bin/phpunit --filter=testPostToCreateUser
Sí, ¡es un objeto UserApi
! El JSON que enviamos se deserializa en este objeto UserApi
, y luego se pasa a nuestro procesador de estado. El objeto UserApi
es el "objeto central" dentro de API Platform para esta petición.
Nuestro trabajo en el procesador de estado es sencillo pero importante: convertir este UserApi
de nuevo en una entidad User
para que podamos guardarlo. Digamos que assert($data instanceof UserApi)
y, dentro, $entity =
se establecen en una nueva función de ayuda: $this->mapDtoToEntity($data)
. Debajo, dd($entity)
.
// ... lines 1 - 6 | |
use App\ApiResource\UserApi; | |
// ... lines 8 - 10 | |
class EntityClassDtoStateProcessor implements ProcessorInterface | |
{ | |
// ... lines 13 - 19 | |
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): void | |
{ | |
assert($data instanceof UserApi); | |
$entity = $this->mapDtoToEntity($data); | |
dd($entity); | |
} | |
// ... lines 27 - 47 | |
} |
Luego ve a añadir ese nuevo private function mapDtoToEntity()
, que aceptará un argumentoobject $dto
y devolverá otro object
.
De nuevo, sabemos que esto realmente aceptará un objeto UserApi
y devolverá una entidad User
... pero estamos intentando mantener esta clase genérica para poder reutilizarla más adelante. Aunque vamos a tener algo de código específico de usuario aquí abajo temporalmente. De hecho, para ayudar a nuestro editor, añade otro assert($dto instanceof UserApi)
.
// ... lines 1 - 6 | |
use App\ApiResource\UserApi; | |
// ... lines 8 - 10 | |
class EntityClassDtoStateProcessor implements ProcessorInterface | |
{ | |
// ... lines 13 - 19 | |
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): void | |
{ | |
assert($data instanceof UserApi); | |
$entity = $this->mapDtoToEntity($data); | |
dd($entity); | |
} | |
private function mapDtoToEntity(object $dto): object | |
{ | |
assert($dto instanceof UserApi); | |
// ... lines 31 - 46 | |
} | |
} |
Consulta de la entidad existente
Tenemos que pensar en dos casos diferentes. El primero es cuando vamos a crear un usuario totalmente nuevo. En ese caso, $dto
tendrá un id null
. Y eso significa que deberíamos crear un objeto User
nuevo. El otro caso es si hiciéramos, por ejemplo, una petición a PATCH
para editar un usuario. En ese caso, el proveedor de elementos cargará primero esa entidad User
de la base de datos... nuestro proveedor la convertirá en un objeto UserApi
con id
igual a 6
... y eso nos lo pasará finalmente aquí. Si el id
es 6... no queremos crear un nuevo objeto User
: queremos consultar la base de datos en busca del User
existente. Nuestro trabajo consiste en manejar ambas situaciones.
Deshaz los cambios en la prueba para no romper nada... y ahora, if
$dto->id
, tenemos que consultar por un User
existente. Para ello, en la parte superior, añade un constructor con private UserRepository $userRepository
.
// ... lines 1 - 8 | |
use App\Repository\UserRepository; | |
class EntityClassDtoStateProcessor implements ProcessorInterface | |
{ | |
public function __construct( | |
private UserRepository $userRepository | |
) | |
{ | |
} | |
// ... lines 19 - 47 | |
} |
Aquí abajo, digamos $entity = $this->userRepository->find($dto->id)
.
Si no encontramos ese User
, lanza una gran excepción gigante que provocará un error 500 con Entity %d not found
.
// ... lines 1 - 8 | |
use App\Repository\UserRepository; | |
class EntityClassDtoStateProcessor implements ProcessorInterface | |
{ | |
public function __construct( | |
private UserRepository $userRepository | |
) | |
{ | |
} | |
// ... lines 19 - 27 | |
private function mapDtoToEntity(object $dto): object | |
{ | |
assert($dto instanceof UserApi); | |
if ($dto->id) { | |
$entity = $this->userRepository->find($dto->id); | |
if (!$entity) { | |
throw new \Exception(sprintf('Entity %d not found', $dto->id)); | |
} | |
// ... lines 37 - 38 | |
} | |
// ... lines 40 - 46 | |
} | |
} |
Puede que te preguntes:
¿No debería esto desencadenar un error 404 en su lugar?
La respuesta, en este caso, es no. Si nos encontramos en esta situación, significa que el proveedor de estado del artículo ya ha consultado con éxito un User
con este id. Así que no debería haber forma de que, de repente, no lo encontremos. Hay algunas excepciones a esto, como si permitieras a tu usuario cambiar su id
... o si permitieras a los usuarios crear objetos completamente nuevos y establecer el id manualmente... pero para la mayoría de las situaciones, incluida la nuestra, si esto ocurre, algo ha ido mal.
A continuación, si no tenemos un id
, digamos $entity = new User()
.
// ... lines 1 - 7 | |
use App\Entity\User; | |
use App\Repository\UserRepository; | |
class EntityClassDtoStateProcessor implements ProcessorInterface | |
{ | |
public function __construct( | |
private UserRepository $userRepository | |
) | |
{ | |
} | |
// ... lines 19 - 27 | |
private function mapDtoToEntity(object $dto): object | |
{ | |
assert($dto instanceof UserApi); | |
if ($dto->id) { | |
$entity = $this->userRepository->find($dto->id); | |
if (!$entity) { | |
throw new \Exception(sprintf('Entity %d not found', $dto->id)); | |
} | |
} else { | |
$entity = new User(); | |
} | |
// ... lines 40 - 46 | |
} | |
} |
¡Listo! En ambos casos, aquí abajo, vamos a mapear el objeto $dto
al objeto$entity
. Este código es aburrido... así que lo haré más rápido. Para la contraseña, pon un TODO
temporalmente porque aún tenemos que hacer el hash. Añade también un TODO
para handle dragon treasures
. Céntrate en lo fácil... y al final,return $entity
.
// ... lines 1 - 7 | |
use App\Entity\User; | |
use App\Repository\UserRepository; | |
class EntityClassDtoStateProcessor implements ProcessorInterface | |
{ | |
public function __construct( | |
private UserRepository $userRepository | |
) | |
{ | |
} | |
// ... lines 19 - 27 | |
private function mapDtoToEntity(object $dto): object | |
{ | |
assert($dto instanceof UserApi); | |
if ($dto->id) { | |
$entity = $this->userRepository->find($dto->id); | |
if (!$entity) { | |
throw new \Exception(sprintf('Entity %d not found', $dto->id)); | |
} | |
} else { | |
$entity = new User(); | |
} | |
$entity->setEmail($dto->email); | |
$entity->setUsername($dto->username); | |
$entity->setPassword('TODO properly'); | |
// TODO: handle dragon treasures | |
return $entity; | |
} | |
} |
Si hemos hecho las cosas bien, cogeremos el UserApi
, lo transformaremos en un $entity
y lo volcaremos. Vuelve a ejecutar la prueba:
symfony php bin/phpunit --filter=testPostToCreateUser
Y... ¡404! Veamos qué ha pasado aquí. Ah... claro. No he vuelto a montar el test. Esto debería ser ->post('/api/users')
. Inténtalo de nuevo y... ¡ya está! ¡Ahí está nuestro objeto entidad User
con el correo electrónico y el nombre de usuario transferidos correctamente!
Siguiente: Guardemos esto aprovechando el núcleo de Doctrine PersistProcessor
yRemoveProcessor
. También nos encargaremos del hashing de la contraseña. Al final, nuestras pruebas de usuario estarán pasando con éxito.
I have an Endpoint with takes a UUID property and an array of UUIDs via POST.
In API Platform 2.6, I had a class tagged as ApiResource with has a path and an Output-Class defined (another ApiResource, which is also a Doctrine-Entity).
Then I had a DataTransformer, which took the ApiResource, which acted as Input-DTO, used the IDs to make a Database-Query and returned a collection of Entities.
Now, I tried to do the same with the help of a data-processor, because there are no data-transformers in API-Platform > 3.
But always, when I try to return more than one entity, I get a 500 response coming from "get_class(): Argument #1 ($object) must be of type object, array given".
How can I take a post with ID's, make the query, and return a collection of entities, without data-transformers?