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!
We control which fields are readable and writable via serialization groups. But what if you have a field that should be included in the API... but only for certain users? Sadly, groups can't pull off that kind of magic on their own.
For example, find the isPublished
field and let's make this part of our API by
adding the treasure:read
and treasure:write
groups.
Now if we spin over and try the tests:
symfony php bin/phpunit
This makes one test fail: testGetCollectionOfTreasures
sees that isPublished
is being returned... and it's not expecting it.
Here's the plan: we'll sneak the field into our API but only for admin
users or owners of this DragonTreasure
. How can we pull that off?
Well, surprise! We don't often need it, but we can add an ApiProperty
attribute
above any property to help further configure it. It has a bunch of
stuff, like a description that helps with your documentation
and many edge-case things. There's even one called readable
. If we said
readable: false
... then the serialization groups would say that this should
be included in the response... but then this would override that. Watch: if we try
the tests:
symfony php bin/phpunit
They pass because the field is gone.
For our mission, we can leverage a super cool option called security
. Set it
to is_granted("ROLE_ADMIN")
.
That's it! If this expression return false, isPublished
will not be included
in the API: it won't be readable or writable.
And when we run the tests now:
symfony php bin/phpunit
They still pass, which means isPublished
is not being returned.
Now let's go test the "happy" path where this field is returned. Pop open
DragonTreasureResourceTest
. Here's the original test: testGetCollectionOfTreasures
.
We're anonymous, so isPublished
isn't returned.
Now scroll down to testAdminCanPatchToEditTreasure
. When we create the
DragonTreasure
, let's make sure it always starts with isPublished
false.
Then, down here, assertJsonMatches('isPublished', false)
to test that the
field is returned.
Copy the test name, spin over and add --filter
to run just that test:
symfony php bin/phpunit --filter=testAdminCanPatchToEditTreasure
And... it passes! The field is returned when we're an admin.
What about if we're the owner of the treasure? Copy the test... rename it
to testOwnerCanSeeIsPublishedField()
... and let's tweak a few things.
Rename $admin
to $user
, simplify this to
DragonTreasureFactory::createOne()
and make sure the owner
is set to our new
$user
.
We could change this to a GET request... but PATCH is fine. In either situation,
we want to make sure the isPublished
field is returned.
Since we haven't implemented this yet... let's make sure it fails. Copy the method name and try it:
symfony php bin/phpunit --filter=testOwnerCanSeeIsPublishedField
Failure achieved! And we know how to solve this! On the security
option,
we could inline the logic with or object.getOwner() === user
. But remember:
we created the voter so that we don't need to do crazy stuff like that! Instead,
say is_granted()
, EDIT
then object
.
Try the test now:
symfony php bin/phpunit --filter=testOwnerCanSeeIsPublishedField
Got it! Oh, and I haven't used it much, but there's also a securityPostDenormalize
option. Just like with the securityPostDenormalize
option on each operation, this
runs after the new data is deserialized onto the object. What's interesting is
that if the expression returns false
, the data on the object is actually reverted.
For example, suppose the isPublished
property started as false
and then the user
sent some JSON to change it to true
. But then, securityPostDenormalize
returned
false
. In that case, API Platform will revert the isPublished
property back
to its original value: it will change it from false
back to true
. Oh, and
by the way, securityPostDenormalize
is not executed on GET
requests: it
only happens when data is being deserialized. So be sure to put your main security
logic in security
and only use securityPostDenormalize
if you need it.
Up next on our to-do list: let's level-up our user operations to hash the password before saving to the database. We'll need a fresh, non-persisted plain password property to make it happen.
"Houston: no signs of life"
Start the conversation!
// 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
}
}