skip to content

In Laravel, what URIs and route names does Route::resource('books.copies', BookCopyController::class) register, and what does ->shallow() change?

level: middleimportance: should knowfreq 48%

answer

  1. dot notation nests resources
  2. /books/{book}/copies/{copy}
  3. names like books.copies.show
  4. shallow: members drop the parent
  5. make:controller --parent=Book

basics

~10 s

Dot notation nests the resource: /books/{book}/copies, /books/{book}/copies/{copy} and so on, named books.copies.index and similar. ->shallow() keeps the parent only on index, create and store; show, edit, update and destroy become /copies/{copy} named copies.show.

solid answer

~30 s

`Route::resource('books.copies', BookCopyController::class)` nests the copies resource under a book: `GET /books/{book}/copies`, `GET /books/{book}/copies/create`, `POST /books/{book}/copies`, and the member routes `/books/{book}/copies/{copy}` (plus `/edit`), named `books.copies.index` through `books.copies.destroy`. Actions receive both parameters, e.g. `show(Book $book, BookCopy $copy)`. Adding `->shallow()` keeps the parent only where it is needed, on the collection routes `index`, `create` and `store`; `show`, `edit`, `update` and `destroy` become `/copies/{copy}` named `copies.show` and so on, because a copy's id already identifies it. `make:controller BookCopyController --parent=Book --model=BookCopy --resource` stubs the nested signatures.

code

php · 21 lines
php
<?php

namespace App\Http\Controllers;

use App\Models\Book;
use App\Models\BookCopy;
use Illuminate\View\View;

class BookCopyController extends Controller
{
    public function index(Book $book): View
    {
        return view('copies.index', ['book' => $book, 'copies' => $book->copies]);
    }

    // with ->shallow(), member actions take only the child
    public function show(BookCopy $copy): View
    {
        return view('copies.show', ['copy' => $copy, 'book' => $copy->book]);
    }
}

go deeper

for a junior

Recognise dot notation for nested resources and read the resulting URIs.

for a middle

List which routes keep the parent under shallow nesting, how names change, and how parameter names drive binding.

for a senior

Choose between scoped full nesting and shallow members based on URL design and ownership checks, and limit nesting depth.

for a principal

Set URL design guidelines for parent-child resources so public URLs stay stable and ownership checks are uniform.

## Nested resources Some records only make sense inside a parent: a library holds several physical **copies** of each book. Laravel expresses that with **dot notation** in the resource name: ```php Route::resource('books.copies', BookCopyController::class); ``` The registrar builds a URI segment and a parameter for each part of the name, singularising each (`books` → `{book}`, `copies` → `{copy}`). | Verb | URI | Action | Name | |---|---|---|---| | GET | `/books/{book}/copies` | `index` | `books.copies.index` | | GET | `/books/{book}/copies/create` | `create` | `books.copies.create` | | POST | `/books/{book}/copies` | `store` | `books.copies.store` | | GET | `/books/{book}/copies/{copy}` | `show` | `books.copies.show` | | GET | `/books/{book}/copies/{copy}/edit` | `edit` | `books.copies.edit` | | PUT/PATCH | `/books/{book}/copies/{copy}` | `update` | `books.copies.update` | | DELETE | `/books/{book}/copies/{copy}` | `destroy` | `books.copies.destroy` | Controller actions take the parent first: `show(Book $book, BookCopy $copy)`. The parameter is named `{copy}`, not after the model class, so the method's variable must be `$copy` for implicit binding to match. ## Shallow nesting On member routes the parent is often redundant: copy `99` is copy `99` whichever book you name. `->shallow()` removes it from the routes that address one existing child: ```php Route::resource('books.copies', BookCopyController::class)->shallow(); ``` | Action | Without `shallow()` | With `shallow()` | |---|---|---| | `index` | `/books/{book}/copies` | `/books/{book}/copies` | | `create` | `/books/{book}/copies/create` | `/books/{book}/copies/create` | | `store` | `/books/{book}/copies` | `/books/{book}/copies` | | `show` | `/books/{book}/copies/{copy}` | `/copies/{copy}` (`copies.show`) | | `edit` | `/books/{book}/copies/{copy}/edit` | `/copies/{copy}/edit` (`copies.edit`) | | `update` | `/books/{book}/copies/{copy}` | `/copies/{copy}` (`copies.update`) | | `destroy` | `/books/{book}/copies/{copy}` | `/copies/{copy}` (`copies.destroy`) | The collection routes keep the parent because they need it: you list or create copies **of a book**. ## Trade-offs - **Shorter URLs and names** for member routes, and no redundant parent lookup. - **Member actions lose the parent parameter**, so `show(BookCopy $copy)` must reach the book through a relationship when it needs it. - **Deep nesting** (`libraries.books.copies`) produces long URIs; one level, plus shallow members, is the usual advice. - **Unscoped by default**: `/books/1/copies/99` still resolves copy 99 even if it belongs to book 2, unless you enable scoping with `scoped()`. ## Generating a nested controller ```bash php artisan make:controller BookCopyController --parent=Book --model=BookCopy --resource ``` `--parent` selects the nested stub, where each action type-hints the parent model and, on member actions, the child. It offers to create either model if it does not exist. ## Checklist when nesting 1. Nest only when the child cannot exist without the parent. 2. Keep to one level; reach deeper data through the child's own routes. 3. Decide between **scoped** full nesting and **shallow** member routes, and apply it consistently. 4. Check the generated names with `php artisan route:list --name=copies` before linking to them in views. ## Common mistakes - **Believing the URL proves ownership.** A nested URI looks hierarchical, but binding treats each parameter separately until scoping is switched on. - **Mixing shallow and non-shallow names** in views: after adding `shallow()`, links that still use `books.copies.show` point at a route name that no longer exists. - **Nesting two or three levels deep** because the database is shaped that way; URLs become long, controllers receive many parameters, and every action must keep them consistent. - **Naming the variable after the model class** (`$bookCopy`) instead of the placeholder (`$copy`), which leaves it unbound. ## Why nest at all Nesting earns its place when the parent gives the child its meaning: listing the copies of one book, or adding a copy to a particular book. Those collection actions genuinely need the parent in the URL, which is exactly the part shallow nesting keeps. For everything else the child's own identity is enough, so many teams default to shallow nesting and switch to fully scoped nesting only when the URL itself should express the hierarchy.

  • Why is the child parameter named {copy} and what must the action variable be called?
    The registrar singularises each segment of the resource name, so `copies` becomes `{copy}` regardless of the model class. Implicit binding matches by name, so the action needs `BookCopy $copy`; naming it `$bookCopy` leaves it unbound, and the container injects an empty `BookCopy` instead. `parameters(['copies' => 'book_copy'])` renames the wildcard if you prefer.
  • Does full nesting guarantee that copy 99 belongs to book 1 in /books/1/copies/99?
    No. By default each parameter is bound independently, so copy 99 loads even if it belongs to another book. Calling `->scoped()` on the nested resource makes Laravel resolve the child through the parent's `copies` relationship and return 404 when it does not belong.

saying these in an interview costs you the question

  • Nested resources automatically check that the child belongs to the parent.
  • shallow() removes the parent from every route, including index and store.
  • The nested parameter is named after the model class, {bookCopy}.
  • Shallow member routes keep the books.copies.show name.
  • Laravel does not support nesting resources; you must write each route by hand.