0

can(), cannot(), authorize() — using policies in controllers and Blade

Intermediate5 min read·lv-17-003

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
<?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