Laravel Tutorial · Laravel Basics · Lesson 5 of 27

Laravel Controllers

Move request handling out of route files into controller classes, use single-action and resource controllers, and inject dependencies.

Create a free account to save this page, take notes and track your progress. Sign in

Why controllers?

Closures in route files are fine for tiny apps, but real handlers grow. A controller groups related actions in a class:

php artisan make:controller PostController --resource

Route to a controller method with an array: Route::get('/posts', [PostController::class, 'index']).

Resource controllers follow a convention for CRUD: index, create, store, show, edit, update, destroy, and Route::resource('posts', PostController::class) registers all seven routes. For JSON APIs, Route::apiResource skips the HTML form routes (create, edit).

Dependency injection: type-hint services in the constructor or method parameters and Laravel's service container creates them for you.

A resource controller with an injected service
<?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

class PriceFormatter
{
    public function format(int $cents): string
    {
        return '$'.number_format($cents / 100, 2);
    }
}

class ProductController
{
    private array $products = [1 => ['name' => 'Mug', 'cents' => 1200], 2 => ['name' => 'Tea', 'cents' => 650]];

    public function __construct(private PriceFormatter $prices) {}   // injected by the container

    public function index()
    {
        return array_map(fn ($p) => $p['name'].' '.$this->prices->format($p['cents']), array_values($this->products));
    }

    public function show(int $id)
    {
        abort_unless(isset($this->products[$id]), 404, 'Product not found');
        return $this->products[$id];
    }

    public function store(Request $request)
    {
        return response()->json(['created' => $request->input('name')], 201);
    }
}

Route::apiResource('products', ProductController::class)->only(['index', 'show', 'store']);

visit('GET', '/products');
visit('GET', '/products/2');
visit('GET', '/products/9');
visit('POST', '/products', ['name' => 'Kettle']);
foreach (app('router')->getRoutes() as $route) {
    if (! str_starts_with($route->uri(), 'products')) continue;   // skip the framework's own routes
    echo str_pad(implode('|', $route->methods()), 10), ' ', str_pad($route->uri(), 18), ' ', $route->getName(), "\n";
}

Output

GET /products -> 200 ["Mug $12.00","Tea $6.50"]
GET /products/2 -> 200 {"name":"Tea","cents":650}
GET /products/9 -> 404 {
    "message": "Product not found"
}
POST /products -> 201 {"created":"Kettle"}
GET|HEAD   products           products.index
POST       products           products.store
GET|HEAD   products/{product} products.show

Run this on your computer — see “Get Started” for setup.

A single-action controller has just an __invoke method: Route::post('/reports', GenerateReport::class).

Test yourself

Which method does Route::apiResource NOT register?

  1. index
  2. store
  3. create
  4. destroy
Show answer

Answer: create

Was this page helpful?