Buy Access to Course
18.

Proveedor: Transformar Entidades en DTOs

|

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

No perdamos de vista el objetivo. Cuando utilizamos por primera vez stateOptions, provocó que se utilizara el proveedor central de la colección Doctrine. Eso está muy bien... salvo que devuelve entidades User, lo que significa que éstas se convirtieron en los objetos centrales de las rutas UserApi. Eso provoca una grave limitación a la hora de serializar: nuestras propiedades UserApi tienen que coincidir con nuestras propiedades User... de lo contrario, el serializador explota.

Para solucionarlo y darnos el control total, hemos creado nuestro propio proveedor de estado que llama al proveedor de colección central. Pero en lugar de devolver estos objetos de entidad User, vamos a devolver objetos UserApi para que se conviertan en los objetos centrales y se serialicen normalmente.

Mapeo al DTO

Crea un array $dtos y foreach sobre $entities as $entity. A continuación, añade al array $dtos llamando a un nuevo método: mapEntityToDto($entity).

25 lines | src/State/EntityToDtoStateProvider.php
// ... lines 1 - 9
class EntityToDtoStateProvider implements ProviderInterface
{
// ... lines 12 - 18
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
{
$entities = $this->collectionProvider->provide($operation, $uriVariables, $context);
dd(iterator_to_array($entities));
}
}

Pulsa "alt" + "enter" para añadir ese método al final. Esto devolverá un object. Bueno... será un objeto UserApi... pero estamos intentando mantener esta clase genérica. Voy a pegar algo de lógica -puedes copiarla del bloque de código de esta página- y luego pulsar "alt" + "enter" para añadir la declaración use que falta. Este código es específico del usuario... pero lo haremos más genérico más adelante, para que podamos reutilizar esta clase para los tesoros del dragón.

43 lines | src/State/EntityToDtoStateProvider.php
// ... lines 1 - 7
use App\ApiResource\UserApi;
// ... lines 9 - 10
class EntityToDtoStateProvider implements ProviderInterface
{
// ... lines 13 - 19
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
{
// ... lines 22 - 23
$dtos = [];
foreach ($entities as $entity) {
$dtos[] = $this->mapEntityToDto($entity);
}
// ... lines 28 - 29
}
private function mapEntityToDto(object $entity): object
{
$dto = new UserApi();
$dto->id = $entity->getId();
$dto->email = $entity->getEmail();
$dto->username = $entity->getUsername();
$dto->dragonTreasures = $entity->getDragonTreasures()->toArray();
return $dto;
}
}

Pero, ¿no es este código refrescantemente aburrido y comprensible? Sólo transferir propiedades del User $entity ... al DTO. Lo único un poco extravagante es donde cambiamos esta colección por una matriz... porque esta propiedad es una array en UserApi.

Por último, en la parte inferior de provide(), return $dtos.

43 lines | src/State/EntityToDtoStateProvider.php
// ... lines 1 - 7
use App\ApiResource\UserApi;
// ... lines 9 - 10
class EntityToDtoStateProvider implements ProviderInterface
{
// ... lines 13 - 19
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
{
// ... lines 22 - 23
$dtos = [];
foreach ($entities as $entity) {
$dtos[] = $this->mapEntityToDto($entity);
}
return $dtos;
}
private function mapEntityToDto(object $entity): object
{
$dto = new UserApi();
$dto->id = $entity->getId();
$dto->email = $entity->getEmail();
$dto->username = $entity->getUsername();
$dto->dragonTreasures = $entity->getDragonTreasures()->toArray();
return $dto;
}
}

Gracias a esto, los objetos centrales serán objetos UserApi... y éstos se serializarán normalmente: nada de fantasías donde el serializador intenta pasar de una entidadUser a una UserApi.

¡Drumoll, por favor! ¡Tada! Funciona... ¡con el mismo resultado que antes! Pero ahora tenemos la posibilidad de añadir propiedades personalizadas.

Añadir propiedades personalizadas

Vuelve a añadir public int $flameThrowingDistance.

37 lines | src/ApiResource/UserApi.php
// ... lines 1 - 21
class UserApi
{
// ... lines 24 - 34
public int $flameThrowingDistance = 0;
}

Luego, en el proveedor, es donde tenemos la oportunidad de establecer esas propiedades personalizadas, como$dto->flameThrowingDistance = rand(1, 10).

44 lines | src/State/EntityToDtoStateProvider.php
// ... lines 1 - 10
class EntityToDtoStateProvider implements ProviderInterface
{
// ... lines 13 - 31
private function mapEntityToDto(object $entity): object
{
// ... lines 34 - 38
$dto->flameThrowingDistance = rand(1, 10);
return $dto;
}
}

Y... ¡voilà! ¡Ahora sí que somos jodidamente peligrosos! Estamos reutilizando el núcleo de Doctrine CollectionProvider, pero con la posibilidad de añadir campos personalizados. Ah, y me olvidé de mencionarlo: los campos JSON-LD @id y @type están de vuelta. ¡Lo hemos conseguido!

Arreglar la paginación

Aunque, parece que ahora nos falta la paginación. El filtro está documentado... ¡pero el campo hydra:view que documenta la paginación ha desaparecido! Vale, en realidad, la paginación sigue funcionando. Observa: si voy a ?page=2, el primer usuario "usuario 1"... se convierte en "usuario 6". Sí, internamente, el núcleo CollectionProvider de Doctrine sigue leyendo la página actual y buscando el conjunto correcto de objetos para esa página. Nos falta el campo hdra:view de la parte inferior que describe la paginación simplemente porque ya no devolvemos un objeto que implementePaginationInterface.

Recuerda que esta variable $entities es en realidad un objeto Pagination. Ahora que sólo devolvemos una matriz, la API Platform piensa que no admitimos la paginación.

La solución es muy sencilla. En lugar de devolver $dtos,return new TraversablePaginator() con un nuevo \ArrayIterator() de $dtos. Para los demás argumentos, podemos coger los del paginador original. Como ayuda,assert($entities instanceof Paginator) (el de Doctrine ORM). Luego, aquí abajo, utiliza $entities->getCurrentPage(), $entities->getItemsPerPage(), y$entities->getTotalItems().

52 lines | src/State/EntityToDtoStateProvider.php
// ... lines 1 - 4
use ApiPlatform\Doctrine\Orm\Paginator;
// ... lines 6 - 7
use ApiPlatform\State\Pagination\TraversablePaginator;
// ... lines 9 - 12
class EntityToDtoStateProvider implements ProviderInterface
{
// ... lines 15 - 21
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
{
$entities = $this->collectionProvider->provide($operation, $uriVariables, $context);
assert($entities instanceof Paginator);
// ... lines 26 - 31
return new TraversablePaginator(
new \ArrayIterator($dtos),
$entities->getCurrentPage(),
$entities->getItemsPerPage(),
$entities->getTotalItems()
);
}
// ... lines 39 - 50
}

El proveedor de la colección principal ya ha hecho todo ese trabajo duro por nosotros. Qué amigo, actualiza ahora. Los resultados no cambian... pero aquí abajo, ¡ha vuelto hydra:view!

Siguiente: Hagamos que esto funcione para nuestras operaciones de artículos, como GET uno o PATCH. También aprovecharemos nuestro nuevo sistema para añadir algo a UserApi que antes teníamos.... pero esta vez, lo haremos de una forma mucho más chula.