Especialização em Backend com PHP & Laravel • Laravel 11 & PHP 8.3+ • Eloquent ORM, Sanctum, Filas, Horizon e APIs Corporativas


🗺️ Mapa Conceitual do Tópico

flowchart TD
    A["Cliente HTTP"] --> B["routes/api.php"]
    B --> C["Route::prefix('v1')->group()"]
    C --> D["Route::apiResource() ou verbo explícito"]
    D --> E["Route Model Binding (implícito/customizado)"]
    E --> F["Controller recebe Model já resolvido"]
    F --> G["404 automático se não encontrado"]

    style A fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
    style B fill:#fff3e0,stroke:#ff9800,stroke-width:2px
    style C fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
    style D fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
    style E fill:#fce4ec,stroke:#e91e63,stroke-width:2px
    style F fill:#f3e5f5,stroke:#9c27b0,stroke-width:2px

🏛️ 1. Roteamento RESTful: apiResource vs. Rotas Explícitas

O padrão REST define sete ações canônicas por recurso. Em APIs, duas delas (create e edit, que servem formulários HTML) não fazem sentido, então o Laravel oferece Route::apiResource() — que registra apenas as cinco rotas relevantes (index, store, show, update, destroy) em uma única linha:

<?php
// routes/api.php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Api\V1\ProdutoController;

Route::prefix('v1')->middleware('auth:sanctum')->group(function () {
    Route::apiResource('produtos', ProdutoController::class);
});

Isso equivale a declarar manualmente GET /produtos, POST /produtos, GET /produtos/{produto}, PUT|PATCH /produtos/{produto} e DELETE /produtos/{produto}, todas mapeadas por convenção de nome de método no controller. O encadeamento fluente (prefix→middleware→group) evita duplicar versionamento e segurança em cada rota individualmente — um ponto central quando a API cresce para dezenas de recursos.


🧩 2. Route Model Binding Implícito

Sem binding, todo método de controller começaria com um Model::findOrFail($id) repetitivo. O Laravel resolve isso via reflexão de tipos: se o parâmetro da rota ({produto}) tem o mesmo nome de uma variável type-hintada com um Eloquent Model, o framework injeta a instância já consultada — ou lança ModelNotFoundException (convertida em 404 HTTP automaticamente) se o registro não existir:

<?php
namespace App\Http\Controllers\Api\V1;

use App\Models\Produto;
use Illuminate\Http\JsonResponse;

class ProdutoController extends Controller
{
    public function show(Produto $produto): JsonResponse
    {
        return response()->json($produto); // $produto já foi buscado pelo ID da URL
    }
}

Binding por campo customizado

Quando a chave primária numérica não deve vazar na URL (SEO, segurança por obscuridade), o binding pode ser direcionado a outra coluna diretamente na definição da rota, usando a sintaxe {parametro:coluna}:

Route::get('/produtos/{produto:slug}', function (Produto $produto) {
    return response()->json($produto);
});

Isso gera a query WHERE slug = ? em vez de WHERE id = ?, sem nenhuma alteração no controller.


🔐 3. Scoped Bindings em Rotas Aninhadas

Em rotas aninhadas (users/{user}/posts/{post}), o binding implícito por padrão resolve {post} de forma independente — permitindo, por erro de URL, acessar um post que pertence a outro usuário. O método scoped() resolve isso automaticamente, adicionando a restrição de relacionamento na query com base na convenção de nome do método de relacionamento no model pai:

Route::apiResource('users.posts', PostController::class)->scoped([
    'post' => 'slug',
]);

Com isso, o Laravel garante em uma única chamada que o post resolvido realmente pertence ao user da URL (via $user->posts()), evitando uma checagem manual de autorização redundante — e ainda permite customizar a coluna de binding (slug) por parâmetro.