Entity -> DTO Item State Provider
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 SubscribeWhat about the item endpoint? If we go to /api/users/6.jsonld... it looks like it works... but it's a trap! It's just the collection format... with a single item!
We know that there are two core providers: CollectionProvider and an item provider, whose job is to return one item or null. Because we set provider to EntityToDtoStateProvider, it's using this one provider for every operation. And that's ok... as long as we make it smart enough to handle both cases.
We saw how to do this earlier: $operation is the key. Add if ($operation instanceof CollectionOperationInterface). Now we can warp all of this code up here. Lovely!
| // ... 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 | |
| } |
Below, this will be our item provider. dd($uriVariables).
| // ... lines 1 - 4 | |
| use ApiPlatform\Metadata\CollectionOperationInterface; | |
| // ... lines 6 - 13 | |
| class EntityToDtoStateProvider implements ProviderInterface | |
| { | |
| // ... lines 16 - 22 | |
| public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null | |
| { | |
| if ($operation instanceof CollectionOperationInterface) { | |
| $entities = $this->collectionProvider->provide($operation, $uriVariables, $context); | |
| assert($entities instanceof Paginator); | |
| $dtos = []; | |
| foreach ($entities as $entity) { | |
| $dtos[] = $this->mapEntityToDto($entity); | |
| } | |
| return new TraversablePaginator( | |
| new \ArrayIterator($dtos), | |
| $entities->getCurrentPage(), | |
| $entities->getItemsPerPage(), | |
| $entities->getTotalItems() | |
| ); | |
| } | |
| dd($uriVariables); | |
| } | |
| // ... lines 44 - 55 | |
| } |
Calling the Core Item Provider
When we try the item operation... nice! That's what we expect to see: the id value, which is the dynamic part of the route.
Just like with the collection provider, we do not want to do the querying work manually. Instead, we'll... "delegate" it the core Doctrine item provider. Add a second argument... we can just copy the first... type-hinted with ItemProvider (the one from Doctrine ORM), and called $itemProvider.
| // ... lines 1 - 4 | |
| use ApiPlatform\Doctrine\Orm\State\ItemProvider; | |
| // ... lines 6 - 14 | |
| class EntityToDtoStateProvider implements ProviderInterface | |
| { | |
| public function __construct( | |
| #[Autowire(service: CollectionProvider::class)] private ProviderInterface $collectionProvider, | |
| #[Autowire(service: ItemProvider::class)] private ProviderInterface $itemProvider, | |
| ) | |
| { | |
| } | |
| // ... lines 24 - 63 | |
| } |
I like it! Back below, let it do the work with $entity = $this->itemProvider->provide() passing $operation, $uriVariables and $context.
| // ... lines 1 - 14 | |
| class EntityToDtoStateProvider implements ProviderInterface | |
| { | |
| // ... lines 17 - 24 | |
| public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null | |
| { | |
| // ... lines 27 - 43 | |
| $entity = $this->itemProvider->provide($operation, $uriVariables, $context); | |
| // ... lines 45 - 50 | |
| } | |
| // ... lines 52 - 63 | |
| } |
This will give us an $entity object or null. If we don't have an $entity object, return null. That will trigger a 404. But if we do have an $entity object, we don't want to return that directly. Remember, the whole point of this class is to take the $entity object and transform it into a UserApi DTO.
So instead, return $this->mapEntityToDto($entity).
| // ... lines 1 - 14 | |
| class EntityToDtoStateProvider implements ProviderInterface | |
| { | |
| // ... lines 17 - 24 | |
| public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null | |
| { | |
| // ... lines 27 - 43 | |
| $entity = $this->itemProvider->provide($operation, $uriVariables, $context); | |
| if (!$entity) { | |
| return null; | |
| } | |
| return $this->mapEntityToDto($entity); | |
| } | |
| // ... lines 52 - 63 | |
| } |
That feels good. And... the endpoint works beautifully. If we try an invalid id, our provider returns null and API Platform takes care of the 404.
Only Showing Published Dragon Treasures
Side note: if you follow some of these related treasures, they may 404 as well. Let's see... we have 21 and 27. 21 works for me... and for 27... that also works... of course. Anyway, the reason some might 404 is that, right now, if I go back, the dragonTreasures property includes all the treasures related to this user: even the unpublished ones. But in a previous tutorial, we created a query extension that prevented unpublished treasures from being loaded.
Back when the User entity was our API resource, we avoided returning unpublished treasures from this property. We created getPublishedDragonTreasures() and made that the dragonTreasures property.
But in our state provider, we're setting all of them. This is an easy fix: change to getPublishedDragonTreasures().
| // ... lines 1 - 14 | |
| class EntityToDtoStateProvider implements ProviderInterface | |
| { | |
| // ... lines 17 - 52 | |
| private function mapEntityToDto(object $entity): object | |
| { | |
| // ... lines 55 - 58 | |
| $dto->dragonTreasures = $entity->getPublishedDragonTreasures()->getValues(); | |
| // ... lines 60 - 62 | |
| } | |
| } |
Actually, undo that... then refresh the collection endpoint. Ok, we see treasures 16 and 40 down here... then after using the new method... only 16! "40" is unpublished.
That was easy! And it highlights something cool. In order to have a dragonTreasures field that returned something special when our User entity was an ApiResource, we needed a dedicated method and a SerializedName attribute. But with a custom class, we don't need any weirdness. We can do whatever we want in the state provider. Our classes stay shiny and clean!
Next: Let's get our users saving with a state processor: a delicate dance that involves handling new and existing users.
8 Comments
Hey there,
Everything works great, but any idea why I have this weird schema?
When I run the finish project I see you have something like:
Would be nice to see a IRI example, as it's confusing now.
Guess it's a minor from stateOptions, #[ApiResource() on a Entity itself it works normal.
No idea if it's a deep bug or we are missing something, just wondering if you have a suggestion to get IRI in the schema?
Hey @Rudi-T!
Hmm, you literally have
path/실례.htmlin your schema? That appears to be... a Korean character? And the.htmlis also weird. I have no idea what would be causing this. The mechanism in API Platform is pretty simple for figuring out if an IRI will be generated:A) API Platform looks at the class that it is generated the schema for - e.g.
UserApiB) It looks at the
dragonTreasuresproperty and tries to figure out its type. It does this by looking at PHPDoc, the property type, etc. IF it determines thatdragonTreasuresis a class that has#[ApiResource]above it (or it's an array of this class), then it will generate a schema with IRI strings.So this looks SUPER bizarre to me...
Could it be possible there might be a bug in the latest version of API Platform ?
Since my last
composer updateon an already existing project (API Platform v3.2.3 and Symfony 6.3.7), I have the same thing happen too. IRIs are weirdly shown in Swagger doc, also withpath/실례.htmlinstead of the IRI.Looking at the two other messages in this conversation posted just a few days ago, it raises a legitimate concern.
Thank you very much.
Clément
Hello!!
I'm having the exact same issue!
It appears in every embedded relation
And sometimes it shows "../словник" instead of the embedded uri.
Wow, this is crazy! So, it's NEVER and correct IRI string? And you see the
path/실례.htmlsometimes? And other times../словник?So, looking at API Platform, the string
실례DOES show up in 1 place (thoughсловникdoes not show up anywhere): in aswagger-ui-bundle.jsfile shipped by API Platform.See https://github.com/api-platform/core/issues/5900#issuecomment-1773752880 - that looks like the same thing. So yes, I think this may be a bug or some sort of unexpected change.
I checked the GitHub issue and it seems to be the same thing indeed.
I changed the
swagger-ui-bundle.jsfile to an older version (April 23), and my previous issue is somewhat fixed but still not ideal (no actual IRI, just "string") :But it still gives a hint the issue may be from the
swagger-ui-bundle.jsfile.Another strange thing is that on version 3.2.2, my redocs are no showing the respective IRI, its showing "entity/1".
The "embedded" resource should show "/embedded/{id}", or something like this, but it is showing "/entity/1".
I've got the same strange string on the redocs for every embedded relation. In the Swagger UI, I have "path/실례.html" instead. So, in both cases I dont have the correct IRI.
Yes, I never get the correct IRI.
When using api-platform version 3.1, I got: "../словник" instead of the IRI,
when using api-platform version 3.2.2, I got: "path/실례.html".
I've tried other versions and had same bug in every embedded relation.
"Houston: no signs of life"
Start the conversation!