Lucky you! You found an early release chapter - it will be fully polished and published shortly!
Rest assured, the gnomes are hard at work
completing this video!
Every DragonTreasure
must have an owner
... and to set that, when you POST
to create a treasure, we require that field. I think we should make that
optional. So, in the test, stop sending the owner
field. When this happens,
let's automatically set it to the currently-authenticated user.
Make sure the test fails. Copy the method name... and run it:
symfony php bin/phpunit --filter=testPostToCreateTreasure
Nailed it. Got a 422, 201 expected. That 422 is a validation error from the owner
property: this value should not be null.
If we're going to make it optional, we need to remove that Assert\NotNull
.
And now when we try the test:
symfony php bin/phpunit --filter=testPostToCreateTreasure
Well hello there gorgeous 500 error! Probably it's because the null owner_id
is
going kaboom when it hits the database. Yup!
So: how can we automatically set this field when it's not sent? In the previous API Platform 2 tutorial, I did this with an entity listener, which is a fine solution. But in API Platform 3, just like when we hashed the user password, there's now a really nice system for this: the state processor system.
As a reminder, our POST and PATCH endpoints for DragonTreasure
already have a state
processor that comes from Doctrine: it's responsible for saving the object to
the database. Our goal will feel familiar at this point: to decorate that state
process so we can run extra code before saving.
Like before, start by running:
php bin/console make:state-processor
Call it DragonTreasureSetOwnerProcessor
.
Over in src/State/
, open that up. Ok, let's decorate! Add the construct method
with private ProcessorInterface $innerProcessor
.
Then down in process()
, call that! This method doesn't return anything - it has
a void
return - so we just need $this->innerProcessor
- don't forget that
part like I am - ->process()
passing $data
, $operation
, $uriVariables
and
$context
.
Now, to make Symfony use our state processor instead of the normal one from
Doctrine, add #[AsDecorator]
... and the id of the service is
api_platform.doctrine.orm.state.persist_processor
.
Cool! Now, everything that uses that service in the system will be passed our service instead... and then the original will be passed into us.
Oh, and there's something cool going on. Look at UserHashPasswordStateProcessor
.
We're decorating the same thing there! Yea, we're decorating that service
twice, which is totally allowed! Internally, this will create a, sort
of, chain of decorated services.
Ok, let's get to work setting the owner. Autowire our favorite Security
service
so we can figure out who is logged in. Then, before we do the saving, if $data
is an instanceof DragonTreasure
and $data->getOwner()
is null and
$this->security->getUser()
- making sure the user is logged in - then
$data->setOwner($this->security->getUser())
.
That should do it! Run that test:
symfony php bin/phpunit --filter=testPostToCreateTreasure
Yikes! Allowed memory size exhausted. I smell recursion! Because... I'm calling
process()
on myself: I need $this->innerProcessor->process()
. Now:
symfony php bin/phpunit --filter=testPostToCreateTreasure
A passing test is so much cooler than recursion. And the owner field is now optional!
Next: we currently return all treasures from our GET collection endpoint, including unpublished treasures. Let's fix that by modifying the query behind that endpoint to hide them.
Hey @Rsteuber!
I JUST recorded the audio today - it'll probably be a week or two before the video is out, but you refresh, you'll see the final "script" for this chapter (though, code blocks will come later). Also, if you download the code, what we do in this chapter is already in there. I hope that helps!
Cheers!
Hmm, it seems I can't download the source code yet. Guess i have to wait when it is released.
Cheers :)
Hey @Rsteuber
Yea, the download button it's disabled for non-published chapters, but if you only want to download the course code, you can go to any other chapter and download it there
Cheers!
// composer.json
{
"require": {
"php": ">=8.1",
"ext-ctype": "*",
"ext-iconv": "*",
"api-platform/core": "^3.0", // v3.1.2
"doctrine/annotations": "^2.0", // 2.0.1
"doctrine/doctrine-bundle": "^2.8", // 2.8.3
"doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.2
"doctrine/orm": "^2.14", // 2.14.1
"nelmio/cors-bundle": "^2.2", // 2.2.0
"nesbot/carbon": "^2.64", // 2.66.0
"phpdocumentor/reflection-docblock": "^5.3", // 5.3.0
"phpstan/phpdoc-parser": "^1.15", // 1.16.1
"symfony/asset": "6.2.*", // v6.2.5
"symfony/console": "6.2.*", // v6.2.5
"symfony/dotenv": "6.2.*", // v6.2.5
"symfony/expression-language": "6.2.*", // v6.2.5
"symfony/flex": "^2", // v2.2.4
"symfony/framework-bundle": "6.2.*", // v6.2.5
"symfony/property-access": "6.2.*", // v6.2.5
"symfony/property-info": "6.2.*", // v6.2.5
"symfony/runtime": "6.2.*", // v6.2.5
"symfony/security-bundle": "6.2.*", // v6.2.6
"symfony/serializer": "6.2.*", // v6.2.5
"symfony/twig-bundle": "6.2.*", // v6.2.5
"symfony/ux-react": "^2.6", // v2.7.1
"symfony/ux-vue": "^2.7", // v2.7.1
"symfony/validator": "6.2.*", // v6.2.5
"symfony/webpack-encore-bundle": "^1.16", // v1.16.1
"symfony/yaml": "6.2.*" // v6.2.5
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.2
"mtdowling/jmespath.php": "^2.6", // 2.6.1
"phpunit/phpunit": "^9.5", // 9.6.3
"symfony/browser-kit": "6.2.*", // v6.2.5
"symfony/css-selector": "6.2.*", // v6.2.5
"symfony/debug-bundle": "6.2.*", // v6.2.5
"symfony/maker-bundle": "^1.48", // v1.48.0
"symfony/monolog-bundle": "^3.0", // v3.8.0
"symfony/phpunit-bridge": "^6.2", // v6.2.5
"symfony/stopwatch": "6.2.*", // v6.2.5
"symfony/web-profiler-bundle": "6.2.*", // v6.2.5
"zenstruck/browser": "^1.2", // v1.2.0
"zenstruck/foundry": "^1.26" // v1.28.0
}
}
When will this finally be ready?