Controller base Laravel 12: da AuthorizesRequests e ValidatesRequests impliciti a Form Request, Gate e composizione esplicita

Controller base Laravel 12: da AuthorizesRequests e ValidatesRequests impliciti a Form Request, Gate e composizione esplicita

In un progetto di lead generation per il mercato europeo, l'aggiornamento da Laravel 10 a Laravel 12 ha prodotto 47 errori Call to undefined method in 23 controller. Tutti chiamavano $this->authorize() o $this->validate() - metodi forniti dai trait AuthorizesRequests e ValidatesRequests che in Laravel 9/10 erano inclusi nella classe base Controller. Taylor Otwell ha rimosso entrambi nel PR #6188 ("Slim skeleton"), con una motivazione diretta nel PR #6172: "Validation using $this->validate in controllers has not been documented in some time. $this->authorize can simply be Gate::authorize." Il principio sottostante è quello che il Gang of Four ha formalizzato nel 1995: "Favor object composition over class inheritance" - le dipendenze implicite ereditate nascondono accoppiamento, le dipendenze esplicite composte lo rendono visibile.

Cosa è cambiato nella classe base Controller tra Laravel 10 e Laravel 12?

Le release notes di Laravel 11 documentano il cambiamento: "The base controller included in new Laravel applications has been simplified. It no longer extends Laravel's internal Controller class, and the AuthorizesRequests and ValidatesRequests traits have been removed." La timeline delle rimozioni è progressiva: DispatchesJobs è stato rimosso in Laravel 10 (commit f62d260, gennaio 2023), AuthorizesRequests e ValidatesRequests in Laravel 11 (PR #6188, novembre 2023). Il Controller base è passato da classe con 3 trait e un'estensione di Illuminate\Routing\Controller a una classe astratta vuota:

/* Laravel 9/10 - Controller con trait impliciti */
namespace App\Http\Controllers;

use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;

abstract class Controller extends BaseController
{
    use AuthorizesRequests, ValidatesRequests;
    ## Ogni controller figlio eredita $this->authorize() e $this->validate()
    ## anche se non li usa mai - accoppiamento implicito
}

/* Laravel 11/12 - Controller minimale */
namespace App\Http\Controllers;

abstract class Controller
{
    ## Nessun trait, nessuna estensione - composizione esplicita
}

/* PRIMA: controller L10 con validate/authorize impliciti */
class PostController extends Controller
{
    public function store(Request $request): JsonResponse
    {
        $this->authorize('create', Post::class);
        $data = $this->validate($request, [
            'title' => 'required|string|max:255|unique:posts',
            'body'  => 'required|string',
        ]);
        $post = $request->user()->posts()->create($data);
        return PostResource::make($post)->response()->setStatusCode(201);
    }
}

/* DOPO: controller L12 con Form Request + Gate espliciti */
class PostController extends Controller
{
    public function store(StorePostRequest $request): JsonResponse
    {
        ## Validazione e autorizzazione già eseguite dalla Form Request
        ## Il controller orchestra, non valida né autorizza
        $post = $request->user()->posts()->create($request->validated());
        return PostResource::make($post)->response()->setStatusCode(201);
    }
}

/* app/Http/Requests/StorePostRequest.php */
class StorePostRequest extends FormRequest
{
    public function authorize(): bool
    {
        return $this->user()->can('create', Post::class);
    }

    public function rules(): array
    {
        return [
            'title' => 'required|string|max:255|unique:posts',
            'body'  => 'required|string',
        ];
    }
}

Il controller "dopo" non ha bisogno di alcun trait. La Form Request, introdotta in Laravel 5.0 (febbraio 2015), gestisce sia validazione che autorizzazione in una classe dedicata, iniettata automaticamente dal Service Container. Se la validazione fallisce, Laravel restituisce automaticamente HTTP 422; se l'autorizzazione fallisce, HTTP 403 - senza codice nel controller.

Come migrare i controller esistenti da Laravel 10 a Laravel 12?

La guida di upgrade di Laravel 11 non impone un approccio specifico - il vecchio Controller con trait funziona ancora se lo mantieni nel tuo progetto. Ma ci sono tre strategie con trade-off diversi. La prima (quick fix) è riaggungere i trait al Controller base: ripristina il funzionamento, ma rinuncia al beneficio della composizione esplicita. La seconda è aggiungere i trait solo ai controller che li usano: rende visibili le dipendenze, ma è transitoria. La terza (best practice) è sostituire $this->validate() con Form Request e $this->authorize() con Gate::authorize(): elimina la necessità dei trait. La documentazione ufficiale sull'autorizzazione chiarisce che $this->authorize() e Gate::authorize() sono funzionalmente identici - il trait è un convenience wrapper.

Per controller con una singola azione complessa, i Single Action Controller con __invoke() si combinano naturalmente con il Controller base vuoto - ogni controller è un'unità autocontenuta senza bagaglio ereditato. Come discusso nell'articolo sul Service Layer, il controller ideale in Laravel 12 orchestra: delega la validazione alla Form Request, l'autorizzazione alla Policy/Gate, la logica di business al servizio. Rimangono 3-5 righe di codice.

Errori comuni nella migrazione dei controller Laravel

Il primo errore è riaggungere i trait al Controller base per default senza valutare se servono. In un'applicazione con 40 controller, tipicamente solo 10-15 usano $this->authorize() e $this->validate(). Reimportare i trait nella classe base per 15 controller che li usano significa accoppiarli implicitamente anche nei 25 che non li usano - esattamente il problema che Laravel 11 ha risolto.

Il secondo è non cercare le chiamate ai trait prima dell'upgrade. Un grep -r '$this->authorize\|$this->validate' app/Http/Controllers/ identifica tutti i punti di impatto in secondi. Senza questa ricerca, gli errori emergono in produzione - tipicamente nelle route meno testate.

Il terzo è ignorare le Form Request e continuare a validare inline nei controller. La validazione inline ($request->validate([...]), disponibile sull'oggetto Request senza trait) funziona, ma sparpaglia le regole di validazione nei controller invece di centralizzarle in classi dedicate. In un gestionale con 50 endpoint, le stesse regole di validazione per un campo "codice_fiscale" appaiono in 8 controller diversi. Una Form Request condivisa elimina la duplicazione.

Il quarto è non convertire $this->authorize() in Gate::authorize() quando si rimuovono i trait. La differenza è solo sintattica - entrambi lanciano AuthorizationException (convertita in HTTP 403) - ma Gate::authorize() rende esplicita la dipendenza dal sistema di autorizzazione. Robert C. Martin definisce le dipendenze esplicite come prerequisito della Clean Architecture: "Source code dependencies can only point inwards." Un controller che dipende implicitamente da un trait ereditato viola questa regola; un controller che importa esplicitamente Gate la rispetta.

La migrazione dei controller è il complemento naturale della ristrutturazione dello skeleton applicativo da Laravel 10 a 12 - entrambi seguono la stessa filosofia di Taylor Otwell: "take the skeleton back down to a slimmer foundation." I middleware completano il quadro gestendo le responsabilità cross-cutting che non appartengono né al controller né alla Form Request. Per conoscere il mio approccio al refactoring architetturale Laravel, visita la mia pagina professionale. Se i tuoi controller ereditano trait che non usano e la validazione è duplicata in decine di endpoint, contattami per una consulenza dedicata - partiamo dall'inventario delle dipendenze implicite e dalla migrazione verso composizione esplicita.

Ultima modifica: