Chapters
-
Course Code
Subscribe to download the code!
Subscribe to download the code!
-
This Video
Subscribe to download the video!
Subscribe to download the video!
-
Subtitles
Subscribe to download the subtitles!
Subscribe to download the subtitles!
-
Course Script
Subscribe to download the script!
Subscribe to download the script!
Scroll down to the script below, click on any sentence (including terminal blocks) to jump to that spot in the video!
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¡Nuestro UserAPI
es ahora una clase de recursos API totalmente funcional! Tenemos nuestroEntityToDtoStateProvider
, que llama al proveedor de estado central de Doctrine, y que nos proporciona todo lo bueno, como la consulta, el filtrado y la paginación. Luego, aquí abajo, aprovechamos el sistema MicroMapper para convertir los objetos $entity
en objetosUserApi
.
Y hacemos lo mismo en el procesador. Utilizamos MicroMapper para pasar deUserApi
a nuestra entidad User
... y luego llamamos al procesador de estado central de Doctrine para que se encargue de guardar o borrar. ¡Me encanta!
Nuestro sueño es crear un DragonTreasureApi
y repetir toda esta magia. Y si podemos hacer que estas clases de procesador y proveedor sean completamente genéricas... eso va a ser superfácil. Así que ¡hagámoslo!
Hacer genérico al proveedor
Empieza en el proveedor. Si buscas "usuario", sólo hay un lugar: donde le decimos a MicroMapper en qué clase convertir nuestro $entity
. ¿Podemos... obtener esto dinámicamente? Aquí arriba, nuestro proveedor recibe el $operation
y el $context
. Volquemos ambos.
Como esto está en nuestro proveedor... podemos ir a actualizar la ruta Colección y... ¡boom! Se trata de una operación GetCollection
... y compruébalo. El objeto de la operación almacena la clase ApiResource a la que está asociado
Así que por aquí, es sencillo: $resourceClass = $operation->getClass()
. Ahora que tenemos eso, aquí abajo, conviértelo en un argumento - string $resourceClass
- y pásalo en su lugar. Por último, tenemos que añadir $resourceClass
como argumento cuando llamemos a mapEntityToDto()
ahí... y justo ahí. Elimina la declaración use
que ya no necesitamos y... así de fácil... ¡sigue funcionando!
Hacer que el procesador sea genérico
¡Ya estamos en marcha! Dirígete al procesador y busca "usuario". Ah, tenemos el mismo problema excepto que, esta vez, necesitamos la clase de entidad User
.
¡Vale! Arriba, dd($operation)
. Y para ello, necesitamos ejecutar una de nuestras pruebas:
symfony php bin/phpunit --filter=testPostToCreateUser
Y... ¡ya está! Vemos la operación Post
... y la clase es, por supuesto,UserApi
. Pero esta vez necesitamos la clase User
. Recuerda: en UserApi
, utilizamos stateOptions
para decir que UserApi
está vinculada a la entidad User
. Y ahora, podemos leer esta información de la operación. Si nos desplazamos un poco hacia abajo... ahí está: la propiedad stateOptions
con el objeto Options
, y entityClass
dentro.
¡Genial! De vuelta al procesador, hacia arriba... quita el dd()
y empieza por $stateOptions = $operation->getStateOptions()
. Luego, para ayudar a mi editor (y también por si configuro algo mal), assert($stateOptions instanceof Options)
(el del ORM Doctrine).
Puedes utilizar diferentes clases Options
para $stateOptions
... como si estuvieras obteniendo datos de ElasticSearch, pero sabemos que estamos utilizando esta de Doctrine. A continuación, digamos $entityClass = $stateOptions->getEntityClass()
.
Y... no necesitamos este assert()
de aquí abajo, entonces pasa $entityClass
amapDtoToEntity()
. Por último, úsalo con string $entityClass
... y pásalo también aquí.
Cuando ahora busquemos "usuario"... podemos deshacernos de las dos declaraciones use
... y... ¡ya estamos limpios! ¡Es genérico! ¡Prueba el test!
symfony php bin/phpunit --filter=testPostToCreateUser
¡Ya está! ¡Estamos listos! ¡Tenemos un proveedor y un procesador reutilizables! A continuación, creemos una clase DragonTreasureApi
, repitamos esta magia, ¡y veamos lo rápido que conseguimos que las cosas encajen!
4 Comments
Thanks for posting this! This is indeed a problem! I'm fine with your solution! Some other users have pointed out that you can also fix this in the mapper, which I quite like! Here's a description of that approach: https://symfonycasts.com/screencast/api-platform-extending#comment-31551 - if it's unclear let me know, but I think you'll understand the approach.
Cheers!
One issue I came across with the generic/custom StateProcessor
is that not all Entities use id/getId()
as their identifier.
So in my DTO class, I leveraged the extraProperties
array in the #[ApiResource]
to expose that information to the StateProcessor
.
The upside is that this works! The downside is that I already told ApiPlatform what my resource's ID is in my DTO class in the properties using the ApiProperty
attribute. But there doesn't seem to be a way to get that information in the process
method of the StateProcessor
. Would be nice to not have to add this extraProperties
config, but hey, this does the trick!
Any thoughts about gotchas with this approach?
DTO class
<?php
namespace App\ApiResource;
use ApiPlatform\Doctrine\Orm\State\Options;
use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;
use App\Entity\Bookmark;
use App\Entity\User;
use App\State\DtoToEntityStateProcessor;
use App\State\EntityToDtoStateProvider;
use Ramsey\Uuid\UuidInterface;
use Symfony\Component\Serializer\Attribute\Ignore;
#[ApiResource(
shortName: 'Bookmark',
paginationItemsPerPage: 10,
provider: EntityToDtoStateProvider::class,
processor: DtoToEntityStateProcessor::class,
stateOptions: new Options(
entityClass: Bookmark::class,
),
extraProperties: [
'identifierProperty' => 'uuid',
'identifierGetter' => 'getUuid',
],
)]
class BookmarkApi
{
#[ApiProperty(identifier: false)]
#[Ignore]
public ?int $id = null;
#[ApiProperty(identifier: true)]
public ?UuidInterface $uuid = null;
public ?string $title = null;
public ?string $description = null;
public ?User $createdBy = null;
public ?User $updatedBy = null;
}
DtoToEntityStateProcessor class
<?php
namespace App\State;
use ApiPlatform\Doctrine\Common\State\PersistProcessor;
use ApiPlatform\Doctrine\Common\State\RemoveProcessor;
use ApiPlatform\Doctrine\Orm\State\Options;
use ApiPlatform\Metadata\DeleteOperationInterface;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfonycasts\MicroMapper\MicroMapperInterface;
class DtoToEntityStateProcessor implements ProcessorInterface
{
public function __construct(
#[Autowire(service: PersistProcessor::class)]
private PersistProcessor $persistProcessor,
#[Autowire(service: RemoveProcessor::class)]
private RemoveProcessor $removeProcessor,
private MicroMapperInterface $microMapper,
) {
}
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = [])
{
$stateOptions = $operation->getStateOptions();
assert($stateOptions instanceof Options);
$entityClass = $stateOptions->getEntityClass();
$entity = $this->mapDtoToEntity($data, $entityClass);
if ($operation instanceof DeleteOperationInterface) {
$this->removeProcessor->process($entity, $operation, $uriVariables, $context);
return null;
}
$this->persistProcessor->process($entity, $operation, $uriVariables, $context);
// Allows custom identifiers to be set in the DTO class
$extraProperties = $operation->getExtraProperties();
$identifierProperty = $extraProperties['identifierProperty'] ?? 'id';
$identifierGetter = $extraProperties['identifierGetter'] ?? 'getId';
$data->{$identifierProperty} = $entity->{$identifierGetter}();
return $data;
}
private function mapDtoToEntity(object $dto, string $entityClass): object
{
return $this->microMapper->map($dto, $entityClass);
}
}
Hey @LCBWeb!
What a cool use of extraProperties
! Well done 👏. But I hear what you mean: this info should already be available. Grabbing the info looks oddly complex. The closest I could find is what's done in this class: https://github.com/api-platform/core/blob/main/src/Api/IdentifiersExtractor.php
If you poke in there and have success, let me know. Otherwise, it's a tiny bit of duplication, but I think your solution is fine too. And if you only have a few different ids, you could always use a method_exists()
to check for the 2-3 possible getter methods for your id and determine it that way.
Cheers!
"Houston: no signs of life"
Start the conversation!
What PHP libraries does this tutorial use?
// composer.json
{
"require": {
"php": ">=8.1",
"ext-ctype": "*",
"ext-iconv": "*",
"api-platform/core": "3.1.x-dev", // 3.1.x-dev
"doctrine/annotations": "^2.0", // 2.0.1
"doctrine/doctrine-bundle": "^2.8", // 2.10.2
"doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.4
"doctrine/orm": "^2.14", // 2.16.1
"nelmio/cors-bundle": "^2.2", // 2.3.1
"nesbot/carbon": "^2.64", // 2.69.0
"phpdocumentor/reflection-docblock": "^5.3", // 5.3.0
"phpstan/phpdoc-parser": "^1.15", // 1.23.1
"symfony/asset": "6.3.*", // v6.3.0
"symfony/console": "6.3.*", // v6.3.2
"symfony/dotenv": "6.3.*", // v6.3.0
"symfony/expression-language": "6.3.*", // v6.3.0
"symfony/flex": "^2", // v2.3.3
"symfony/framework-bundle": "6.3.*", // v6.3.2
"symfony/property-access": "6.3.*", // v6.3.2
"symfony/property-info": "6.3.*", // v6.3.0
"symfony/runtime": "6.3.*", // v6.3.2
"symfony/security-bundle": "6.3.*", // v6.3.3
"symfony/serializer": "6.3.*", // v6.3.3
"symfony/stimulus-bundle": "^2.9", // v2.10.0
"symfony/string": "6.3.*", // v6.3.2
"symfony/twig-bundle": "6.3.*", // v6.3.0
"symfony/ux-react": "^2.6", // v2.10.0
"symfony/ux-vue": "^2.7", // v2.10.0
"symfony/validator": "6.3.*", // v6.3.2
"symfony/webpack-encore-bundle": "^2.0", // v2.0.1
"symfony/yaml": "6.3.*", // v6.3.3
"symfonycasts/micro-mapper": "^0.1.0" // v0.1.1
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.4
"mtdowling/jmespath.php": "^2.6", // 2.6.1
"phpunit/phpunit": "^9.5", // 9.6.11
"symfony/browser-kit": "6.3.*", // v6.3.2
"symfony/css-selector": "6.3.*", // v6.3.2
"symfony/debug-bundle": "6.3.*", // v6.3.2
"symfony/maker-bundle": "^1.48", // v1.50.0
"symfony/monolog-bundle": "^3.0", // v3.8.0
"symfony/phpunit-bridge": "^6.2", // v6.3.2
"symfony/stopwatch": "6.3.*", // v6.3.0
"symfony/web-profiler-bundle": "6.3.*", // v6.3.2
"zenstruck/browser": "^1.2", // v1.4.0
"zenstruck/foundry": "^1.26" // v1.35.0
}
}
It seems the example provided does not work when using Writable Relation Fields with serialization groups.
Trying to create a new object though a relationship throws the dreaded
Unable to generate an IRI for the item of type \"App\\ApiResource\\EntityApi\""
error. This seems to only affect Post Operations due to relationships not being properly hydrated during serialization. Get, Get Collection, and Patch all seem to work fine here.For example, Lets say you have two entities: Business - OneToOne - Address. If you were to try create a new business by posting
{ "name":"Little Caesars", "address": { "city": "Detroit" }}
to your/api/business/
endpoint you would see the following errorUnable to generate an IRI for the item of type \"App\\ApiResource\\AddressApi\""
. This is happening because the example in the script does not fully hydrate the DTO before api-platform tries to serialize.Below is a quick and dirty fix that resolved the issue for me on my end.
Hope this helps someone!