Laravel API Resources: Transforming Your Eloquent Models for JSON Responses
Create consistent API responses with API resources.
Authorization determines what authenticated users are allowed to do. While authentication answers "Who are you?", authorization answers "What are you allowed to do?"
Gates are closure-based authorization rules that define abilities across your application. They're perfect for authorization that doesn't require a specific model.
<?php
// app/Providers/AuthServiceProvider.php
namespace App\Providers;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Gate;
use App\Models\User;
class AuthServiceProvider extends ServiceProvider
{
public function boot(): void
{
// Basic gate - check if user is admin
Gate::define('view-admin-dashboard', function (User $user) {
return $user->is_admin === true;
});
// Gate with parameters
Gate::define('update-settings', function (User $user, string $section) {
if ($user->is_admin) {
return true;
}
return in_array($section, ['profile', 'notifications']);
});
// Gate before - runs before all other gates
Gate::before(function (User $user, $ability) {
if ($user->is_super_admin) {
return true;
}
});
// Gate after - runs after all other gates
Gate::after(function (User $user, $ability, $result, $arguments) {
if ($user->is_super_admin) {
return true;
}
});
}
}
// app/Providers/AuthServiceProvider.php
public function boot(): void
{
// Check if user can manage users
Gate::define('manage-users', function (User $user) {
return $user->hasRole('admin') || $user->hasRole('moderator');
});
// Check if user can access billing
Gate::define('access-billing', function (User $user) {
return $user->subscribed() || $user->onTrial();
});
// Check if user can export data
Gate::define('export-data', function (User $user, $dataType) {
if (!$user->hasPermission('export')) {
return false;
}
$allowedTypes = match($user->role) {
'admin' => ['users', 'orders', 'products', 'analytics'],
'manager' => ['orders', 'products'],
default => ['orders'],
};
return in_array($dataType, $allowedTypes);
});
// Time-based gate (only during business hours)
Gate::define('access-live-chat', function (User $user) {
$now = now();
$isBusinessHours = $now->isWeekday() &&
$now->between('09:00', '17:00');
return $user->hasSupportAccess() && $isBusinessHours;
});
// Resource limit gate
Gate::define('create-project', function (User $user) {
$maxProjects = match($user->plan) {
'premium' => 50,
'professional' => 20,
'basic' => 5,
default => 1,
};
return $user->projects()->count() < $maxProjects;
});
}
<?php
// app/Http/Controllers/AdminController.php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
class AdminController extends Controller
{
public function dashboard(Request $request)
{
// Check authorization
if (!Gate::allows('view-admin-dashboard')) {
abort(403);
}
return view('admin.dashboard');
}
public function settings(Request $request, $section)
{
// Check authorization with parameters
if (!Gate::allows('update-settings', $section)) {
abort(403, "You cannot update {$section} settings.");
}
return view("settings.{$section}");
}
// Using authorize method (throws AuthorizationException)
public function exportData(Request $request, $dataType)
{
$this->authorize('export-data', $dataType);
// Export logic here
return $this->export($dataType);
}
// Multiple authorization checks
public function manageUsers(Request $request)
{
Gate::authorize('manage-users');
Gate::authorize('access-admin-panel');
return view('admin.users');
}
}
{{-- Basic gate check --}}
@can('view-admin-dashboard')
<a href="/admin/dashboard" class="nav-link">Admin Dashboard</a>
@endcan
{{-- Gate with parameters --}}
@can('update-settings', 'billing')
<a href="/settings/billing" class="nav-link">Billing Settings</a>
@endcan
{{-- Alternative syntax --}}
@if (Gate::allows('manage-users'))
<div class="admin-panel">
<h3>User Management</h3>
<!-- User management interface -->
</div>
@endif
{{-- Multiple conditions --}}
@can('access-billing')
@can('export-data', 'invoices')
<button onclick="exportInvoices()">Export Invoices</button>
@endcan
@endcan
{{-- Else condition --}}
@can('create-project')
<button>Create New Project</button>
@else
<p class="text-muted">Upgrade your plan to create more projects.</p>
@endcan
// routes/api.php
Route::middleware('auth:sanctum')->group(function () {
// Using gate in route middleware
Route::get('/admin/metrics', function (Request $request) {
Gate::authorize('view-admin-dashboard');
return response()->json([
'metrics' => $this->getAdminMetrics()
]);
});
Route::get('/exports/{type}', function (Request $request, $type) {
if (!Gate::check('export-data', $type)) {
return response()->json([
'error' => 'Unauthorized to export this data type.'
], 403);
}
return $this->generateExport($type);
});
});
Policies are class-based authorization rules that organize authorization logic around a particular model or resource. They're perfect for CRUD operations and model-specific permissions.
# Create policy for Post model
php artisan make:policy PostPolicy --model=Post
# Create policy for multiple models
php artisan make:policy UserPolicy --model=User
php artisan make:policy CommentPolicy --model=Comment
# Create policy without model
php artisan make:policy ReportPolicy
Laravel automatically discovers policies if you follow naming conventions:
<?php
// app/Policies/PostPolicy.php
namespace App\Policies;
use App\Models\Post;
use App\Models\User;
class PostPolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
// Anyone can view posts list
return true;
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, Post $post): bool
{
// Users can view their own posts or published posts
return $post->user_id === $user->id || $post->is_published;
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
// Users must be verified to create posts
return $user->hasVerifiedEmail() && $user->can_create_posts;
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, Post $post): bool
{
// Users can update their own posts
// Admins can update any post
return $user->id === $post->user_id || $user->is_admin;
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, Post $post): bool
{
// Users can delete their own posts
// Admins can delete any post except their own (prevent accidental deletion)
return $user->id === $post->user_id ||
($user->is_admin && $user->id !== $post->user_id);
}
/**
* Determine whether the user can restore the model.
*/
public function restore(User $user, Post $post): bool
{
// Only admins can restore soft deleted posts
return $user->is_admin;
}
/**
* Determine whether the user can permanently delete the model.
*/
public function forceDelete(User $user, Post $post): bool
{
// Only super admins can permanently delete
return $user->is_super_admin;
}
/**
* Determine whether the user can publish the post.
*/
public function publish(User $user, Post $post): bool
{
// Users can publish their own drafts
// Admins can publish any draft
return $post->user_id === $user->id || $user->is_admin;
}
/**
* Determine whether the user can feature the post.
*/
public function feature(User $user, Post $post): bool
{
// Only editors and admins can feature posts
return $user->hasRole('editor') || $user->is_admin;
}
/**
* Determine whether the user can view post analytics.
*/
public function viewAnalytics(User $user, Post $post): bool
{
// Post owners and admins can view analytics
return $post->user_id === $user->id || $user->is_admin;
}
}
<?php
// app/Policies/UserPolicy.php
namespace App\Policies;
use App\Models\User;
class UserPolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
// Only admins and HR can view user list
return $user->is_admin || $user->hasRole('hr');
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, User $model): bool
{
// Users can view their own profile
// Admins can view any profile
return $user->id === $model->id || $user->is_admin;
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
// Only admins can create new users
return $user->is_admin;
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, User $model): bool
{
// Users can update their own profile
// Admins can update any profile except their own role
if ($user->id === $model->id) {
return true;
}
return $user->is_admin && $user->id !== $model->id;
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, User $model): bool
{
// Admins can delete users except themselves and other admins
return $user->is_admin &&
$user->id !== $model->id &&
!$model->is_admin;
}
/**
* Determine whether the user can restore the model.
*/
public function restore(User $user, User $model): bool
{
return $user->is_admin;
}
/**
* Determine whether the user can permanently delete the model.
*/
public function forceDelete(User $user, User $model): bool
{
// Only super admins can permanently delete users
return $user->is_super_admin;
}
/**
* Determine whether the user can change user roles.
*/
public function changeRole(User $user, User $model): bool
{
// Only admins can change roles, and cannot change their own role
return $user->is_admin && $user->id !== $model->id;
}
/**
* Determine whether the user can impersonate another user.
*/
public function impersonate(User $user, User $model): bool
{
// Only admins can impersonate, and cannot impersonate other admins
return $user->is_admin &&
$user->id !== $model->id &&
!$model->is_admin;
}
/**
* Determine whether the user can view user analytics.
*/
public function viewAnalytics(User $user, User $model): bool
{
// Users can view their own analytics, admins can view all
return $user->id === $model->id || $user->is_admin;
}
}
<?php
// app/Http/Controllers/PostController.php
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
class PostController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(Request $request)
{
// Policy automatically checks viewAny
$this->authorize('viewAny', Post::class);
$posts = Post::with('user')->latest()->paginate(10);
return view('posts.index', compact('posts'));
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
$this->authorize('create', Post::class);
return view('posts.create');
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
$this->authorize('create', Post::class);
$validated = $request->validate([
'title' => 'required|string|max:255',
'content' => 'required|string',
]);
$post = $request->user()->posts()->create($validated);
return redirect()->route('posts.show', $post);
}
/**
* Display the specified resource.
*/
public function show(Post $post)
{
// Automatically uses view policy
$this->authorize('view', $post);
return view('posts.show', compact('post'));
}
/**
* Show the form for editing the specified resource.
*/
public function edit(Post $post)
{
$this->authorize('update', $post);
return view('posts.edit', compact('post'));
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, Post $post)
{
$this->authorize('update', $post);
$validated = $request->validate([
'title' => 'required|string|max:255',
'content' => 'required|string',
]);
$post->update($validated);
return redirect()->route('posts.show', $post);
}
/**
* Remove the specified resource from storage.
*/
public function destroy(Post $post)
{
$this->authorize('delete', $post);
$post->delete();
return redirect()->route('posts.index');
}
/**
* Publish a post.
*/
public function publish(Post $post)
{
$this->authorize('publish', $post);
$post->update(['is_published' => true]);
return redirect()->route('posts.show', $post)
->with('success', 'Post published successfully!');
}
/**
* Feature a post.
*/
public function feature(Post $post)
{
$this->authorize('feature', $post);
$post->update(['is_featured' => true]);
return redirect()->route('posts.show', $post)
->with('success', 'Post featured successfully!');
}
}
{{-- Basic policy checks --}}
@can('create', App\Models\Post::class)
<a href="{{ route('posts.create') }}" class="btn btn-primary">
Create Post
</a>
@endcan
@foreach($posts as $post)
<div class="post-card">
<h3>{{ $post->title }}</h3>
<p>{{ $post->excerpt }}</p>
{{-- Policy for specific post --}}
@can('view', $post)
<a href="{{ route('posts.show', $post) }}">Read More</a>
@endcan
{{-- Multiple policy checks --}}
<div class="post-actions">
@can('update', $post)
<a href="{{ route('posts.edit', $post) }}" class="btn btn-sm btn-outline-secondary">
Edit
</a>
@endcan
@can('delete', $post)
<form action="{{ route('posts.destroy', $post) }}" method="POST" class="d-inline">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-sm btn-outline-danger"
onclick="return confirm('Are you sure?')">
Delete
</button>
</form>
@endcan
@can('publish', $post)
@if(!$post->is_published)
<form action="{{ route('posts.publish', $post) }}" method="POST" class="d-inline">
@csrf
<button type="submit" class="btn btn-sm btn-outline-success">
Publish
</button>
</form>
@endif
@endcan
@can('feature', $post)
@if(!$post->is_featured)
<form action="{{ route('posts.feature', $post) }}" method="POST" class="d-inline">
@csrf
<button type="submit" class="btn btn-sm btn-outline-warning">
Feature
</button>
</form>
@endif
@endcan
</div>
</div>
@endforeach
{{-- Policy checks without else --}}
@can('viewAnalytics', $post)
<div class="analytics-section">
<h4>Post Analytics</h4>
<p>Views: {{ $post->views }}</p>
<p>Engagement: {{ $post->engagement_rate }}%</p>
</div>
@endcan
// routes/api.php
Route::middleware('auth:sanctum')->group(function () {
// Post routes with policy authorization
Route::get('/posts', function (Request $request) {
// Check viewAny policy
if ($request->user()->cannot('viewAny', Post::class)) {
return response()->json(['error' => 'Unauthorized'], 403);
}
return Post::with('user')->latest()->paginate(10);
});
Route::post('/posts', function (Request $request) {
$request->user()->authorize('create', Post::class);
$post = $request->user()->posts()->create(
$request->validate([
'title' => 'required|string|max:255',
'content' => 'required|string',
])
);
return response()->json($post, 201);
});
Route::put('/posts/{post}', function (Request $request, Post $post) {
$request->user()->authorize('update', $post);
$post->update(
$request->validate([
'title' => 'sometimes|string|max:255',
'content' => 'sometimes|string',
])
);
return response()->json($post);
});
Route::delete('/posts/{post}', function (Request $request, Post $post) {
$request->user()->authorize('delete', $post);
$post->delete();
return response()->json(['message' => 'Post deleted']);
});
});
<?php
// app/Policies/ProjectPolicy.php
namespace App\Policies;
use App\Models\Project;
use App\Models\User;
class ProjectPolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
// Users can view projects from their teams
return $user->teams()->exists();
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, Project $project): bool
{
// User must be member of project's team
return $user->teams->contains($project->team_id);
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
// User must be team owner or admin to create projects
return $user->ownedTeams()->exists() || $user->is_admin;
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, Project $project): bool
{
// Team owners and project managers can update
return $project->team->owner_id === $user->id ||
$project->manager_id === $user->id ||
$user->is_admin;
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, Project $project): bool
{
// Only team owners and admins can delete
return $project->team->owner_id === $user->id || $user->is_admin;
}
/**
* Determine whether the user can manage project members.
*/
public function manageMembers(User $user, Project $project): bool
{
return $project->team->owner_id === $user->id ||
$project->manager_id === $user->id ||
$user->hasRole('project_lead');
}
/**
* Determine whether the user can view project budget.
*/
public function viewBudget(User $user, Project $project): bool
{
return $project->team->owner_id === $user->id ||
$project->manager_id === $user->id ||
$user->hasRole('finance');
}
}
<?php
// app/Policies/TimeEntryPolicy.php
namespace App\Policies;
use App\Models\TimeEntry;
use App\Models\User;
use Carbon\Carbon;
class TimeEntryPolicy
{
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
// Can only create entries during work hours
$now = now();
return $now->isWeekday() && $now->between('06:00', '20:00');
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, TimeEntry $timeEntry): bool
{
// Can only update recent entries (within 7 days)
return $timeEntry->created_at->gte(now()->subDays(7)) &&
$user->id === $timeEntry->user_id;
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, TimeEntry $timeEntry): bool
{
// Can only delete if not approved and within 24 hours
return !$timeEntry->is_approved &&
$timeEntry->created_at->gte(now()->subDay()) &&
$user->id === $timeEntry->user_id;
}
/**
* Determine whether the user can approve time entries.
*/
public function approve(User $user, TimeEntry $timeEntry): bool
{
// Managers can approve their team's entries
// Cannot approve own entries
return $user->is_manager &&
$user->id !== $timeEntry->user_id &&
$user->team_id === $timeEntry->user->team_id;
}
}
<?php
// app/Providers/AuthServiceProvider.php
namespace App\Providers;
use App\Models\Post;
use App\Models\User;
use App\Models\Project;
use App\Models\Comment;
use App\Policies\PostPolicy;
use App\Policies\UserPolicy;
use App\Policies\ProjectPolicy;
use App\Policies\CommentPolicy;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Gate;
class AuthServiceProvider extends ServiceProvider
{
/**
* The policy mappings for the application.
*
* @var array<class-string, class-string>
*/
protected $policies = [
Post::class => PostPolicy::class,
User::class => UserPolicy::class,
Project::class => ProjectPolicy::class,
Comment::class => CommentPolicy::class,
// Laravel will auto-discover policies that follow naming conventions
];
public function boot(): void
{
$this->registerPolicies();
// Global "before" gate - runs before all other checks
Gate::before(function ($user, $ability) {
// Super admins can do anything
if ($user->is_super_admin ?? false) {
return true;
}
});
// Global "after" gate - runs after all other checks
Gate::after(function ($user, $ability, $result, $arguments) {
// Log all authorization attempts for audit
if (app()->environment('production')) {
\Log::channel('auth')->info('Authorization attempt', [
'user_id' => $user->id,
'ability' => $ability,
'result' => $result,
'arguments' => $arguments,
]);
}
});
// Define additional gates
Gate::define('access-debug', function ($user) {
return app()->environment('local') && $user->is_developer;
});
Gate::define('view-horizon', function ($user) {
return $user->is_admin && app()->environment('production');
});
Gate::define('view-telescope', function ($user) {
return $user->is_developer;
});
}
}
<?php
// tests/Feature/PostPolicyTest.php
namespace Tests\Feature;
use App\Models\Post;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class PostPolicyTest extends TestCase
{
use RefreshDatabase;
public function test_user_can_view_their_own_posts()
{
$user = User::factory()->create();
$post = Post::factory()->create(['user_id' => $user->id]);
$this->assertTrue($user->can('view', $post));
}
public function test_user_cannot_view_other_users_unpublished_posts()
{
$user1 = User::factory()->create();
$user2 = User::factory()->create();
$post = Post::factory()->create([
'user_id' => $user1->id,
'is_published' => false
]);
$this->assertFalse($user2->can('view', $post));
}
public function test_user_can_view_published_posts()
{
$user = User::factory()->create();
$post = Post::factory()->create([
'user_id' => User::factory()->create()->id,
'is_published' => true
]);
$this->assertTrue($user->can('view', $post));
}
public function test_admin_can_view_any_post()
{
$admin = User::factory()->create(['is_admin' => true]);
$post = Post::factory()->create([
'user_id' => User::factory()->create()->id,
'is_published' => false
]);
$this->assertTrue($admin->can('view', $post));
}
public function test_user_can_update_their_own_posts()
{
$user = User::factory()->create();
$post = Post::factory()->create(['user_id' => $user->id]);
$this->assertTrue($user->can('update', $post));
}
public function test_user_cannot_update_other_users_posts()
{
$user1 = User::factory()->create();
$user2 = User::factory()->create();
$post = Post::factory()->create(['user_id' => $user1->id]);
$this->assertFalse($user2->can('update', $post));
}
public function test_admin_can_update_any_post()
{
$admin = User::factory()->create(['is_admin' => true]);
$post = Post::factory()->create([
'user_id' => User::factory()->create()->id
]);
$this->assertTrue($admin->can('update', $post));
}
}
// tests/Feature/PostControllerTest.php
class PostControllerTest extends TestCase
{
use RefreshDatabase;
public function test_guest_cannot_access_create_post_page()
{
$response = $this->get(route('posts.create'));
$response->assertRedirect('/login');
}
public function test_user_can_access_create_post_page()
{
$user = User::factory()->create();
$response = $this->actingAs($user)
->get(route('posts.create'));
$response->assertOk();
}
public function test_user_cannot_update_other_users_post()
{
$user1 = User::factory()->create();
$user2 = User::factory()->create();
$post = Post::factory()->create(['user_id' => $user1->id]);
$response = $this->actingAs($user2)
->put(route('posts.update', $post), [
'title' => 'Updated Title'
]);
$response->assertForbidden();
}
public function test_admin_can_delete_any_post()
{
$admin = User::factory()->create(['is_admin' => true]);
$post = Post::factory()->create([
'user_id' => User::factory()->create()->id
]);
$response = $this->actingAs($admin)
->delete(route('posts.destroy', $post));
$response->assertRedirect(route('posts.index'));
$this->assertSoftDeleted($post);
}
}
Gates are closure-based and best for actions not tied to specific models (like "view-admin-dashboard"). Policies are class-based and organize authorization logic around specific models (like PostPolicy for Post model actions).
Use Gates for general abilities that don't relate to specific models. Use Policies when you have model-specific authorization logic, especially for CRUD operations.
Laravel automatically discovers policies if they follow naming conventions (Post model → PostPolicy) and are in the default namespace. You can also manually register policies in AuthServiceProvider.
Gate::before runs before all other gate checks - useful for super admin privileges. Gate::after runs after all checks - useful for logging or final overrides.
Use Laravel's testing helpers like $user->can('ability'), $this->authorize(), and test for expected HTTP status codes (403 for forbidden, 200 for successful authorization).
Yes, they work together seamlessly. Policies are actually implemented using Gates under the hood. You can mix and match based on what makes sense for your application.
// Eager load relationships to avoid N+1 in policies
public function view(User $user, Post $post): bool
{
// Bad - causes N+1 if checking multiple posts
// return $post->user_id === $user->id;
// Good - if posts are loaded with user relationship
return $post->user->id === $user->id;
}
// Use caching for expensive authorization checks
public function canAccessPremiumFeature(User $user): bool
{
return Cache::remember(
"user_{$user->id}_premium_access",
3600, // 1 hour
fn() => $this->checkPremiumAccess($user)
);
}
You've now mastered Laravel authorization with Gates and Policies! In our next post, we'll explore Laravel CSRF Protection: What It Is and How Laravel Handles It For You to understand web application security.