can(), cannot(), authorize() — using policies in controllers and Blade
Concept
The before and after hooks on gates allow super-admin bypass patterns and audit logging without modifying every policy method.
Gate::before(callable $callback): Runs before all gate checks. If the callback returns a non-null value, that value overrides the gate result. If it returns null, normal gate evaluation continues.
Gate::after(callable $callback): Runs after all gate checks. The callback receives the user, ability, result, and arguments. Cannot override the result — used for logging, auditing.
Super-admin pattern: A common use is allowing super-admins to bypass all authorization: Gate::before(fn($user) => $user->isSuperAdmin() ? true : null). Return null (not false) when NOT super-admin to allow normal evaluation.
Warning: The before hook runs for ALL gates including Policies. This is global. If you return true unconditionally, ALL authorization is bypassed for that user.
Policy before method: Policies can also define a before() method that runs before all policy methods for that specific policy. Same return semantics — null allows normal evaluation, true/false overrides.
Guest users in before: The before callback receives null when unauthenticated. Always type-hint ?User and handle accordingly.
Code Example
<?php
use Illuminate\Support\Facades\Gate;
// Global before hook — super admin bypass
Gate::before(function(?User $user, string $ability): bool|null {
if ($user === null) return null; // guest — let normal evaluation handle it
if ($user->isSuperAdmin()) return true; // bypass ALL gates
return null; // normal evaluation for everyone else
});
// After hook — audit log every authorization decision
Gate::after(function(?User $user, string $ability, bool $result, mixed $arguments): void {
if ($user !== null) {
\Illuminate\Support\Facades\Log::info('Authorization check', [
'user_id' => $user->id,
'ability' => $ability,
'result' => $result ? 'granted' : 'denied',
'model' => isset($arguments[0]) ? get_class($arguments[0]) : null,
]);
}
});
// Policy-level before — only affects THIS policy
class PostPolicy
{
public function before(User $user, string $ability): bool|null
{
// Admins can do anything with posts
if ($user->role === 'admin') return true;
return null; // defer to specific policy method
}
public function update(User $user, Post $post): bool
{
return $user->id === $post->user_id;
// Note: if before() returned true for admin, this never runs for admins
}
}
// Multiple before hooks (they chain)
Gate::before(function($user, $ability) {
if ($user?->isBanned()) return false; // banned users denied everything
return null;
});
Gate::before(function($user, $ability) {
if ($user?->isSuperAdmin()) return true;
return null;
});
// First before: banned check → if not banned, null → second before: superadmin check