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've got things set up so that only the owner of a treasure can edit it. Now,
a new requirement has come down from on-high: admin users should be able to edit
any treasure. That means a user that has ROLE_ADMIN
.
To the test-mobile! Add a public function testAdminCanPatchToEditTreasure()
.
Then create an admin user with UserFactory::createOne()
passing roles set to
ROLE_ADMIN
.
That'll work fine. But if we need to create a lot of admin users in our tests,
we can add a shortcut to Foundry. Open UserFactory
. We're going to create something
called a "state" method. Anywhere inside, add a public function called, how about
withRoles()
that has an array $roles
argument and returns self
, which will
make this more convenient when we use it. Then
return $this->addState(['roles' => $roles])
.
Whatever we pass to addState()
becomes part of the data that will be used to
make this user.
To use the state method, the code changes to UserFactory::new()
. Instead of creating
a User
object, this instantiates a new UserFactory
... and then we can call
withRoles()
and pass ROLE_ADMIN
.
So, we're "crafting" what we want the user to look like. When we're done, call
create()
. createOne()
is a static shortcut method. But since we have an
instance of the factory, use create()
.
But we can go even further. Back in UserFactory
, add another state method called
asAdmin()
that returns self
. Inside return $this->withRoles(['ROLE_ADMIN'])
.
Thanks to that, we can simplify to UserFactory::new()->asAdmin()->create()
.
Nice!
Now let's get this test going. Create a new $treasure
set to
DragonTreasureFactory::createOne()
.
Because we're not passing an owner
, this will create a new User
in the background
and use that as the owner
. This means that our admin user will not be the
owner.
Now, $this->browser()->actingAs($adminUser)
then ->patch()
to
/api/treasures/
$treasure->getId()
, sending json
to update value
to the
same 12345
. ->assertStatus(200)
and assertJsonMatches()
value
, 12345
.
Cool! Copy the method name. Let's try it:
symfony php bin/phpunit --filter=testAdminCanPatchToEditTreasure
And... okay! We haven't implemented this yet, so it fails.
So, how do we allow admins to edit any treasure? Well, at first, it's relatively
easy because we have total control via the security
expression. So we can add
something like if is_granted("ROLE_ADMIN") OR
and then put parentheses around the
other use-case.
Let's make sure it works!
symfony php bin/phpunit --filter=testAdminCanPatchToEditTreasure
A 500 error! Let's see what's going on. Click to open this.
Unexpected token "name" around position 26.
So... that was an accident. Change OR
to or
. Then try the test again:
symfony php bin/phpunit --filter=testAdminCanPatchToEditTreasure
Got it! But my screw-up brings up a great point: the security
expression is
getting too complex. It's about as readable as a single-line PERL script... and
we do not want to make mistakes when it comes to security.
So next, let's centralize this logic with a voter.
"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
}
}