Under the Hood

@can Directive - How Blade Checks Permissions

Learn how Laravel's @can Blade directive checks permissions behind the scenes. Understand authorization in templates, directive compilation, and performance in Laravel 12.

Muhammad Waqas

Muhammad Waqas

CEO at CentoSquare

|
12-Dec-2025
6 min read
@can Directive - How Blade Checks Permissions

The @can directive makes authorization checks in Blade templates elegantly simple. But what happens when you write @can('edit', $post) in your view? Behind this clean syntax lies Blade's compilation engine, Laravel's Gate system, and intelligent caching. Let's explore how permission checks happen in your templates.

What Is the @can Directive?

The @can directive is Blade's way of checking authorization directly in templates. It provides a clean, readable syntax for showing or hiding content based on user permissions:

@can('update', $post)
    <a href="{{ route('posts.edit', $post) }}">Edit Post</a>
@endcan

If the user is authorized to update the post, the edit link appears. Otherwise, the entire block is hidden.

Basic @can Syntax

@can('ability-name', $model)
    <!-- Content shown if authorized -->
@endcan

The directive accepts:

  • First parameter: Ability name (gate or policy method)
  • Second parameter: Optional model instance for authorization

How @can Compiles to PHP

Blade is a templating engine that compiles to pure PHP. When you use @can, Blade transforms it during compilation:

// What you write in Blade
@can('update', $post)
    <button>Edit</button>
@endcan

// What Blade compiles to
<?php if (app(\Illuminate\Contracts\Auth\Access\Gate::class)->check('update', $post)): ?>
    <button>Edit</button>
<?php endif; ?>

The @can directive is syntactic sugar for Gate::check().

The Complete Authorization Flow

Step 1: Blade Compilation

When Laravel encounters a .blade.php file, it compiles it to cached PHP:

// Behind the scenes
$compiled = $this->compileString($bladeSyntax);

// Stored in storage/framework/views/
// Filename: hash of original blade file path

The @can directive triggers a specific compiler:

// Blade compiler for @can
protected function compileCan($expression)
{
    return "<?php if (app(\Illuminate\Contracts\Auth\Access\Gate::class)->check{$expression}): ?>";
}

Step 2: Runtime Gate Resolution

When the compiled view executes, it resolves the Gate:

// Behind the scenes
$gate = app(\Illuminate\Contracts\Auth\Access\Gate::class);
// Resolves to the Gate instance

This happens on every request—Blade caches the compiled PHP, not the authorization result.

Step 3: Authorization Check

The Gate checks if the user can perform the ability:

// Behind the scenes in Gate::check()
$user = Auth::user();

if (!$user) {
    return false; // No authenticated user
}

// Find the ability (gate or policy)
$ability = $this->resolveAbility('update', $post);

// Execute authorization callback
return $ability($user, $post);

Step 4: Content Rendering

Based on the authorization result:

if ($authorized) {
    // Render the content between @can and @endcan
    echo '<button>Edit</button>';
} else {
    // Skip the content entirely
}

The content inside @can is only rendered if authorization succeeds.

Additional @can Variants

@cannot - The Inverse

@cannot('delete', $post)
    <p>You cannot delete this post</p>
@endcannot

// Compiles to
<?php if (app(\Illuminate\Contracts\Auth\Access\Gate::class)->denies('delete', $post)): ?>
    <p>You cannot delete this post</p>
<?php endif; ?>

Shows content when authorization FAILS.

@canany - Check Multiple Abilities

@canany(['update', 'delete'], $post)
    <div class="post-actions">
        <!-- Show actions if user can do ANY of these -->
    </div>
@endcanany

// Compiles to
<?php if (app(\Illuminate\Contracts\Auth\Access\Gate::class)->any(['update', 'delete'], $post)): ?>
    <!-- Content -->
<?php endif; ?>

Checks if user has ANY of the specified abilities.

@else and @elsecan

@can('update', $post)
    <button>Edit</button>
@elsecan('view', $post)
    <button>View</button>
@else
    <p>No access</p>
@endcan

Provides fallback content for different authorization states.

Working Without Models

You can check abilities without passing models:

@can('create-posts')
    <a href="{{ route('posts.create') }}">Create Post</a>
@endcan

// Compiles to
<?php if (app(\Illuminate\Contracts\Auth\Access\Gate::class)->check('create-posts')): ?>
    <!-- Content -->
<?php endif; ?>

Useful for general permissions not tied to specific models.

Guest User Behavior

If no user is authenticated, @can returns false by default:

// No logged-in user
@can('view-dashboard')
    <!-- Never rendered for guests -->
@endcan

To explicitly handle guests, define gates that accept nullable users:

// In AuthServiceProvider
Gate::define('view-homepage', function (?User $user) {
    return true; // Everyone (including guests) can view
});

How @can Finds Policies Automatically

When you pass a model to @can, Laravel automatically looks for a policy:

@can('update', $post)
    <!-- Laravel looks for PostPolicy::update() -->
@endcan

Behind the scenes:

// Laravel's policy discovery
$policyClass = 'App\Policies\\' . class_basename($post) . 'Policy';
// Result: App\Policies\PostPolicy

if (class_exists($policyClass)) {
    // Use policy method
    return $policy->update($user, $post);
}

// Otherwise, look for gate
return Gate::allows('update', $post);

This automatic policy resolution happens seamlessly.

Performance Implications

Compilation Caching

Blade compiles templates once and caches the result:

// First request - compiles .blade.php to PHP
// Subsequent requests - uses cached PHP file
// No recompilation unless blade file changes

The compiled PHP lives in storage/framework/views/:

storage/framework/views/
├── 1a2b3c4d5e6f.php
├── 9f8e7d6c5b4a.php
└── ...

Authorization Check Performance

Every @can triggers a runtime authorization check:

@can('update', $post)     // Check 1 - ~5-10ms
@can('delete', $post)     // Check 2 - ~5-10ms
@can('publish', $post)    // Check 3 - ~5-10ms

Multiple @can directives = multiple checks. For expensive authorization logic, cache results:

@php
    $canUpdate = Gate::allows('update', $post);
    $canDelete = Gate::allows('delete', $post);
@endphp

@if($canUpdate)
    <button>Edit</button>
@endif

@if($canDelete)
    <button>Delete</button>
@endif

Combining Multiple Conditions

You can combine @can with other Blade directives:

@auth
    @can('update', $post)
        <button>Edit</button>
    @endcan
@endauth

// Or with @if
@if(Auth::check() && Gate::allows('update', $post))
    <button>Edit</button>
@endif

But @can already checks authentication internally, so @auth is redundant:

// Redundant
@auth
    @can('update', $post)
        <!-- ... -->
    @endcan
@endauth

// Sufficient
@can('update', $post)
    <!-- ... -->
@endcan

Using @can in Loops

@foreach($posts as $post)
    <div class="post">
        <h2>{{ $post->title }}</h2>
        
        @can('update', $post)
            <a href="{{ route('posts.edit', $post) }}">Edit</a>
        @endcan
        
        @can('delete', $post)
            <form method="POST" action="{{ route('posts.destroy', $post) }}">
                @csrf
                @method('DELETE')
                <button>Delete</button>
            </form>
        @endcan
    </div>
@endforeach

Authorization is checked for each post individually.

Debugging @can Directives

Check Compiled Output

# View compiled Blade file
cat storage/framework/views/hash.php

You'll see the pure PHP that @can compiled to.

Debug Authorization

@can('update', $post)
    <button>Edit</button>
@else
    <!-- Debug why authorization failed -->
    <p>Debug: Cannot update post</p>
    <p>User ID: {{ Auth::id() }}</p>
    <p>Post Owner: {{ $post->user_id }}</p>
@endcan

Use Gate Inspection

@php
    $response = Gate::inspect('update', $post);
@endphp

@if($response->allowed())
    <button>Edit</button>
@else
    <p>Denied: {{ $response->message() }}</p>
@endif

Shows the exact authorization failure reason.

Common Patterns

Action Buttons

<div class="post-actions">
    @can('update', $post)
        <a href="{{ route('posts.edit', $post) }}" class="btn">Edit</a>
    @endcan
    
    @can('delete', $post)
        <button class="btn btn-danger" onclick="deletePost()">Delete</button>
    @endcan
</div>

Admin-Only Content

@can('access-admin-panel')
    <a href="{{ route('admin.dashboard') }}">Admin Panel</a>
@endcan

Conditional Table Columns

<table>
    <thead>
        <tr>
            <th>Title</th>
            <th>Author</th>
            @canany(['update', 'delete'], Post::class)
                <th>Actions</th>
            @endcanany
        </tr>
    </thead>
    <tbody>
        @foreach($posts as $post)
            <tr>
                <td>{{ $post->title }}</td>
                <td>{{ $post->author }}</td>
                @canany(['update', 'delete'], $post)
                    <td>
                        @can('update', $post)
                            <a href="{{ route('posts.edit', $post) }}">Edit</a>
                        @endcan
                        @can('delete', $post)
                            <button onclick="deletePost({{ $post->id }})">Delete</button>
                        @endcan
                    </td>
                @endcanany
            </tr>
        @endforeach
    </tbody>
</table>

Security Considerations

@can Is Not Enough

The @can directive only hides UI elements—it doesn't prevent unauthorized access:

// In Blade - hides button
@can('delete', $post)
    <button>Delete</button>
@endcan

// In Controller - actually prevents deletion
public function destroy(Post $post)
{
    $this->authorize('delete', $post);
    
    $post->delete();
}

Always authorize in controllers, not just views.

Don't Expose Sensitive Data

<!-- BAD - exposes data even if button is hidden -->
@can('view-salary', $employee)
    <p>Salary: ${{ $employee->salary }}</p>
@endcan

<!-- GOOD - don't load sensitive data at all -->
@can('view-salary', $employee)
    <p>Salary: ${{ $employee->public_salary }}</p>
@endcan

If authorization fails, don't load the sensitive data in the first place.

Best Practices

1. Keep Authorization Logic in Gates/Policies

<!-- Don't do complex logic in views -->
@if($user->role === 'admin' || $user->id === $post->user_id)
    <!-- ... -->
@endif

<!-- Do this instead -->
@can('update', $post)
    <!-- ... -->
@endcan

2. Use Descriptive Ability Names

<!-- Unclear -->
@can('manage', $post)

<!-- Clear -->
@can('update', $post)
@can('delete', $post)
@can('publish', $post)

3. Cache Repeated Checks

@php
    $canManagePost = Gate::allows('update', $post) || Gate::allows('delete', $post);
@endphp

@if($canManagePost)
    <div class="post-actions">
        <!-- Multiple actions -->
    </div>
@endif

4. Combine with Other Security Measures

<!-- Blade directive -->
@can('delete', $post)
    <form method="POST" action="{{ route('posts.destroy', $post) }}">
        @csrf <!-- CSRF protection -->
        @method('DELETE')
        <button>Delete</button>
    </form>
@endcan

// Controller authorization
public function destroy(Post $post)
{
    $this->authorize('delete', $post); // Double-check
    $post->delete();
}

Conclusion

The @can directive is Blade's elegant interface to Laravel's powerful Gate system. By understanding how it compiles to PHP, resolves policies, and checks authorization at runtime, you can write cleaner, more secure templates.

Remember: @can controls what users see, but controller authorization controls what they can actually do. Always implement both layers for robust security.


Building Laravel applications with complex authorization requirements? At NeedLaravelSite, we specialize in Laravel security and application development from version 7 to 12. From role-based access control to multi-tenant permissions, we build secure, maintainable authorization systems.


Article Tags

blade can directive laravel @can laravel blade permissions @cannot directive @canany directive laravel

About the Author

Muhammad Waqas

Muhammad Waqas

CEO at CentoSquare

Founder & CEO at CentoSquare | Creator of NeedLaravelSite | Helping Businesses Grow with Cutting-Edge Web, Mobile & Marketing Solutions | Building Innovative Products