Login to bookmark this video
Buy Access to Course
27.

¡Rápido! Crear un DTO DragonTreasure

|

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

Es hora de convertir nuestro DragonTreasure ApiResource en una clase DTO propiamente dicha. Empezaremos borrando un montón de cosas: todo lo relacionado con API Platform en DragonTreasure... para empezar de cero. Volveremos a añadir lo que necesitemos poco a poco. Adiós a las cosas del filtro... a los validadores... a todas las cosas del grupo de serialización... y luego podremos hacer algo de limpieza en nuestras propiedades. Teníamos aquí un código bastante complejo... y aunque no lo añadiremos todo de nuevo, añadiremos las cosas más importantes.

171 lines | src/Entity/DragonTreasure.php
// ... lines 1 - 2
namespace App\Entity;
use App\Repository\DragonTreasureRepository;
use Carbon\Carbon;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use function Symfony\Component\String\u;
#[ORM\Entity(repositoryClass: DragonTreasureRepository::class)]
class DragonTreasure
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
private ?string $name = null;
#[ORM\Column(type: Types::TEXT)]
private ?string $description = null;
/**
* The estimated value of this treasure, in gold coins.
*/
#[ORM\Column]
private ?int $value = 0;
#[ORM\Column]
private ?int $coolFactor = 0;
#[ORM\Column]
private \DateTimeImmutable $plunderedAt;
#[ORM\Column]
private bool $isPublished = false;
#[ORM\ManyToOne(inversedBy: 'dragonTreasures')]
#[ORM\JoinColumn(nullable: false)]
private ?User $owner = null;
/**
* @var bool Non-persisted property to help determine if the treasure is owned by the authenticated user
*/
private bool $isOwnedByAuthenticatedUser = false;
// ... lines 48 - 169
}

Deja que me desplace hacia abajo para asegurarme de que lo tenemos todo. Sí, ¡eso debería ser todo! Ahora tenemos una buena y aburrida clase de entidad. En src/ApiPlatform/, vamos a eliminar también AdminGroupsContextBuilder. Esta era una forma compleja de hacer que los campos pudieran ser leídos o escritos por nuestro administrador... pero vamos a solucionarlo con la seguridad deApiProperty. Deshazte también del normalizador personalizado... que añadía un campo y un grupo extra. Y por último, elimina las clases personalizadasDragonTreasureStateProvider y DragonTreasureStateProcessor.

Las extensiones de consulta se siguen llamando

Pero conservamos una cosa: DragonTreasureIsPublishedExtension. Como el nuevo sistema seguirá utilizando el núcleo de Doctrine CollectionProvider, estas extensiones de consulta seguirán funcionando y se seguirán llamando. Es una cosa menos de la que tenemos que preocuparnos.

Ve y actualiza la documentación. De acuerdo Sólo Quest y User. Aunque, puede que notes algunas cosas de DragonTreasure aquí abajo... porque UserApi tiene una relación con la entidad DragonTreasure. Así que, aunque DragonTreasure no sea un recurso API, API Platform sigue intentando documentar qué es ese campo en User. En realidad, no importa, porque vamos a arreglar eso y a utilizar completamente clases API en todas partes

Crear la clase DTO

En src/ApiResource/, crea la nueva clase: DragonTreasureApi.

26 lines | src/ApiResource/DragonTreasureApi.php
// ... lines 1 - 2
namespace App\ApiResource;
// ... lines 4 - 18
class DragonTreasureApi
{
// ... lines 21 - 24
}

A continuación, en UserApi, roba parte del código básico de nuestro #[ApiResource]... pégalo aquí y, por ahora, elimina operations. También podemos deshacernos de estas declaraciones use. ¡Perfecto!

Utilizaremos un shortName - Treasure - le daremos a este 10 elementos por página, y eliminaremos la línea security. Lo más importante es que tenemos provider yprocessor (tal como están aquí), y stateOptions, que apuntará aDragonTreasure::class.

26 lines | src/ApiResource/DragonTreasureApi.php
// ... lines 1 - 2
namespace App\ApiResource;
use ApiPlatform\Doctrine\Orm\State\Options;
// ... line 6
use ApiPlatform\Metadata\ApiResource;
use App\Entity\DragonTreasure;
use App\State\EntityClassDtoStateProcessor;
use App\State\EntityToDtoStateProvider;
#[ApiResource(
shortName: 'Treasure',
paginationItemsPerPage: 10,
provider: EntityToDtoStateProvider::class,
processor: EntityClassDtoStateProcessor::class,
stateOptions: new Options(entityClass: DragonTreasure::class),
)]
class DragonTreasureApi
{
// ... lines 21 - 24
}

También coge la propiedad $id. Como antes, en realidad no queremos que esto forme parte de nuestra API, así que es readable: false y writable: false. Aquí abajo, añadepublic ?string $name = null.

26 lines | src/ApiResource/DragonTreasureApi.php
// ... lines 1 - 2
namespace App\ApiResource;
use ApiPlatform\Doctrine\Orm\State\Options;
use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;
use App\Entity\DragonTreasure;
use App\State\EntityClassDtoStateProcessor;
use App\State\EntityToDtoStateProvider;
#[ApiResource(
shortName: 'Treasure',
paginationItemsPerPage: 10,
provider: EntityToDtoStateProvider::class,
processor: EntityClassDtoStateProcessor::class,
stateOptions: new Options(entityClass: DragonTreasure::class),
)]
class DragonTreasureApi
{
#[ApiProperty(readable: false, writable: false, identifier: true)]
public ?int $id = null;
public ?string $name = null;
}

¡Buen comienzo! Tenemos una clase pequeñita y... ¡qué demonios, vamos a probarla! Actualiza los documentos. ¡Sí! ¡Nuestras operaciones Tesoro están aquí! Si probamos la ruta de recogida... obtenemos:

No se ha encontrado ningún mapeador para DragonTreasure -&gt DragonTreasureApi

Añadir la clase mapeadora

¡Fantástico! El único trabajo real que tenemos que hacer es implementar esos mapeadores, ¡así que vamos allá!

En el directorio src/Mapper/, crea una clase llamadaDragonTreasureEntityToApiMapper. Ya lo hemos hecho antes: implementa MapperInterfacey añade el atributo #[AsMapper()]. Vamos a from: DragonTreasure::classto: DragonTreasureApi::class .

// ... lines 1 - 2
namespace App\Mapper;
use App\ApiResource\DragonTreasureApi;
use App\Entity\DragonTreasure;
use Symfonycasts\MicroMapper\AsMapper;
use Symfonycasts\MicroMapper\MapperInterface;
#[AsMapper(from: DragonTreasure::class, to: DragonTreasureApi::class)]
class DragonTreasureEntityToApiMapper implements MapperInterface
{
// ... lines 13 - 34
}

Y así de fácil, el micro mapeador sabe que debe utilizar esto. Genera los dos métodos de la interfaz: load() y populate(). Por cordura, añade $entity = $from, y assert() que $entity es uninstanceof DragonTreasure.

// ... lines 1 - 2
namespace App\Mapper;
use App\ApiResource\DragonTreasureApi;
use App\Entity\DragonTreasure;
use Symfonycasts\MicroMapper\AsMapper;
use Symfonycasts\MicroMapper\MapperInterface;
#[AsMapper(from: DragonTreasure::class, to: DragonTreasureApi::class)]
class DragonTreasureEntityToApiMapper implements MapperInterface
{
public function load(object $from, string $toClass, array $context): object
{
$entity = $from;
assert($entity instanceof DragonTreasure);
// ... lines 17 - 21
}
// ... lines 23 - 34
}

Aquí abajo, crea el objeto DTO con $dto = new DragonTreasureApi(). Y recuerda, el trabajo de load() es crear el objeto y ponerle un identificador si lo tiene. Así que añade $dto->id = $entity->getId(). Por último, return $dto.

// ... lines 1 - 2
namespace App\Mapper;
use App\ApiResource\DragonTreasureApi;
use App\Entity\DragonTreasure;
use Symfonycasts\MicroMapper\AsMapper;
use Symfonycasts\MicroMapper\MapperInterface;
#[AsMapper(from: DragonTreasure::class, to: DragonTreasureApi::class)]
class DragonTreasureEntityToApiMapper implements MapperInterface
{
public function load(object $from, string $toClass, array $context): object
{
$entity = $from;
assert($entity instanceof DragonTreasure);
$dto = new DragonTreasureApi();
$dto->id = $entity->getId();
return $dto;
}
// ... lines 23 - 34
}

Para populate(), roba unas líneas de arriba que establecen la variable $entity... luego di también $dto = $to, y añade una más assert() que $dto es uninstanceof DragonTreasureApi.

// ... lines 1 - 2
namespace App\Mapper;
use App\ApiResource\DragonTreasureApi;
use App\Entity\DragonTreasure;
use Symfonycasts\MicroMapper\AsMapper;
use Symfonycasts\MicroMapper\MapperInterface;
#[AsMapper(from: DragonTreasure::class, to: DragonTreasureApi::class)]
class DragonTreasureEntityToApiMapper implements MapperInterface
{
public function load(object $from, string $toClass, array $context): object
{
$entity = $from;
assert($entity instanceof DragonTreasure);
$dto = new DragonTreasureApi();
$dto->id = $entity->getId();
return $dto;
}
public function populate(object $from, object $to, array $context): object
{
$entity = $from;
$dto = $to;
assert($entity instanceof DragonTreasure);
assert($dto instanceof DragonTreasureApi);
// ... lines 30 - 33
}
}

La única propiedad que tenemos en nuestro DTO ahora mismo es name, así que todo lo que necesitamos es$dto->name = $entity->getName(). Al final, return $dto.

// ... lines 1 - 2
namespace App\Mapper;
use App\ApiResource\DragonTreasureApi;
use App\Entity\DragonTreasure;
use Symfonycasts\MicroMapper\AsMapper;
use Symfonycasts\MicroMapper\MapperInterface;
#[AsMapper(from: DragonTreasure::class, to: DragonTreasureApi::class)]
class DragonTreasureEntityToApiMapper implements MapperInterface
{
public function load(object $from, string $toClass, array $context): object
{
$entity = $from;
assert($entity instanceof DragonTreasure);
$dto = new DragonTreasureApi();
$dto->id = $entity->getId();
return $dto;
}
public function populate(object $from, object $to, array $context): object
{
$entity = $from;
$dto = $to;
assert($entity instanceof DragonTreasure);
assert($dto instanceof DragonTreasureApi);
$dto->name = $entity->getName();
return $dto;
}
}

Y, ¡gente! Acabamos de crear una clase que mapea desde la entidad al DTO... y nuestro proveedor de estado utiliza internamente el micro mapeador... así que creo que esto debería... ¡simplemente funcionar!

Y... ¡funciona! ¡Vaya! Con sólo la clase Recurso API y este único mapeador, ya tenemos una clase Recurso API personalizada potenciada por la base de datos. ¡Guau!

Añadir un campo de relación

Ahora las cosas se ponen interesantes. Cada entidad DragonTreasure tiene un propietario, que es una relación con la entidad User. En nuestra API, vamos a tener la misma relación. Pero en lugar de ser una relación de DragonTreasureApi a un objeto de entidadUser, será a un objeto UserApi.

¡Compruébalo! Digamos public ?UserApi $owner = null.

28 lines | src/ApiResource/DragonTreasureApi.php
// ... lines 1 - 18
class DragonTreasureApi
{
// ... lines 21 - 25
public ?UserApi $owner = null;
}

Vamos a rellenarlo en el mapeador. Aquí abajo, digamos $dto->owner =... pero... espera un segundo. Esto no es tan sencillo como decir $entity->getOwner(), porque ése es un objeto entidad de usuario. ¡Necesitamos un objeto UserApi! ¿Se te ocurre algo que sea realmente bueno convirtiendo una entidad User en UserApi? Así es, ¡MicroMapper!

Aquí arriba, inyecta private MicroMapperInterface $microMapper... y, aquí abajo, di $dto->owner = $this->microMapper->map() para mapear de$entity->getOwner() -el objeto entidad User - a UserApi::class.

// ... lines 1 - 9
use Symfonycasts\MicroMapper\MicroMapperInterface;
// ... lines 11 - 12
class DragonTreasureEntityToApiMapper implements MapperInterface
{
public function __construct(
private MicroMapperInterface $microMapper,
)
{
}
// ... lines 20 - 31
public function populate(object $from, object $to, array $context): object
{
// ... lines 34 - 39
$dto->owner = $this->microMapper->map($entity->getOwner(), UserApi::class);
// ... lines 41 - 48
}
}

¿No es genial? Una cosa que debes tener en cuenta es que si, en tu sistema,$entity->getOwner() puede ser null, debes codificar para ello. Por ejemplo, si tienes un propietario, llama al mapeador; si no, simplemente establece owner en null... o no lo establezcas en absoluto. En nuestro caso, siempre vamos a tener un propietario, así que esto debería ser seguro.

Vamos a probarlo Actualiza y... oooh. Tenemos un campo owner y es un IRI. ¿Por qué aparece como un IRI? Porque API Platform reconoce que el objeto UserApi es un recurso API. ¿Y cómo muestra los recursos API que son relaciones? Exacto Los muestra como un IRI. Así que eso es exactamente lo que queríamos ver.

Añadir más campos

Rellenemos el resto de campos que necesitamos: Lo haré superrápido. Uno de los campos que voy a añadir es $shortDescription. Antes era un campo personalizado... pero ahora será más sencillo. Otro campo personalizado que teníamos era $isMine, que también será simplemente una propiedad normal.

40 lines | src/ApiResource/DragonTreasureApi.php
// ... lines 1 - 18
class DragonTreasureApi
{
// ... lines 21 - 25
public ?string $description = null;
public int $value = 0;
public int $coolFactor = 0;
public ?UserApi $owner = null;
public ?string $shortDescription = null;
public ?string $plunderedAtAgo = null;
public ?bool $isMine = null;
}

En nuestro mapeador, vamos a configurarlo todo. Pasaré rápidamente por las partes aburridas. Pero $shortDescription es un poco interesante. Antes, enDragonTreasure, teníamos un método getShortDescription() y eso se exponía directamente como campo de la API.

Con la nueva configuración, es una propiedad normal como cualquier otra, y nos encargamos de establecer los datos personalizados en nuestro mapeador: $shortDescription es igual a$entity->getShortDescription(). Por último, para $dto->isMine, lo codificamos temporalmente como true.

// ... lines 1 - 12
class DragonTreasureEntityToApiMapper implements MapperInterface
{
// ... lines 15 - 31
public function populate(object $from, object $to, array $context): object
{
// ... lines 34 - 40
$dto->description = $entity->getDescription();
$dto->value = $entity->getValue();
$dto->coolFactor = $entity->getCoolFactor();
$dto->shortDescription = $entity->getShortDescription();
$dto->plunderedAtAgo = $entity->getPlunderedAtAgo();
$dto->isMine = true;
// ... lines 47 - 48
}
}

Vamos a comprobarlo Actualiza y... ¡qué bonito!

En tests/Functional/, tenemos DragonTreasureResourceTest. Aquí, tenemostestGetCollectionOfTreasures(), que comprueba que sólo vemos elementos publicados. Si nuestra extensión de consulta sigue funcionando, esto pasará. Esto también comprueba que vemos las claves correctas.

Veamos si funciona:

symfony php bin/phpunit --filter=testGetCollectionOfTreasures

Funciona. Alucinante.

Rellenar el extraño campo isMine

Antes de terminar, vamos a arreglar el código true de isMine. Esto es fácil, pero demuestra lo agradable que es trabajar con campos personalizados. En nuestro mapeador, éste es un servicio, así que podemos inyectar otros servicios como el de $security. Luego, podemos rellenarlo con los datos que queramos. Así que isMinees verdadero si $this->security->getUser() es igual a DragonTreasure, getOwner()(que es un objeto de entidad User ).

// ... lines 1 - 7
use Symfony\Bundle\SecurityBundle\Security;
// ... lines 9 - 13
class DragonTreasureEntityToApiMapper implements MapperInterface
{
public function __construct(
// ... line 17
private Security $security,
)
{
}
// ... lines 22 - 33
public function populate(object $from, object $to, array $context): object
{
// ... lines 36 - 47
$dto->isMine = $this->security->getUser() && $this->security->getUser() === $entity->getOwner();
// ... lines 49 - 50
}
}

Prueba la prueba una vez más para asegurarte de que funciona, y... funciona. ¡Guau!

Lo siguiente: Quiero profundizar en las relaciones en nuestra API potenciada por DTO. Porque, si no tienes cuidado, ¡podemos llegar a la temida recursividad infinita!