Reusable Entity->Dto Provider & Processor
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 SubscribeOur UserAPI
is now a fully functional API resource class! We've got our EntityToDtoStateProvider
, which calls the core state provider from Doctrine, and that gives us all the good stuff, like querying, filtering, and pagination. Then, down here, we leverage the MicroMapper system to convert the $entity
objects into UserApi
objects.
And we do the same thing in the processor. We use MicroMapper to go from UserApi
to our User
entity... then call the core Doctrine state processor to let it do the saving or deleting. I love that!
Our dream is to create a DragonTreasureApi
and repeat all of this magic. And if we can make these processor and provider classes completely generic... that's going to be super easy. So let's do it!
Making the Provider Generic
Start in the provider. If you search for "user", there's only one spot: where we tell MicroMapper which class to convert our $entity
into. Can... we fetch this dynamically? Up here, our provider receives the $operation
and $context
. Let's dump both of these.
// ... lines 1 - 15 | |
class EntityToDtoStateProvider implements ProviderInterface | |
{ | |
// ... lines 18 - 26 | |
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null | |
{ | |
dd($operation, $context); | |
// ... lines 30 - 53 | |
} | |
// ... lines 55 - 59 | |
} |
Since this is in our provider... we can just go refresh the Collection endpoint and... boom! This is a GetCollection
operation... and check it out. The operation object stores the ApiResource class that it's attached to!
So over here, it's simple: $resourceClass = $operation->getClass()
. Now that we've got that, down here, make it an argument - string $resourceClass
- and pass that instead. Finally, we need to add $resourceClass
as the argument when we call mapEntityToDto()
there... and right there. Remove the use
statement we don't need anymore and... just like that... it still works!
// ... lines 1 - 14 | |
class EntityToDtoStateProvider implements ProviderInterface | |
{ | |
// ... lines 17 - 25 | |
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null | |
{ | |
$resourceClass = $operation->getClass(); | |
if ($operation instanceof CollectionOperationInterface) { | |
// ... lines 30 - 33 | |
foreach ($entities as $entity) { | |
$dtos[] = $this->mapEntityToDto($entity, $resourceClass); | |
} | |
// ... lines 37 - 43 | |
} | |
// ... lines 45 - 51 | |
return $this->mapEntityToDto($entity, $resourceClass); | |
} | |
private function mapEntityToDto(object $entity, string $resourceClass): object | |
{ | |
return $this->microMapper->map($entity, $resourceClass); | |
} | |
} |
Making the Processor Generic
We're on a roll! Head to the processor and search for "user". Ah, we have the same problem except, this time, we need the User
entity class.
Ok! Up on top, dd($operation)
. And for this, we need to run one of our tests:
// ... lines 1 - 14 | |
class EntityClassDtoStateProcessor implements ProcessorInterface | |
{ | |
// ... lines 17 - 25 | |
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []) | |
{ | |
dd($operation); | |
// ... lines 29 - 43 | |
} | |
// ... lines 45 - 49 | |
} |
symfony php bin/phpunit --filter=testPostToCreateUser
And... got it! We see the Post
operation... and the class is, of course, UserApi
. But this time we need the User
class. Remember: in UserApi
, we use stateOptions
to say that UserApi
is tied to the User
entity. And now, we can read this info from the operation. If we scroll down a bit... there it is: the stateOptions
property with the Options
object, and entityClass
inside.
Cool! Back in the processor, towards the top... remove the dd()
and start with $stateOptions = $operation->getStateOptions()
. Then, to help my editor (and also in case I misconfigure something), assert($stateOptions instanceof Options)
(the one from Doctrine ORM).
You can use different Options
classes for $stateOptions
... like if you're getting data from ElasticSearch, but we know we're using this one from Doctrine. Below, say $entityClass = $stateOptions->getEntityClass()
.
// ... lines 1 - 15 | |
class EntityClassDtoStateProcessor implements ProcessorInterface | |
{ | |
// ... lines 18 - 26 | |
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []) | |
{ | |
$stateOptions = $operation->getStateOptions(); | |
assert($stateOptions instanceof Options); | |
$entityClass = $stateOptions->getEntityClass(); | |
// ... lines 32 - 46 | |
} | |
// ... lines 48 - 52 | |
} |
And... we don't need this assert()
down here, then pass $entityClass
to mapDtoToEntity()
. Finally, use that with string $entityClass
... and also pass it here.
// ... lines 1 - 15 | |
class EntityClassDtoStateProcessor implements ProcessorInterface | |
{ | |
// ... lines 18 - 26 | |
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []) | |
{ | |
$stateOptions = $operation->getStateOptions(); | |
assert($stateOptions instanceof Options); | |
$entityClass = $stateOptions->getEntityClass(); | |
// ... lines 32 - 46 | |
} | |
// ... lines 48 - 52 | |
} |
When we search for "user" now... we can get rid of the two use
statements... and... we're clean! It's generic! Try the test!
symfony php bin/phpunit --filter=testPostToCreateUser
That's it! We're ready! We have a reusable provider and processor! Next, let's create a DragonTreasureApi
class, repeat this magic, and see how quickly we can get things to fall into place!
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!