0

Resource controllers and Route::resource()

Intermediate5 min read·lv-06-006

Concept

Route::resource() generates seven conventional routes for a CRUD resource in a single line, eliminating the boilerplate of registering index, create, store, show, edit, update, and destroy individually. This follows the REST convention that HTTP verbs combined with URI patterns express intent: GET /users lists, POST /users creates, GET /users/{user} shows, and so on.

ActionVerbURIController MethodRoute Name
ListGET/usersindexusers.index
Create formGET/users/createcreateusers.create
StorePOST/usersstoreusers.store
ShowGET/users/{user}showusers.show
Edit formGET/users/{user}/editeditusers.edit
UpdatePUT/PATCH/users/{user}updateusers.update
DestroyDELETE/users/{user}destroyusers.destroy

Internally, Router::resource() creates a Illuminate\Routing\ResourceRegistrar instance and calls register(). The registrar maps each action to a route via addResourceIndex(), addResourceCreate(), etc. These methods call the underlying Route::get/post/put/patch/delete methods just as you would manually, injecting the standard parameter name (singularized resource name) into each parameterized route.

Limiting generated routes is done with ->only(['index', 'show']) or ->except(['create', 'edit']) for API resources that don't serve HTML forms. Naming overrides use ->names(['index' => 'users.list']). Parameter naming overrides use ->parameters(['users' => 'account']) to change {user} to {account}.

Nested resources — Route::resource('users.posts', PostController::class) — generate routes with both {user} and {post} parameters, producing URIs like /users/{user}/posts/{post}. Shallow nesting is preferred for deeply nested resources: ->shallow() generates full nested URIs for collection operations (index, create, store) but flat URIs for single-resource operations (show, edit, update, destroy).

Code Example

php
<?php

use App\Http\Controllers\UserController;
use App\Http\Controllers\PostController;
use Illuminate\Support\Facades\Route;

// Full resource — all 7 routes
Route::resource('users', UserController::class);

// Limit to specific actions
Route::resource('users', UserController::class)->only(['index', 'show']);
Route::resource('users', UserController::class)->except(['create', 'edit']);

// Override parameter name: /users/{account} instead of /users/{user}
Route::resource('users', UserController::class)
    ->parameters(['users' => 'account']);

// Override generated names
Route::resource('users', UserController::class)
    ->names(['index' => 'users.list', 'show' => 'users.detail']);

// Nested resource: /users/{user}/posts/{post}
Route::resource('users.posts', PostController::class);

// Shallow nesting — flat URIs for single-resource operations
Route::resource('users.posts', PostController::class)->shallow();
// GET /users/{user}/posts         => posts.index
// POST /users/{user}/posts        => posts.store
// GET /posts/{post}               => posts.show
// PUT /posts/{post}               => posts.update
// DELETE /posts/{post}            => posts.destroy

// Inside a group with name prefix
Route::prefix('/admin')->name('admin.')->group(function () {
    Route::resource('users', UserController::class);
    // Produces: admin.users.index, admin.users.show, etc.
});

// The corresponding controller
class UserController extends Controller
{
    public function index(): View { /* list all users */ }
    public function create(): View { /* show create form */ }
    public function store(Request $request): RedirectResponse { /* persist */ }
    public function show(User $user): View { /* show one user */ }
    public function edit(User $user): View { /* show edit form */ }
    public function update(Request $request, User $user): RedirectResponse { /* update */ }
    public function destroy(User $user): RedirectResponse { /* delete */ }
}

Interview Q&A

Q: Why does Route::resource() register both PUT and PATCH for the update action, and what is the semantic difference between them?

Route::resource() registers update for both PUT and PATCH because browsers can only submit GET and POST forms natively. Laravel uses a hidden _method field (method spoofing) so a POST with _method=PUT or _method=PATCH is treated as the corresponding verb. The semantic distinction: PUT means replace the entire resource (you must send all fields), PATCH means partial update (send only changed fields). Laravel's resource controller doesn't enforce this semantically — the controller method handles both verbs — but the distinction matters for API clients that send proper HTTP requests.


Q: What does ->shallow() do and when should you use it?

->shallow() generates full nested URIs for collection-level routes — where you need the parent context to create or list children (GET /users/{user}/posts, POST /users/{user}/posts) — but flat URIs for member-level routes, because the child's own ID is sufficient to locate it (GET /posts/{post}, DELETE /posts/{post}). Use it when your child resource has a globally unique ID (auto-increment or UUID), which is the common case. Deep nesting like /users/{user}/posts/{post}/comments/{comment} becomes unwieldy and breaks REST conventions; shallow nesting solves this ergonomically.


Q: How do you add a non-standard route to a resource controller beyond the seven defaults?

Register the extra route before Route::resource() so it takes precedence over the resource's parameterized routes. For example, to add GET /users/export, place Route::get('/users/export', [UserController::class, 'export'])->name('users.export') before Route::resource('users', UserController::class). If placed after, /users/{user} with user=export would match first and attempt to bind export as a model key. For many extra routes, consider breaking the resource up or using apiResource() combined with a separate route group.