Laravel Controllers Reference Guide

A Laravel controllers reference guide: organising route logic into controller classes, resource controllers, and route model binding.

Basic controllers

A controller groups related request-handling logic into a single class instead of stacking closures in the routes file. Generate one with Artisan:

php artisan make:controller PostController
namespace App\Http\Controllers;

class PostController extends Controller
{
    public function index()
    {
        return view('posts.index', [
            'posts' => Post::latest()->get(),
        ]);
    }

    public function show(Post $post)
    {
        return view('posts.show', compact('post'));
    }
}
use App\Http\Controllers\PostController;

Route::get('/posts', [PostController::class, 'index']);
Route::get('/posts/{post}', [PostController::class, 'show']);

Route model binding

When a route or controller method's type-hinted parameter name matches a route segment, Laravel automatically resolves the model instance (or throws a 404 if it isn't found) — no manual findOrFail() needed. See the routing guide for more on how route parameters and constraints work.

Route::get('/posts/{post}', [PostController::class, 'show']);

public function show(Post $post)
{
    return view('posts.show', compact('post'));
}

// Bind by a different column, e.g. a slug
Route::get('/posts/{post:slug}', [PostController::class, 'show']);

Single action (invokable) controllers

For a controller that only ever does one thing, define __invoke() instead of a named method, and omit the method when registering the route.

php artisan make:controller ProvisionServer --invokable
class ProvisionServer extends Controller
{
    public function __invoke(Request $request)
    {
        // ...
    }
}

Route::post('/servers', ProvisionServer::class);

Resource controllers

Generate a controller with stub methods for the seven conventional CRUD actions:

php artisan make:controller PostController --resource
php artisan make:controller PostController --resource --model=Post
Route::resource('posts', PostController::class);

// Only some of the seven actions
Route::resource('posts', PostController::class)->only(['index', 'show']);
Route::resource('posts', PostController::class)->except(['destroy']);

// A read-only, JSON-only version for an API
Route::apiResource('posts', PostController::class);

See the API reference for more on apiResource and building JSON resource controllers.

// Nested resource — /posts/{post}/comments/{comment}
Route::resource('posts.comments', CommentController::class);

// Shallow nesting — the child's show/edit/update/destroy drop the parent
// segment once you already have the child's own ID: /comments/{comment}
Route::resource('posts.comments', CommentController::class)->shallow();

Full nesting keeps every URI scoped under its parent (useful for confirming a comment actually belongs to that post before it's even queried), but the URIs get long fast for anything beyond one level. shallow() is the usual compromise: the collection routes (index, create, store) stay nested under the parent, while routes that already identify a single child by its own ID drop the parent segment.

VerbURIActionRoute name
GET/postsindexposts.index
GET/posts/createcreateposts.create
POST/postsstoreposts.store
GET/posts/{post}showposts.show
GET/posts/{post}/editeditposts.edit
PUT/PATCH/posts/{post}updateposts.update
DELETE/posts/{post}destroyposts.destroy

These same seven action names line up with policy methods, letting authorizeResource() authorize every action in one line instead of calling $this->authorize() in each method.

Dependency injection

The service container automatically resolves type-hinted class dependencies in both constructors and individual methods (in addition to any route-bound models).

class PostController extends Controller
{
    public function __construct(
        protected PostRepository $posts,
    ) {}

    public function store(StorePostRequest $request)
    {
        $post = $this->posts->create($request->validated());

        return redirect()->route('posts.show', $post);
    }
}

StorePostRequest here is a Form Request — see the validation rules guide for how these keep validation logic out of the controller.

Middleware

Attach middleware to a controller's routes either in the route definition or, in Laravel 11+, via a static middleware() method on the controller itself.

Route::get('/dashboard', [DashboardController::class, 'index'])
    ->middleware('auth');

class PostController extends Controller implements HasMiddleware
{
    public static function middleware(): array
    {
        return [
            'auth',
            new Middleware('log', only: ['store']),
        ];
    }
}

Returning different response types

// Redirect with flashed session data, readable via session('status') on the next request
return redirect()->route('posts.index')->with('status', 'Post created.');

// JSON, with an explicit status code
return response()->json(['error' => 'Not found.'], 404);

// A file download, or serving a file inline
return response()->download(storage_path('app/report.pdf'));
return response()->file(storage_path('app/report.pdf'));

Returning a plain array or Eloquent model/collection from a controller method is automatically converted to a JSON response — there's rarely a need to wrap it in response()->json() explicitly unless a specific status code or header is required.