Buy Access to Course
20.

DTO -> Procesador del Estado de la Entidad

|

Share this awesome video!

|

Keep on Learning!

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Ya 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 idpoblado, 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.

15 lines | src/State/EntityClassDtoStateProcessor.php
// ... 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.

39 lines | src/ApiResource/UserApi.php
// ... lines 1 - 9
use App\State\EntityClassDtoStateProcessor;
// ... lines 11 - 13
#[ApiResource(
// ... 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.

39 lines | src/ApiResource/UserApi.php
// ... lines 1 - 9
use App\State\EntityClassDtoStateProcessor;
// ... lines 11 - 13
#[ApiResource(
// ... 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 UserApide 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).

49 lines | src/State/EntityClassDtoStateProcessor.php
// ... 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).

49 lines | src/State/EntityClassDtoStateProcessor.php
// ... 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.

49 lines | src/State/EntityClassDtoStateProcessor.php
// ... 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.

49 lines | src/State/EntityClassDtoStateProcessor.php
// ... 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().

49 lines | src/State/EntityClassDtoStateProcessor.php
// ... 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 TODOpara handle dragon treasures. Céntrate en lo fácil... y al final,return $entity.

49 lines | src/State/EntityClassDtoStateProcessor.php
// ... 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.