Over the years I have tried many implementations, first using a binary system

0 - guest(0)
1 - user(1)
10 - moderator(2)
...
10000000000 - admin (1024)
...
1000000000000000 - superadmin (32768)

basically, a new role had the highest order bit set to 1, then the rest zeros. To add a user to moderator and user (11), or admin & moderator (10000000010), for example, you would do something like this. At the method level I could either do bitwise operations to check for certain bits being set to 1 or I could just check that the decimal was >= a specific decimal value (but this could allow users I may not want having access). The limitations being many, not least of which: what should I set superadmin/superuser to b/c this determines the max amount of future roles that can be added without going through the code and refactoring.

I then moved to a system of roles and user permissions, meaning I had permissions at the group/role level and permissions at the user level. I would merge these permissions together when user is authenticated. At the user level I could remove a role from a user belonging to a group by adding the same permission as found in the group but with an integer -1, or i could add a permission to a specific user with an integer value of 1. I was still checking for role and permission at the user level. This was better in the sense that I could add as many roles as I wanted, as many permissions as I wanted. Merging permissions was a bit of a pain but once it was all worked out it worked pretty good. I was still checking for roles/group membership and permissions at the method level and well this causes a lot refactoring when you want to change what permissions and groups can execute a particular method. I had always felt this was a pain and this system was a bit of overkill but it did exactly what was needed at the time.

I then came across an old article, pretty sure it was this one - https://lostechies.com/derickbailey/2011/05/24/dont-do-role-... - and it led me to the path, right or wrong, that I do now. I create a table for permissions, users, groups. I then create a pivot/join table between groups and permissions, and a pivot/join table between users and permissions. This is so I can give group level and user level permissions. an example permission might look like this: user.create, user.delete, user.update, user.block, user.suspend, etc.. At a user group level a user may have all those permissions but user.suspend, but I may want a specific user to have user.suspend capabilities but not the other capabilities that come at the next level, say moderator, so I keep the user in the user group but give them user.suspend permissions at the user perm level.

Now when those two permission groups are merged (group, users) this makes up the permissions available to a given user.

At the method level I just check for a specific permission ... so at the method to create a user, I check for the permission user.create. At a user update method I may check for user.update permission and maybe the user id (dont want a user updating someone else's profile). The point is that by looking for a specific permission rather than group(s)/role(s) I cut down the amount of refactoring I need to do. Every scenario I've outlined has pros and cons. For instance, what happens if I have a user that can update anyone's profile but i'snt a superadmin or admin?? Maybe create a new permission that give global.user.update and check for both of those. Who knows?!

Hope this gives you some ideas.