Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Entity & Field Permissions

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Most of the time, securing your admin will probably mean denying access to entire sections or specific actions based on a role. But we can go a lot further.

Hiding a Field for some Admins

Let's imagine that, for some reason, the number of votes is a sensitive number that should only be displayed and modified by super admins. Head over to QuestionCrudController. This is something we can control on each field, so find VotesField. Here it is. Add ->setPermission() and then pass ROLE_SUPER_ADMIN.

... lines 1 - 18
class QuestionCrudController extends AbstractCrudController
... lines 21 - 45
public function configureFields(string $pageName): iterable
... lines 48 - 70
yield VotesField::new('votes', 'Total Votes')
... line 72
... lines 74 - 91

I'm currently logged in as "moderatoradmin", so I'm not a super admin. And so, when I refresh, it's as simple as that! The votes field disappears, both on the list page and on the edit page. Super cool!

Hiding some Results for some Admins

Ok, let's try something different. What if we want to show only some items in an admin section based on the user? Maybe, for some reason, my user can only see certain questions.

Or, here's a better example. I'm currently logged in as a moderator, whose job is to approve questions. If we click the Users section, a moderator probably shouldn't be able to see and edit other user accounts. We could hide the section entirely for moderators, or we could add some security so that only their own user account is visible to them. This is called "entity permissions". It answers the question of whether or not to show a specific row in an admin section based on the current user. And we control this on the CRUD level: we set an entity permission for an entire CRUD section.

Head over to UserCrudController and, at the bottom, override the configureCrud() method. And now, for this entire CRUD, we can say ->setEntityPermission() and pass ADMIN_USER_EDIT.

... lines 1 - 5
use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
... lines 7 - 17
class UserCrudController extends AbstractCrudController
... lines 20 - 24
public function configureCrud(Crud $crud): Crud
return parent::configureCrud($crud)
... lines 30 - 63

Notice this is not a role. EasyAdmin calls the security system for each entity that it's about to display and passes this ADMIN_USER_EDIT string into the security system. If we used a role here - like ROLE_SUPER_ADMIN - that would return true or false for every item. It would end up showing either all the items or none of them.

Nope, a role won't work here. So, instead, I'm passing this ADMIN_USER_EDIT string, which is something I totally just invented. In a few minutes, we're going to create a custom voter to handle that.

But since we haven't created that voter yet, this will return false in the security system in all cases. In other words, if this is working correctly, we won't see any items in this list.

Entity Permissions and formatValue()

Let's try it! Refresh and... okay. We don't see any items in the list, but it's because we have a gigantic error. It's coming from UserCrudController: the formatValue() callback on the avatar field:

Argument #2 ($user) must be of type App\Entity\User, null given

This error originates in a configurator. Go look at that field. Let's see... avatar... here it is. You might remember that formatValue() is the way we control how a value is rendered on the index and detail pages. And it's simple: it passes us the current User object - since we're in the UserCrudController and rendering users - and then we return whatever value we want.

But, when you use entity permissions, it's possible that this User object will be null because this is a row that won't be displayed. I'm not sure exactly why EasyAdmin calls our callback... even though the row is about to be hidden, but it does. So it means that we need to allow this to be null. I'll add a question mark to make it nullable.

And then, because we're using PHP 8, we can be super trendy by using a new syntax: $user?->getAvatarUrl(). That says that if there is a user, call ->getAvatarUrl() and return it. Else, just return null.

... lines 1 - 30
public function configureFields(string $pageName): iterable
... lines 33 - 34
yield AvatarField::new('avatar')
->formatValue(static function ($value, ?User $user) {
return $user?->getAvatarUrl();
... lines 39 - 62
... lines 64 - 65

There's one other place that we need to do this. It's in QuestionCrudController, down here on the askedBy field. Add a question mark, and then another question mark right in the middle of $question?->getAskedBy().

... lines 1 - 18
class QuestionCrudController extends AbstractCrudController
... lines 21 - 45
public function configureFields(string $pageName): iterable
... lines 48 - 73
yield AssociationField::new('askedBy')
... line 75
->formatValue(static function ($value, ?Question $question): ?string {
if (!$user = $question?->getAskedBy()) {
return null;
... lines 80 - 81
... lines 83 - 91

Go refresh again and... beautiful! No results are showing, and we get this nice message:

Some results can't be displayed because you don't have enough permissions.

Woo! And of course, if we tried to search for something, that would also take into account our permissions.

Next, let's create the voter so that we can deny access exactly when we want to and ultimately show only our user record when a moderator is in the Users section.

Leave a comment!

Login or Register to join the conversation
Christophe R. Avatar
Christophe R. Avatar Christophe R. | posted 1 year ago | edited

What is the best way to define field permission with different ROLES for read and write?

My understand is to duplicate the Field but there is some duplicate codes with the help() for example.
Here is my code for now but I am not sure:

yield TextField::new('name', 'Nom')->setColumns(6)

        ->setHelp('Nom pour reconnaître rapidement à quoi sert la variable');

yield TextField::new('name', 'Nom')->setColumns(6)

        ->setHelp('Nom pour reconnaître rapidement à quoi sert la variable');


The goal is to have readonly the name for ROLE_ADMIN.
with this code, the ROLE_ADMIN can not edit the name, he can see on index or details but when editing, it is not there but I would like to display the content to indiecate the name of the value he have to write.

One trick is to set this in the configureCrud()
->setEntityLabelInSingular(fn (?Content $content, ?string $pageName) => $content ? $content->getName() : 'Content')


Hey Christophe R. !

Hmm, this is a good question. I believe you have the correct approach. The big downside is duplication... and, while I can think of a solution to that... I can't think of a GREAT solution. My first thought was to setup the main properties of the field and set to a variable. Then "clone" that, modify it for the form/view, and yield it. Unfortunately, I don't think this works because clone is "shallow", and the underlying data for the TextField is stored in a DTO (in other words, cloning will still mean that you're modifying the same underlying DTO... so it's not really a clone).

So what about this:

$nameField = function() {
    return TextField::new('name', 'Nom')
        ->setHelp('Nom pour reconnaître rapidement à quoi sert la variable');

yield $nameField()

yield $nameField()

Effectively, we create our own "clone"... which is really a function that creates a new object each time. What do you think?


1 Reply
Cat in space

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

What PHP libraries does this tutorial use?

// composer.json
    "require": {
        "php": ">=8.1.0",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", //
        "doctrine/doctrine-bundle": "^2.1", // 2.5.5
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.2.1
        "doctrine/orm": "^2.7", // 2.10.4
        "easycorp/easyadmin-bundle": "^4.0", // v4.0.2
        "handcraftedinthealps/goodby-csv": "^1.4", // 1.4.0
        "knplabs/knp-markdown-bundle": "dev-symfony6", // dev-symfony6
        "knplabs/knp-time-bundle": "^1.11", // 1.17.0
        "sensio/framework-extra-bundle": "^6.0", // v6.2.5
        "stof/doctrine-extensions-bundle": "^1.4", // v1.7.0
        "symfony/asset": "6.0.*", // v6.0.1
        "symfony/console": "6.0.*", // v6.0.2
        "symfony/dotenv": "6.0.*", // v6.0.2
        "symfony/flex": "^2.0.0", // v2.0.1
        "symfony/framework-bundle": "6.0.*", // v6.0.2
        "symfony/mime": "6.0.*", // v6.0.2
        "symfony/monolog-bundle": "^3.0", // v3.7.1
        "symfony/runtime": "6.0.*", // v6.0.0
        "symfony/security-bundle": "6.0.*", // v6.0.2
        "symfony/stopwatch": "6.0.*", // v6.0.0
        "symfony/twig-bundle": "6.0.*", // v6.0.1
        "symfony/ux-chartjs": "^2.0", // v2.0.1
        "symfony/webpack-encore-bundle": "^1.7", // v1.13.2
        "symfony/yaml": "6.0.*", // v6.0.2
        "twig/extra-bundle": "^2.12|^3.0", // v3.3.7
        "twig/twig": "^2.12|^3.0" // v3.3.7
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.3", // 3.4.1
        "symfony/debug-bundle": "6.0.*", // v6.0.2
        "symfony/maker-bundle": "^1.15", // v1.36.4
        "symfony/var-dumper": "6.0.*", // v6.0.2
        "symfony/web-profiler-bundle": "6.0.*", // v6.0.2
        "zenstruck/foundry": "^1.1" // v1.16.0