Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

jrdavidson's avatar

Users and Roles

I guess I really need to take a step back and rethink how I'm handling users and roles. I was hoping I can get some help with a few ideas involving this. I've checked out a few such as Sentry but there's more in there than I need and want. Besides I'm more the type like I've heard @JeffreyWay mention in his videos that he prefers to write all his logic from scratch just because he can later debug it wen issues arrive or can understand every part of what is wrong and what it should and should not be doing.

Right now I have a user's table and a roles table. The user's table has a foreign key that is set up in the relationship with the roles table to be the primary key of that table in such a way that a user can have one role. So a user can only be a basic user or an admin, or a site owner and so on. I would like to figure out what I need to do to set up what these roles are allowed to handle.

I would also like to figure out what I could do in the event I have a user lets say in the basic user's group that normally aren't able to write posts but this particular user I can give a permission too. Obviously this sort of thing has happened before so I'm curious as to get so help on ideas.

0 likes
11 replies
jrdavidson's avatar

What should I do with the db structure of my application for the permissions part of this though?

themsaid's avatar

I'd consider having a structure like this:

User
--- user name
--- role_id

Role
--- role name

Capability
--- name
--- key

role_capability
--- role_id
--- capability_id

user_capability
--- user_id
--- capability_id

So to check if the user can do "delete_post", you might do

// User model

function can($capability)
{
    // if the capability exists in the users's role capabilities
    if( in_array($capability, $this->role->capabilities->lists('key')) )
    {
        return true;
    }

    // If the capability is assigned specially for this user
    if( in_array($capability, $this->capabilities->lists('key')) )
    {
        return true;
    }

    return false;
}
4 likes
jrdavidson's avatar

@themsaid That is a great reply. I wish this was something was more discussed in articles, tutorials, videos.

I'm curious to know if anyone has any issues with someone doing it in this method.

themsaid's avatar

There are many other ways to do this, however I find this more straight forward. Here's a tricky situation:

what if you want to give the user and admin role BUT remove his ability to delete_post?

To do that, you can add a column in the user_capability pivot table, let's say:

user_capability
--- user_id
--- capability_id
--- has

The has flag can be 0, or 1 with 0 indicating that this capability should be excluded, 1 one means this capability should be added.

So the can method can be something like:

function can($capability)
{
   $can = false;

    // if the capability exists in the users's role capabilities
    if( in_array($capability, $this->role->capabilities->lists('key')) )
    {
        $can  = true;
    }

    // If it's excluded from the user capabilities
    if( in_array($capability, $this->capabilities()->wherePivot('has', 0)->lists('key')) )
    {
        $can  = false;
    }
    // If not excluded so check if it's added
    elseif( in_array($capability, $this->capabilities()->wherePivot('has', 1)->lists('key')) )
    {
        $can  = true;
    }

    return $can;
}

Of course this code can be optimized, however I'm just showing how things can be like.

nolros's avatar

The easiest way is to setup 3 tables user, roles and a user_roles pivot.

Roles table would have e.g. user_id, a roles name, descriptions, slug and also a Boolean field for every permission / capability type you want can_post, can_delete, can_add, can_ ... etc. For every role you would set the states in those flags so Admin can_post = true (or 1), can_delete = true, whereas, a guest, would have can_view = 1 and the rest false. Then add map users to roles. You can then setup middleware to check on role type and setup routes groups to allow or deny overall access and fine grain permissions on flag check.

You will then look up the users role (guest) and its can_post flag state which if set to 0 will the return a false and you will send the user a message, etc.

Let me say that these are ugly when it comes to DB overhead which can offset to some degree with caching as roles and users don't change that often. However, the right way to build ACL and permission is with bitmasks that separate roles, action, objects and permissions into multi-stage auth.

usman's avatar

HI, @xtremer360 I did the same thing as @themsaid explained in one of my projects and also extracted the package for the future use. If you're using Laravel 4, here is the link usm4n/guardian, you can override the interface and plenty of helpers are also available.

Regards!

2 likes
jrdavidson's avatar

@usman Do you have any further examples of use with this because I am interested but need to see more of a real world use with this.

jrdavidson's avatar

@nolros Only thing is how do I relate those to see if a certain type of role can do certain actions in the controller functions.

nolros's avatar

I should add that the best way to setup permissions / ACL are with bitmasks. When you look at the code below you can see how it can quickly get ugly. Second best is the key approach recommended above which provides a certain level of abstraction.

Again, this is very basic but you could do it in couple of ways, but going to go with basic and you can then build up from there.

  1. Have a Controller domain controller so every request gets passed through a roles and permissions authenticator.
// 3 x tables
// 1. users
// 2. roles
// 3. users_roles - pivot table  

// example of 2 roles
// admin id:1, name: admin, can_create: 1, can_delete: 1, can_edit : 1, can_read 1
// guest id:2, name: guest, can_create: 0, can_delete: 0, can_edit : 0, can_read 1

// roles model
public function users()
{
    return $this->belongsToMany('Role', 'users_roles', 'role_id', 'user_id');
}

// users model
public function roles()
{
    return $this->belongsToMany('User', 'users_roles', 'user_id', 'role_id');
}


// inside on my Controller I have a method for creating Posts
// a user creates a Post which gets routed here
public function createPost()
{

    // 1. you need to capture and pass through the verb / action - read, edit, delete
    // 2. You can do that a number fo ways 
    //   - parse out the method name e.g. create
    //   - inject as below
    //   - get from route, method, etc. 
    // you can simplify this will ServiceProvider, but for the example lets keep it simple  
    $result = $this->authManager->execute('create');

    if($result)
    {
        // do whatever it is you do do
    }

    //  inform user that they dont have permissions to Post
    //  return redirect('/posts')->withErrors();
    //  flash message, ect

}

use User;
use Role;

Class AuthManager {

    // sremoving constructor for brevity

    public function execute($action)
    {
        // is user autherticated
        // can be here is inside the controller
        if(Auth::check())
        {
            $userId = Auth::user()->id;

            $getRole = self::getRole($userId);

            if($getRole)
            {
                return self::hasPermission($getRole, $action)
            }
        }

        public function getRole($userId)
        {
            // you can have a model, a relationship 
            return User::find($userId)->roles();
        }

        private static function hasPermission($role, $action)
        {
            $status = null;

            // use switch, if or a foreach if you are passing an array of permission requests
            if($action === 'createPost') 
            {
                    return true;
            }
            return false;

        }

        public function isAdmin($$userId)
        {
            // check if the user has a an admin role in picot table 
        }

        // should make these static but your call
        public function canRead($role)
        {
            // check the read flag in the role
        }

        public function canCreate($role)
        {
            // check the create flag in role
        }

    }

}

Option 2: which should be combined with 1, but optional depending in the amount of security is filters

Route::filter('admin', function()
{
    // closure method or you can create simple role rules 
});

// OR
// which would call the above class
Route::filter('admin', 'AuthManager@isAdmin');

// then apply all the route magic you want
Route::when('post/*', 'admin');
// the role filter would be applied to all routes beginning with post/. The asterisk is used as a wildcard, and will match any combination of characters

// apply it to a controller
Route::get('user', array('before' => 'admin', 'uses' => 'PostController@createPost'));

// or multi filters per route, you would need to set up each filter for this in the same 
Route::get('user', array('before' => 'auth|canCreate|canEdit|canDelete', function()
{
    //
}));

A more sophisticated, but again not ideal ACL process would look like this.

Note this can get very complex very quickly, but will attempt to outline the basic approach. I have a very complex ACL system on my app and it is a crap load of code. At a certain point you will need to look at bitmasks to minimize DB usage.

//    Create roles table with all the roles you will ever need for any type of e.g. admin, user, moderator,coordinator, manager, etc. Columns would be id, user_id, name, description, slug, etc.
//    Create a permission table with all the sets of permissions you can think of for any  permission_types. Example, 20 perm types. can_attend, can_get_content, can_access_web, can .... etc.
 //   Create an object type table.
 //  Create an permission_map which maps the the types of permissions you want from the selection above to a role permissions_roles_map.
 //   Optional - create a permissions granted table to set the state of 10 permissions OR you can set them permissions_roles_map, I prefer to have a granted table, but your call. WHy do you need this? Permissions_roles_map just says the Admin has a permission but not its state. Example, you could have a Boolean can_read column that will default to null or 0 if no value is set in the DB which equates to cannot read so you will need to set can_read = 1 (or true)
 //   Create a roles_object_map table which maps the event to the permissions_roles_map. Which now states that there is a role Admin available for object id = 77. Think of this as a Group, or Security Group. You now have an Object Mapped To Role that is Mapped to a set of Permissions. Next step is invite members to this "group"
  //  You would then have users mapped to permissions_roles_map so that fenos userId = 55 is Admin at object id = 77. 

Please or to participate in this conversation.