Decorating the Core State Provider
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.
// ... 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.
// ... 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
.
// ... lines 1 - 19 | |
use App\State\DragonTreasureStateProvider; | |
// ... lines 21 - 30 | |
( | |
// ... 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.
// ... 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
: argumentitemProvider
referencesProviderInterface
, 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
.
// ... 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.
// ... 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
.
// ... 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()
.
// ... 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.