dev-master
9999999-devPowerful, flexible, relational permissions using Eloquent.
The Requires
- php >=5.4.0
- illuminate/database 5.0.*
The Development Requires
by Toby Zerner
Powerful, flexible, relational permissions using Eloquent.
Powerful, flexible, relational permissions using Eloquent., (*1)
Permissible allows you to define permissions as independent logic clauses which can be directly evaluated against a model's data, or compiled into a Fluent query's WHERE clause., (*2)
Sometimes, permissions can be complex. Imagine a scenario with discussions which each have many posts. Here is our permission logic:, (*3)
The cascade starts to build: a user can view a post ONLY IF the discussion it's in was started by them., (*4)
So how do we tell if a post can be viewed or not? The obvious solution might be to do something like this:, (*5)
class Post extends Eloquent { public function discussion() { return $this->belongsTo('Discussion'); } public function canView() { return $this->discussion->canView(); } } class Discussion extends Eloquent { public function canView() { return $this->start_user_id == Auth::user()->id; } }
Great! Now we can call $post->canView()
to determine whether or not a post can be viewed. But this won't work in all cases. Imagine we're doing a search for posts:, (*6)
$results = Post::with('discussion') ->where('content', 'like', '%hello%') ->take(20) ->get();
Let's say we got the 20 results we requested, and there are more in the database that we didn't get. But now we have to filter them down to only the ones we can view:, (*7)
$results = $results->filter(function ($post) { return $post->canView(); });
Now we might only have 10, or 5, or even 0! We can't present this to the user — they want a full page of results, and there's no good reason why they shouldn't get what they want. Clearly, we need to filter out the posts that the user can't view in the search query., (*8)
OK, so how about something like this:, (*9)
$viewableDiscussions = function ($query) { $query->select('id') ->from('discussions') ->where('start_user_id', Auth::user()->id); }; $results = Post::where('content', 'LIKE', '%hello%') ->whereIn('discussion_id', $viewableDiscussions) ->take(20) ->get();
Great, problem solved, right? Well, yes, but now we've duplicated our permission logic in the Discussion model's canView
method and our sub-select query., (*10)
We could of course move the sub-select logic into a scopeCanView
method, but the logic is still duplicated — the canView
copy of the logic evaluates the model's data, while the scopeCanView
copy of the logic adds a WHERE clause to a query to filter its results. It's the exact same logic, just in a different form! When permissions get really complex, this duplication is painful., (*11)
Permissible makes it really easy to deal with scenarios like these. Permissions are defined by agnostic condition clauses which can be directly evaluated against a model's data, or compiled into a query's WHERE clause to filter results., (*12)
via Composer:, (*13)
"tobscure/permissible": "*"
On any models which you want to have permission checking available for, simply include the Tobscure\Permissible\Permissible
trait. Easy!, (*14)
use Tobscure\Permissible\Permissible; class Discussion extends Eloquent { use Permissible; }
This trait provides can
and scopeWhereCan
methods so you can do things like this:, (*15)
// Check if a user has permission to view a certain discussion $discussion = Discussion::find(1); if (! $discussion->can($user, 'view')) { echo 'permission denied'; } // List all of the discussions that a user has permission to view $discussions = Discussion::whereCan($user, 'view')->get();
However, these won't be much good without having defined any conditions upon which to grant the permission, because initially all permissions are denied., (*16)
To define a condition on which to grant a permission, the trait provides us with a static grant
method. We can call this when we boot up the model. The first argument is the name of the permission; the second is a Closure which accepts a Tobscure\Permissible\Condition\Builder
object, and a $user object., (*17)
The Condition\Builder
class is very similar to Laravel's query builder; you will be familiar with the where
, whereNull
, whereNotNull
, whereIn
, and whereNotIn
methods, as well as the or
variants of them all., (*18)
public static function boot() { parent::boot(); static::grant('view', function ($grant, $user) { $grant->where('start_user_id', $user->id); }); }
In addition to these static where
conditions, you can define conditions which depend upon another permission, or a permission on a relationship. This is done using the whereCan
and whereCannot
methods. (These also have or
variants.), (*19)
$grant->whereCan('edit') ->orWhereCan('view', 'user');
You may also define conditions which check for the presence or absence of a database record by passing a Closure to whereExists
or whereNotExists
. As well as a query builder, this Closure accepts a value which represents the ID of the model; it will be a binding placeholder (?
) if the condition is being evaluated against model data, and it will be the name of a column if the condition is being added to a query., (*20)
$grant->whereExists(function ($query, $id) use ($user) { return $query->select(DB::raw(1)) ->from('discussions_private') ->where('user_id', $user->id) ->whereRaw("discussion_id = $id"); });
You may grant multiple permissions using the same logic by passing in an array of permission names, or by ommitting the permission argument altogether. The third argument passed into the closure is the name of the permission being evaluated., (*21)
To always grant a permission, regardless of the specific entity in question, you may return true from the callback., (*22)
static::grant(['view', 'edit'], function ($grant, $user) { return $user->isAdmin(); });
If any one of the sets of conditions defined for a certain permission is satisfied, then the permission is granted. So we can dyanmically add new grant conditions at runtime:, (*23)
// Users can view discussions if they are started by themselves static::grant('view', function ($grant, $user) { $grant->where('start_user_id', $user->id); }); // Users can also view discussions if they are started by their mothers static::grant('view', function ($grant, $user) { $grant->where('start_user_id', $user->mother->id); });
But what if, at runtime, we needed to make it so that a user couldn't view any discussions that are more than a year old, regardless of who they were started by? To achieve this, we have to add required conditions using the check
method:, (*24)
// Users can view discussions ONLY if they are less than a year old static::check('view', function ($check, $user) { $oneYearAgo = strtotime('-1 year', time()); $check->where('start_time', '<', $oneYearAgo); });
For a permission to be granted, all of the conditions defined using the check
method must be satisfied, if there are any., (*25)
To summarise, in order for a permission to be granted, two sets of conditions must be satisfied:, (*26)
Powerful, flexible, relational permissions using Eloquent.