Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Lucky you! You found an early release chapter - it will be fully polished and published shortly!

Allow Admin Users to Edit any Treasure

This Chapter isn't
quite ready...

Rest assured, the gnomes are hard at work
completing this video!

Browse Tutorials

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.

Foundry State Methods

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!

Writing the Test

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.

Allowing Admins to Edit Anything

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.

Leave a comment!

Login or Register to join the conversation
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// 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