Buy Access to Course
19.

Entity -> DTO Item State Provider

|

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

What 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!

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
}

Below, this will be our item provider. dd($uriVariables).

57 lines | src/State/EntityToDtoStateProvider.php
// ... 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.

65 lines | src/State/EntityToDtoStateProvider.php
// ... 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.

65 lines | src/State/EntityToDtoStateProvider.php
// ... 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).

65 lines | src/State/EntityToDtoStateProvider.php
// ... 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().

65 lines | src/State/EntityToDtoStateProvider.php
// ... 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.