Buy Access to Course
03.

Decorating the Core State Provider

|

Share this awesome video!

|

To populate the non-persisted property on our entity, we'll leverage a custom state provider. Create one with:

php bin/console make:state-provider

Let's dub it DragonTreasureStateProvider.

Spin over and open this up in src/State/. Ok, it implements a ProviderInterface which requires one method: provide(). Our job is to return the DragonTreasure object for the current API request - which is a Patch request in our test.

15 lines | src/State/DragonTreasureStateProvider.php
// ... lines 1 - 7
class DragonTreasureStateProvider implements ProviderInterface
{
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
{
// Retrieve the state from somewhere
}
}

Before we think about doing that, dd($operation) so we can see if this is executed. When we try the test... the answer is that it is not called. We get the same error as before.

15 lines | src/State/DragonTreasureStateProvider.php
// ... lines 1 - 9
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
{
dd($operation);
}
// ... lines 14 - 15

So, creating a state provider and implementing ProviderInterface is not enough to make our class be used. And this is great! We get to control this on an resource-by-resource basis... or even on an operation-by-operation basis.

In DragonTreasure, way up on top, inside the ApiResource attribute, add provider then the service ID, which is the class in our case: DragonTreasureStateProvider::class.

275 lines | src/Entity/DragonTreasure.php
// ... lines 1 - 19
use App\State\DragonTreasureStateProvider;
// ... lines 21 - 30
#[ApiResource(
// ... lines 32 - 64
provider: DragonTreasureStateProvider::class,
// ... lines 66 - 68
)]
// ... lines 70 - 90
class DragonTreasure
// ... lines 92 - 275

So now, whenever API Platform needs to "load" a dragon treasure, it will call our provider. And our test is a perfect example. When we make a PATCH request, the first thing API Platform will do is ask the state provider to load this treasure. Then it will update it using the JSON.

Watch, when we run the test now:

symfony php bin/phpunit --filter=testOwnerCanSeeIsPublishedAndIsMineFields

We hit the dump!

Decorating the Provider

But... I don't want to do all the work of querying the database for the dragon treasures... because there's already a core entity provider that does all that! So let's use it!

Add a constructor... oh and I'll keep that dd() for now. Add a private ProviderInterface $itemProvider argument.

19 lines | src/State/DragonTreasureStateProvider.php
// ... lines 1 - 5
use ApiPlatform\State\ProviderInterface;
// ... line 7
class DragonTreasureStateProvider implements ProviderInterface
{
public function __construct(private ProviderInterface $itemProvider)
{
}
// ... lines 13 - 17
}

As a reminder: the Get one, Patch, Put and Delete operations all use the ItemProvider, which knows to query for a single item. Since our test uses Patch, we're going to focus on using that provider first.

If we run the test now, it fails. The error is:

Cannot autowire service DragonTreasureStateProvider: argument itemProvider references ProviderInterface, but no such service exists.

Often in Symfony, if we type-hint an interface, Symfony will pass us what we need. But in the case of ProviderInterface, there are multiple services that implement this - including the core ItemProvider and CollectionProvider.

This means that we need to tell Symfony which we want. Do that with the handy-dandy #[Autowire] attribute with service set to ItemProvider::class - make sure to get the one from ORM.

23 lines | src/State/DragonTreasureStateProvider.php
// ... lines 1 - 7
use Symfony\Component\DependencyInjection\Attribute\Autowire;
// ... line 9
class DragonTreasureStateProvider implements ProviderInterface
{
public function __construct(
#[Autowire(service: ItemProvider::class)] private ProviderInterface $itemProvider
)
{
}
// ... lines 17 - 21
}

And yup! That is a valid service id. There is also a harder-to-remember service id, but API Platform provides a service alias so that we can just use this. Lovely!

Ok, go test go! Yes! We hit the dump which means that the item provider was injected. So now, we're dangerous. $treasure equals $this->itemProvider->provide() passing the 3 args.

32 lines | src/State/DragonTreasureStateProvider.php
// ... lines 1 - 18
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
{
$treasure = $this->itemProvider->provide($operation, $uriVariables, $context);
// ... lines 22 - 29
}
// ... lines 31 - 32

At this point, $treasure will be null or a valuable DragonTreasure object. If it is not a DragonTreasure instance, return null.

But if we do have a treasure, we're in business! Call setIsOwnedByAuthenticatedUser() and hardcode true for now. Then return $treasure.

32 lines | src/State/DragonTreasureStateProvider.php
// ... lines 1 - 18
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
{
// ... lines 21 - 22
if (!$treasure instanceof DragonTreasure) {
return $treasure;
}
$treasure->setIsOwnedByAuthenticatedUser(true);
return $treasure;
}
// ... lines 31 - 32

Ok, go test go!

symfony php bin/phpunit --filter=testOwnerCanSeeIsPublishedAndIsMineFields

Shazam! We're green! So let's go set that value for real. This is easy enough: add a private Security argument... and make sure you first arg has a comma.

Then this is true if $this->security->getUser() equals $treasure->getOwner().

34 lines | src/State/DragonTreasureStateProvider.php
// ... lines 1 - 11
class DragonTreasureStateProvider implements ProviderInterface
{
public function __construct(
// ... line 15
private Security $security,
)
{
}
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
{
// ... lines 23 - 28
$treasure->setIsOwnedByAuthenticatedUser($this->security->getUser() === $treasure->getOwner());
return $treasure;
}
}

And... then... the test still passes. Custom field accomplished! And, most importantly, it is documented inside our API.

However, we did just break our GetCollection endpoint. Let's fix that next.