I have the following policy which determines if a user is able to view a contract.
public function view(User $user, Contract $contract)
{
if ($user->user_type->id == 2) { // If user is a vecino
if ($user->id == $contract->customer_id) {
return true;
}
} else if ($user->user_type->is_admin == true) { // If user is an admin
return true;
}
return false;
}
Which is then checked for authorization with
$this->authorize('view', $contract);
How do I check authorization for a list/array/collection? Like if I get a list of contracts via Contract::all()
I haven't found any way to do this. I could do a loop and call $this->authorize for every iteration to check for authorization but that might impact performance.
Is there a better way of doing this?
One solution I am currently using is a hybrid approach where you define your rules within a scope and then reference that scope from the policy allowing you to reuse your authorization logic.
// Contract model
public function scopeViewable($query)
{
// If the user is admin, just return the query unfiltered.
if (Auth::user()->user_type->is_admin) {
return $query;
}
// Check the contract belongs to the logged in user.
return $query->where('customer_id', Auth::id());
}
And then in your policy, reference that scope but restrict it to the current model. Make sure to return a boolean using exists()
. This essentially checks that your model is viewable.
// Contract Policy
public function view(User $user, Contract $contract)
{
return Contract::viewable()
->where('id', $contract->id)
->exists()
;
}
Importantly, you should use the scope when retrieving a collection of models and not the policy which would run the scope query for each model in the collection. Policies should be used on individual model instances.
Contract::viewable()->paginate(10);
// Or
Contract::viewable()->get();
But, when you want to check an individual contract you can use your policy directly.
$this->authorize('view', $contract);
// Or
Auth::user()->can('view', [Contract::class, $contract]);
The design i often sees in this case, is to check if all elements in the query is allowed to be viewed through the policy. This does not scale well and works bad with pagination.
Instead of filtering out the contracts with policies, the better solution is to filter the contracts already in the query. This mainly because if you want to do pagination down the line, you want to do all filtering before the query is executed to avoid having weird pagination meta data. While also having to run n operations for each element, which would already be a problem at 1000 elements.
There for doing the following query clause, can obtain the same result as your policy.
Contract::where('user_id', $user->id)->get();
A version of this i usually do to make things easier for my self is creating a scope in the user model.
public function scopeOwned($query, User $user)
{
return $this->query->where('user_id', $user->id);
}
Contract::owned($user)->get();
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With