Vai al contenuto principale

Confronto con Laravel 10 e precedenti

Con Laravel 11 la struttura dell’applicazione è stata rinnovata in modo profondo, sotto il nome di “Slim Application Skeleton”. Il cambiamento più grande è la fine della dispersione della configurazione: tutto confluisce in un unico bootstrap/app.php.
ElementoPrima di Laravel 10Da Laravel 11
Kernel HTTPapp/Http/Kernel.phpRimosso (integrato nel framework)
Kernel Consoleapp/Console/Kernel.phpRimosso (spostato in routes/console.php)
Exception handlerapp/Exceptions/Handler.phpRimosso (concentrato in bootstrap/app.php)
Service provider5 fileUn solo AppServiceProvider.php
File delle rotteweb.php / api.php di defaultSolo web.php di default, api.php opt-in
BootstrapConfigurazione sparsa su più fileConcentrata in bootstrap/app.php
DB di defaultMySQL/PostgreSQLSQLite
Il cambiamento riguarda i nuovi progetti. L’aggiornamento di app Laravel 10 esistenti mantiene funzionante la vecchia struttura.

Nuova struttura di directory e file

Struttura dello skeleton

laravel-app/
├── app/
│   ├── Http/
│   │   └── Controllers/
│   ├── Models/
│   │   └── User.php
│   └── Providers/
│       └── AppServiceProvider.php
├── bootstrap/
│   ├── app.php          ← centro della configurazione dell'applicazione
│   ├── cache/
│   └── providers.php    ← elenco dei service provider
├── config/
├── database/
├── public/
│   └── index.php        ← entry point
├── resources/
├── routes/
│   ├── web.php          ← rotte web (default)
│   └── console.php      ← comandi Artisan e schedule
├── storage/
└── tests/

bootstrap/app.php — il centro della configurazione

<?php

use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware): void {
        //
    })
    ->withExceptions(function (Exceptions $exceptions): void {
        //
    })->create();
In questo unico file configuri routing, middleware e gestione delle eccezioni. Ciò che in Laravel 10 era diviso tra app/Http/Kernel.php, app/Console/Kernel.php e app/Exceptions/Handler.php è ora concentrato qui.

bootstrap/providers.php — elenco dei service provider

<?php

use App\Providers\AppServiceProvider;

return [
    AppServiceProvider::class,
];
Questo è il file dove si registrano i service provider. In Laravel 10 lo indicavi nell’array providers di config/app.php: ora è stato spostato in bootstrap/providers.php. Nel default di Laravel 11 c’è solo AppServiceProvider.
Installando pacchetti con composer require, il pacchetto può aggiornare automaticamente bootstrap/providers.php. config/app.php non è ignorato, ma per nuove registrazioni il posto consigliato è ora bootstrap/providers.php.

Cambiamenti nella directory routes/

routes/
├── web.php      ← rotte web (caricate di default)
└── console.php  ← comandi Artisan e schedule
api.php e channels.php non ci sono di default. Li generi al bisogno con comandi Artisan.
# aggiunge le rotte API (api.php + installa Sanctum)
php artisan install:api

# aggiunge il broadcasting (channels.php + installazione di Reverb, ecc.)
php artisan install:broadcasting
In routes/console.php definisci anche lo schedule.
<?php

use Illuminate\Foundation\Inspiring;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Schedule;

Artisan::command('inspire', function () {
    $this->comment(Inspiring::quote());
})->purpose('Display an inspiring quote');

Schedule::command('emails:send')->daily();

File rimossi

Il Kernel HTTP è stato integrato in Illuminate\Foundation\Http\Kernel. La personalizzazione dei middleware si fa in bootstrap/app.php con withMiddleware().
// Laravel 10 e precedenti: app/Http/Kernel.php
protected $middleware = [
    \Illuminate\Http\Middleware\TrustProxies::class,
    // ...
];

protected $middlewareGroups = [
    'web' => [
        \App\Http\Middleware\EncryptCookies::class,
        // ...
    ],
];
// da Laravel 11: bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
    $middleware->web(append: [
        EnsureUserIsSubscribed::class,
    ]);

    $middleware->validateCsrfTokens(except: ['stripe/*']);
})
I due ruoli del Kernel della console sono stati separati. I comandi Artisan vanno in app/Console/Commands/ (auto-scoperti), lo schedule si scrive in routes/console.php.
// Laravel 10 e precedenti: app/Console/Kernel.php
protected function schedule(Schedule $schedule): void
{
    $schedule->command('emails:send')->daily();
}
// da Laravel 11: routes/console.php
use Illuminate\Support\Facades\Schedule;

Schedule::command('emails:send')->daily();
L’exception handler è stato integrato in Illuminate\Foundation\Exceptions\Handler. La personalizzazione si fa in bootstrap/app.php con withExceptions().
// Laravel 10 e precedenti: app/Exceptions/Handler.php
public function register(): void
{
    $this->reportable(function (InvalidOrderException $e) {
        // ...
    });
}
// da Laravel 11: bootstrap/app.php
->withExceptions(function (Exceptions $exceptions) {
    $exceptions->report(function (InvalidOrderException $e) {
        // ...
    });
})

Come funziona Application::configure()

Implementazione interna nel framework

Application::configure() è un metodo statico di Illuminate\Foundation\Application.
// da Illuminate\Foundation\Application
public static function configure(?string $basePath = null)
{
    $basePath = match (true) {
        is_string($basePath) => $basePath,
        default => static::inferBasePath(),
    };

    return (new Configuration\ApplicationBuilder(new static($basePath)))
        ->withKernels()
        ->withEvents()
        ->withCommands()
        ->withProviders();
}
Il metodo esegue questi passaggi.
  1. Determina la root dell’app da basePath
  2. Crea l’istanza Application
  3. La avvolge con ApplicationBuilder applicando le configurazioni di default
  4. Restituisce l’istanza ApplicationBuilder
Un dettaglio importante: withKernels() / withEvents() / withCommands() / withProviders() sono già stati chiamati dentro configure(). Non serve richiamarli in bootstrap/app.php.

Flusso di configurazione tramite method chain

Application::configure(basePath: dirname(__DIR__))  // restituisce ApplicationBuilder
    ->withRouting(...)       // configura il routing e restituisce $this
    ->withMiddleware(...)    // configura i middleware e restituisce $this
    ->withExceptions(...)    // configura le eccezioni e restituisce $this
    ->create();              // restituisce l'istanza Application
Con la chiamata a create() viene estratta l’istanza Application dal ApplicationBuilder: è questa istanza che bootstrap/app.php restituisce.

Dal ricevimento della richiesta all’avvio dell’app

public/index.php è l’entry point: legge bootstrap/app.php e ottiene l’Application. Poi il Kernel HTTP fa passare la richiesta nella pipeline dei middleware e il router la dispatcha al controller.

Approfondimento sui metodi principali di ApplicationBuilder

withRouting() — processo interno di registrazione delle rotte

public function withRouting(
    ?Closure $using = null,
    array|string|null $web = null,
    array|string|null $api = null,
    ?string $commands = null,
    ?string $channels = null,
    ?string $pages = null,
    ?string $health = null,
    string $apiPrefix = 'api',
    ?callable $then = null
)
Internamente registra una callback in AppRouteServiceProvider::loadRoutesUsing() e registra AppRouteServiceProvider al booting dell’app.
// interno di withRouting() (versione semplificata)
protected function buildRoutingCallback(...)
{
    return function () use ($web, $api, $pages, $health, $apiPrefix, $then) {
        if (is_string($api) || is_array($api)) {
            Route::middleware('api')->prefix($apiPrefix)->group($api);
        }

        if (is_string($health)) {
            Route::get($health, function () {
                Event::dispatch(new DiagnosingHealth);
                return response(View::file(...), status: 200);
            });
        }

        if (is_string($web) || is_array($web)) {
            Route::middleware('web')->group($web);
        }

        if (is_callable($then)) {
            $then($this->app);
        }
    };
}
Punti chiave:
  • Alle rotte api vengono applicati automaticamente il gruppo middleware api e il prefisso /api
  • Se passi una stringa a health, viene registrato automaticamente l’endpoint di health check (default /up)
  • Il path di health viene escluso anche durante la modalità di manutenzione (configurato in PreventRequestsDuringMaintenance::except())
  • Attenzione: api viene registrato prima di web. Se definisci lo stesso path in web e api, la rotta api ha priorità
  • Passando una stringa a pages abiliti il routing di Laravel Folio

withMiddleware() — personalizzazione dei middleware

public function withMiddleware(?callable $callback = null)
{
    $this->app->afterResolving(HttpKernel::class, function ($kernel) use ($callback) {
        $middleware = (new Middleware)
            ->redirectGuestsTo(fn () => route('login'));

        if (! is_null($callback)) {
            $callback($middleware);
        }

        $kernel->setGlobalMiddleware($middleware->getGlobalMiddleware());
        $kernel->setMiddlewareGroups($middleware->getMiddlewareGroups());
        $kernel->setMiddlewareAliases($middleware->getMiddlewareAliases());
        // ...
    });

    return $this;
}
withMiddleware() esegue la callback dopo che HttpKernel è stato risolto: usa infatti l’hook afterResolving(). L’oggetto Middleware passato alla callback ha molti metodi di personalizzazione.
->withMiddleware(function (Middleware $middleware) {
    // aggiungi un middleware globale
    $middleware->append(MyGlobalMiddleware::class);

    // aggiungi al gruppo web
    $middleware->web(append: [EnsureUserIsSubscribed::class]);

    // sostituisci un middleware nel gruppo api
    $middleware->api(replace: [
        OldMiddleware::class => NewMiddleware::class,
    ]);

    // esclusioni CSRF
    $middleware->validateCsrfTokens(except: ['stripe/*', 'webhook/*']);

    // cambia il redirect per gli utenti non autenticati
    $middleware->redirectGuestsTo('/custom-login');

    // priorità dei middleware
    $middleware->priority([
        \Illuminate\Session\Middleware\StartSession::class,
        \Illuminate\View\Middleware\ShareErrorsFromSession::class,
    ]);
})

withExceptions() — configurazione della gestione eccezioni

public function withExceptions(?callable $using = null)
{
    $this->app->singleton(
        \Illuminate\Contracts\Debug\ExceptionHandler::class,
        \Illuminate\Foundation\Exceptions\Handler::class
    );

    if ($using !== null) {
        $this->app->afterResolving(
            \Illuminate\Foundation\Exceptions\Handler::class,
            fn ($handler) => $using(new Exceptions($handler)),
        );
    }

    return $this;
}
withExceptions() registra la classe Handler del framework come singleton e imposta la callback tramite afterResolving(). Alla callback viene passato un wrapper Exceptions.
->withExceptions(function (Exceptions $exceptions) {
    // non riportare eccezioni specifiche
    $exceptions->dontReport(MissedFlightException::class);

    // report personalizzato per un'eccezione specifica
    $exceptions->report(function (InvalidOrderException $e) {
        // notifica su Slack, ecc.
    });

    // personalizza la response HTTP per un'eccezione specifica
    $exceptions->render(function (NotFoundHttpException $e, Request $request) {
        if ($request->is('api/*')) {
            return response()->json(['message' => 'Not Found'], 404);
        }
    });

    // throttling (non riportare la stessa eccezione ripetuta)
    $exceptions->throttle(function (Throwable $e) {
        return Limit::perMinute(20);
    });
})

withProviders() — registrazione dei service provider

public function withProviders(array $providers = [], bool $withBootstrapProviders = true)
{
    RegisterProviders::merge(
        $providers,
        $withBootstrapProviders
            ? $this->app->getBootstrapProvidersPath()
            : null
    );

    return $this;
}
withProviders() viene chiamato di default dentro Application::configure(), quindi bootstrap/providers.php viene caricato automaticamente. Se vuoi passare provider aggiuntivi devi chiamarlo esplicitamente in bootstrap/app.php.
Application::configure(basePath: dirname(__DIR__))
    ->withProviders([
        // aggiungi provider oltre a quelli di bootstrap/providers.php di default
        App\Providers\CustomServiceProvider::class,
    ])
    ->withRouting(...)
    ->create();
Passando withBootstrapProviders: false disabiliti il caricamento di bootstrap/providers.php. Non farlo se non hai un motivo specifico.

Altri metodi principali

MetodoDescrizione
withKernels()Registra come singleton il Kernel HTTP/Console. Chiamato in automatico da configure()
withEvents()Abilita l’event discovery. Chiamato in automatico da configure()
withCommands(array $commands)Registra ulteriori classi o directory di comandi Artisan
withSchedule(callable $callback)Definisce lo schedule in bootstrap/app.php
withBroadcasting(string $channels)Registra il file dei channel di broadcasting
withBindings(array $bindings)Registra i binding del container
withSingletons(array $singletons)Registra binding singleton
registered(callable $callback)Callback eseguita dopo la registrazione dei service provider
booting(callable $callback)Callback eseguita al booting
booted(callable $callback)Callback eseguita a booted
create()Restituisce l’istanza Application

Intenti di design: perché è così

Configurazione “code-first”

In Laravel 10 app/Http/Kernel.php elencava i middleware in array. È una scrittura vicina a un file di configurazione, poco supportata dal type system PHP e dagli IDE. Con Laravel 11 si è passati a uno stile a callback come withMiddleware(function (Middleware $middleware) { ... }). In questo modo ottieni autocompletamento dei tipi e puoi scrivere in modo naturale logica dinamica con condizioni e cicli.

Da “convention over configuration” a “configurazione esplicita”

api.php è diventato opt-in: nelle app che non usano API non ha più senso caricare sempre il gruppo api. Filosofia: ciò che non usi non è presente di default.

Uso dell’hook afterResolving()

withMiddleware() e withExceptions() usano afterResolving() per evitare problemi d’ordine nella configurazione. I metodi di ApplicationBuilder vengono chiamati prima che l’app sia completamente avviata, ma le azioni concrete (applicare le impostazioni al Kernel) vengono differite fino alla prima risoluzione del Kernel.

Esempi di personalizzazione pratica

Convivenza di API e Web

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        api: __DIR__.'/../routes/api.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
        apiPrefix: 'api/v1',  // cambia dal /api di default
    )
    ->withMiddleware(function (Middleware $middleware): void {
        //
    })
    ->withExceptions(function (Exceptions $exceptions): void {
        //
    })->create();

Personalizzazione dei middleware

->withMiddleware(function (Middleware $middleware) {
    // aggiungi controllo di verifica alle rotte web
    $middleware->web(append: [
        \App\Http\Middleware\EnsureEmailIsVerified::class,
    ]);

    // rimuovi un middleware nelle rotte api
    $middleware->api(remove: [
        \Illuminate\Session\Middleware\StartSession::class,
    ]);

    // escludi gli endpoint webhook dal CSRF
    $middleware->validateCsrfTokens(except: [
        'webhook/*',
        'stripe/webhook',
    ]);

    // imposta un alias per un middleware specifico
    $middleware->alias([
        'subscribed' => \App\Http\Middleware\EnsureUserIsSubscribed::class,
    ]);
})

Concentrare lo schedule in bootstrap/app.php

Lo schedule si può scrivere anche in routes/console.php, ma con withSchedule() puoi raggrupparlo in bootstrap/app.php.
use Illuminate\Console\Scheduling\Schedule;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withSchedule(function (Schedule $schedule) {
        $schedule->command('emails:send')->daily();
        $schedule->command('reports:generate')->weeklyOn(1, '8:00');
        $schedule->job(new PruneOldRecords)->daily();
    })
    ->withMiddleware(function (Middleware $middleware): void {
        //
    })
    ->withExceptions(function (Exceptions $exceptions): void {
        //
    })->create();

Personalizzare la gestione delle eccezioni

->withExceptions(function (Exceptions $exceptions) {
    // restituisci sempre JSON alle richieste API
    $exceptions->render(function (Throwable $e, Request $request) {
        if ($request->is('api/*') || $request->wantsJson()) {
            $status = match (true) {
                $e instanceof NotFoundHttpException => 404,
                $e instanceof AuthenticationException => 401,
                $e instanceof AuthorizationException => 403,
                $e instanceof ValidationException => 422,
                default => 500,
            };

            return response()->json([
                'message' => $e->getMessage(),
            ], $status);
        }
    });

    // notifica Slack solo in produzione
    if (app()->isProduction()) {
        $exceptions->report(function (Throwable $e) {
            app(SlackNotifier::class)->notify($e);
        })->stop();
    }
})

Gestire i binding del container in bootstrap/app.php

In app piccole puoi scrivere binding semplici direttamente in bootstrap/app.php invece che in AppServiceProvider.
return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(...)
    ->withSingletons([
        \App\Contracts\PaymentGateway::class => \App\Services\StripeGateway::class,
        \App\Contracts\MailService::class => \App\Services\SendgridMailService::class,
    ])
    ->withMiddleware(...)
    ->withExceptions(...)
    ->create();

Ordine di avvio di Laravel

All’avvio dell’app, service provider e hook dell’Application vengono eseguiti in questo ordine.
  1. register() di tutti i ServiceProvider
  2. registered() dell’Application
  3. booting() dell’Application
  4. boot() di tutti i ServiceProvider
  5. booted() dell’Application
Aggiungendo il codice seguente a AppServiceProvider puoi verificarne l’ordine.
class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        info('1. AppServiceProvider register');

        $this->booting(function () {
            info('4. AppServiceProvider booting');
        });

        $this->booted(function () {
            info('5. AppServiceProvider booted');
        });

        $this->app->registered(function () {
            info('2. app registered');
        });

        $this->app->booting(function () {
            info('3. app booting');
        });

        $this->app->booted(function () {
            info('6. AppServiceProvider@register app booted');
        });
    }

    public function boot(): void
    {
        $this->app->booted(function () {
            info('7. AppServiceProvider@boot app booted');
        });
    }
}
registered(), booting() e booted() di ApplicationBuilder si limitano a registrare callback nell’Application. Di solito non ti servono, ma consentono operazioni particolari, come modificare il Kernel a booted() quando i preparativi di avvio sono completi.
// bootstrap/app.php

use Illuminate\Contracts\Http\Kernel;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(...)
    ->withMiddleware(...)
    ->withExceptions(...)
    ->booted(function (Application $app) {
        $kernel = $app->make(Kernel::class);

        //

        $app->instance(Kernel::class, $kernel);
    })
    ->create();

Prossimi passi

Service container

Comprendi il meccanismo del service container usato internamente da ApplicationBuilder.

FAQ nuova struttura

Raccolta di dubbi frequenti e risposte sulla nuova struttura dell’applicazione.
Ultima modifica il 13 luglio 2026