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.