If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeWe need to centralize this logic so that it can be reused in other places:
... lines 1 - 11 | |
class ArticleAdminController extends AbstractController | |
{ | |
... lines 14 - 31 | |
public function edit(Article $article) | |
{ | |
if ($article->getAuthor() != $this->getUser() && !$this->isGranted('ROLE_ADMIN_ARTICLE')) { | |
throw $this->createAccessDeniedException('No access!'); | |
} | |
... lines 37 - 38 | |
} | |
} |
How? Well... it may look a bit weird at first. Remove all of this logic and replace it with: if (!$this->isGranted('MANAGE', $article))
:
... lines 1 - 11 | |
class ArticleAdminController extends AbstractController | |
{ | |
... lines 14 - 31 | |
public function edit(Article $article) | |
{ | |
if (!$this->isGranted('MANAGE', $article)) { | |
... line 35 | |
} | |
... lines 37 - 38 | |
} | |
} |
Hmm. I'm using the same isGranted()
function as before. But instead of passing a role, I'm just "inventing" a string: MANAGE
. It also turns out that isGranted()
has an optional second argument: a piece of data that is relevant to making this access decision.
Don't worry - this will not magically work somehow. If you try it... yep!
Access denied.
Let me explain what's happening. Whenever you call isGranted
, or one of the other functions like denyAccessUnlessGranted()
, Symfony executes what's known as the "Voter system". Basically, it takes the string - MANAGE
, or ROLE_ADMIN_ARTICLE
- and it asks each voter:
Hey voter! Do you know how to decide whether or not the current user has this string -
ROLE_ADMIN_ARTICLE
orMANAGE
?
In the core of Symfony, there are basically two voters by default: RoleVoter
and AuthenticatedVoter
. When you pass any string that starts with ROLE_
, the RoleVoter
says:
Ah, yea! I totally know how to determine if the user should have access!
Then, it checks to see if the User
has that role and returns true
or false
. The other voter "abstains" - which means it doesn't vote - and so access is entirely granted or denied by that one voter.
When you pass any string that starts with IS_AUTHENTICATED_
, like IS_AUTHENTICATED_FULLY
, the other voters says:
Oh. This is me! I know how to check this!
And it returns true
or false
based on how authenticated the user is and which of those three IS_AUTHENTICATED_
strings we passed.
The really cool thing is that we can add our own custom voters. Right now, when we call isGranted()
with the string MANAGE
, both voters say:
Hmm, no, we don't understand what this is
They both "abstain" from voting. And when nobody votes, access is denied by default. So our goal is clear: introduce a new voter that understands how to handle the string MANAGE
and an Article
object. By the way, up until now, I've been calling this MANAGE
string a role... because it has usually started with ROLE_
. But actually, it's generally called a "permission attribute". Some permission attributes are roles, but some are other strings handled by other voters.
Oh, and why did I choose the word MANAGE
? I just made that up. If you need different permissions for edit, show and delete, you would use different attributes for each - like EDIT
, SHOW
, DELETE
- and create a voter that can handle all of those. You'll see soon. My case is simpler: I'll use MANAGE
for any operation on an Article - for example, for editing, deleting or publishing it.
Ok, let's finally create our voter!
"Houston: no signs of life"
Start the conversation!
// composer.json
{
"require": {
"php": "^7.1.3",
"ext-iconv": "*",
"composer/package-versions-deprecated": "^1.11", // 1.11.99
"knplabs/knp-markdown-bundle": "^1.7", // 1.7.0
"knplabs/knp-paginator-bundle": "^2.7", // v2.8.0
"knplabs/knp-time-bundle": "^1.8", // 1.8.0
"nexylan/slack-bundle": "^2.0,<2.2.0", // v2.0.0
"php-http/guzzle6-adapter": "^1.1", // v1.1.1
"sensio/framework-extra-bundle": "^5.1", // v5.2.0
"stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
"symfony/asset": "^4.0", // v4.1.4
"symfony/console": "^4.0", // v4.1.4
"symfony/flex": "^1.0", // v1.17.6
"symfony/framework-bundle": "^4.0", // v4.1.4
"symfony/lts": "^4@dev", // dev-master
"symfony/orm-pack": "^1.0", // v1.0.6
"symfony/security-bundle": "^4.0", // v4.1.4
"symfony/serializer-pack": "^1.0", // v1.0.1
"symfony/twig-bundle": "^4.0", // v4.1.4
"symfony/web-server-bundle": "^4.0", // v4.1.4
"symfony/yaml": "^4.0", // v4.1.4
"twig/extensions": "^1.5" // v1.5.2
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.0", // 3.0.2
"easycorp/easy-log-handler": "^1.0.2", // v1.0.7
"fzaninotto/faker": "^1.7", // v1.8.0
"symfony/debug-bundle": "^3.3|^4.0", // v4.1.4
"symfony/dotenv": "^4.0", // v4.1.4
"symfony/maker-bundle": "^1.0", // v1.7.0
"symfony/monolog-bundle": "^3.0", // v3.3.0
"symfony/phpunit-bridge": "^3.3|^4.0", // v4.1.4
"symfony/profiler-pack": "^1.0", // v1.0.3
"symfony/var-dumper": "^3.3|^4.0" // v4.1.4
}
}