Estructura de aplicación en Laravel 11 y posteriores
Presenta el Slim Application Skeleton introducido en Laravel 11 en detalle y explica la implementación interna, desde Application::configure() hasta ApplicationBuilder.
En Laravel 11 la estructura de la aplicación se ha rediseñado profundamente con el llamado «Slim Application Skeleton». El cambio más importante es la eliminación de la dispersión de configuración: todo se concentra en bootstrap/app.php.
Elemento
Laravel 10 y anteriores
Laravel 11 y posteriores
Kernel HTTP
app/Http/Kernel.php
Eliminado (integrado en el framework)
Kernel de consola
app/Console/Kernel.php
Eliminado (migrado a routes/console.php)
Handler de excepciones
app/Exceptions/Handler.php
Eliminado (unificado en bootstrap/app.php)
Service providers
5 archivos
Un único archivo AppServiceProvider.php
Archivos de rutas
web.php y api.php por defecto
Solo web.php por defecto, api.php opt-in
Bootstrap
Configuración dispersa en varios archivos
Concentrada en bootstrap/app.php
BD por defecto
MySQL/PostgreSQL
SQLite
Este cambio es para proyectos nuevos. Si actualizas una aplicación existente de Laravel 10, la estructura antigua seguirá funcionando sin problemas.
laravel-app/├── app/│ ├── Http/│ │ └── Controllers/│ ├── Models/│ │ └── User.php│ └── Providers/│ └── AppServiceProvider.php├── bootstrap/│ ├── app.php ← centro de la configuración de la aplicación│ ├── cache/│ └── providers.php ← lista de service providers├── config/├── database/├── public/│ └── index.php ← punto de entrada├── resources/├── routes/│ ├── web.php ← rutas web (por defecto)│ └── console.php ← comandos de Artisan y schedule├── storage/└── tests/
Con este único archivo puedes configurar el enrutamiento, los middlewares y el manejo de excepciones. La configuración que en Laravel 10 y anteriores estaba repartida entre app/Http/Kernel.php, app/Console/Kernel.php y app/Exceptions/Handler.php se concentra aquí.
Este archivo es el lugar donde se registran los service providers. En Laravel 10 se declaraban en el array providers de config/app.php; ahora esa lista se ha separado en bootstrap/providers.php. Por defecto en Laravel 11 solo aparece AppServiceProvider.
Cuando instalas un paquete con composer require, el paquete puede actualizar automáticamente bootstrap/providers.php. config/app.php no ha dejado de leerse, pero ahora el lugar recomendado para registrar providers nuevos es bootstrap/providers.php.
El kernel HTTP se ha integrado en la clase Illuminate\Foundation\Http\Kernel del framework. La personalización de middlewares se hace con withMiddleware() en bootstrap/app.php.
Las dos responsabilidades del kernel de consola se han separado. Los comandos de Artisan se colocan en app/Console/Commands/ y se autodetectan; el schedule se declara en routes/console.php.
// Laravel 10 y anteriores: app/Console/Kernel.phpprotected function schedule(Schedule $schedule): void{ $schedule->command('emails:send')->daily();}
// Laravel 11 y posteriores: routes/console.phpuse Illuminate\Support\Facades\Schedule;Schedule::command('emails:send')->daily();
Eliminación de app/Exceptions/Handler.php
El handler de excepciones se ha integrado en la clase Illuminate\Foundation\Exceptions\Handler del framework. La personalización se hace con withExceptions() en bootstrap/app.php.
// Laravel 10 y anteriores: app/Exceptions/Handler.phppublic function register(): void{ $this->reportable(function (InvalidOrderException $e) { // ... });}
Application::configure() es un método estático de Illuminate\Foundation\Application.
// De Illuminate\Foundation\Applicationpublic 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();}
Este método realiza lo siguiente:
Determina el directorio raíz de la aplicación a partir de basePath.
Crea una instancia de Application.
La envuelve con ApplicationBuilder y aplica la configuración por defecto.
Devuelve la instancia de ApplicationBuilder.
Un detalle importante: dentro de configure()ya se han llamado withKernels() / withEvents() / withCommands() / withProviders(). No necesitas volver a invocarlas en bootstrap/app.php.
Application::configure(basePath: dirname(__DIR__)) // devuelve un ApplicationBuilder ->withRouting(...) // configura las rutas y devuelve $this ->withMiddleware(...) // configura los middlewares y devuelve $this ->withExceptions(...) // configura el manejo de excepciones y devuelve $this ->create(); // devuelve la instancia de Application
Al llamar a create(), se extrae la instancia de Application del ApplicationBuilder, y esa Application es lo que retorna bootstrap/app.php.
Flujo desde la petición hasta el arranque de la aplicación
public/index.php es el punto de entrada; carga bootstrap/app.php para obtener Application. A continuación, el HTTP Kernel pasa la petición por el pipeline de middlewares y el router la envía al controlador correspondiente.
Internamente registra una callback en AppRouteServiceProvider::loadRoutesUsing() y registra el propio AppRouteServiceProvider durante el booting de la aplicación.
// Procesamiento interno de withRouting() (versión simplificada)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); } };}
Puntos clave:
A las rutas api se les aplican automáticamente el grupo de middleware api y el prefijo /api.
Si pasas una cadena a health, se registra automáticamente un endpoint de health check (por defecto /up).
La ruta de health queda excluida incluso en modo mantenimiento (se configura con PreventRequestsDuringMaintenance::except()).
Ten en cuenta que api se registra antes que web. Si defines rutas web y de API en la misma ruta, prevalecerá la de API.
Si pasas una cadena a pages, se habilita el enrutamiento de Laravel Folio.
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() ejecuta la callback después de que se haya resuelto HttpKernel, gracias al hook afterResolving(). El objeto Middleware que se pasa a la callback ofrece numerosos métodos de personalización.
->withMiddleware(function (Middleware $middleware) { // Añadir un middleware global $middleware->append(MyGlobalMiddleware::class); // Añadir middleware al grupo web $middleware->web(append: [EnsureUserIsSubscribed::class]); // Reemplazar un middleware del grupo api $middleware->api(replace: [ OldMiddleware::class => NewMiddleware::class, ]); // Configurar rutas excluidas de CSRF $middleware->validateCsrfTokens(except: ['stripe/*', 'webhook/*']); // Cambiar el destino de redirección para invitados $middleware->redirectGuestsTo('/custom-login'); // Establecer la prioridad de los middlewares $middleware->priority([ \Illuminate\Session\Middleware\StartSession::class, \Illuminate\View\Middleware\ShareErrorsFromSession::class, ]);})
withExceptions() — configuración del manejo de excepciones
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 clase Handler del framework como singleton y luego programa la callback con afterResolving(). A la callback se le pasa un wrapper Exceptions.
->withExceptions(function (Exceptions $exceptions) { // No reportar una excepción concreta $exceptions->dontReport(MissedFlightException::class); // Reportar una excepción concreta de forma personalizada $exceptions->report(function (InvalidOrderException $e) { // Notificar a Slack, etc. }); // Personalizar la respuesta HTTP para una excepción concreta $exceptions->render(function (NotFoundHttpException $e, Request $request) { if ($request->is('api/*')) { return response()->json(['message' => 'Not Found'], 404); } }); // Throttling (no reportar repetidamente la misma excepción) $exceptions->throttle(function (Throwable $e) { return Limit::perMinute(20); });})
public function withProviders(array $providers = [], bool $withBootstrapProviders = true){ RegisterProviders::merge( $providers, $withBootstrapProviders ? $this->app->getBootstrapProvidersPath() : null ); return $this;}
Como Application::configure() llama por defecto a withProviders(), bootstrap/providers.php se carga automáticamente. Si quieres añadir providers adicionales, tienes que llamarlo explícitamente en bootstrap/app.php.
Application::configure(basePath: dirname(__DIR__)) ->withProviders([ // Añadir providers además de los de bootstrap/providers.php App\Providers\CustomServiceProvider::class, ]) ->withRouting(...) ->create();
Si pasas withBootstrapProviders: false, dejará de cargarse bootstrap/providers.php. Omite ese parámetro salvo que tengas una razón concreta.
El app/Http/Kernel.php de Laravel 10 y anteriores utilizaba un estilo en el que los middlewares se enumeraban en arrays. Era un estilo cercano a un archivo de configuración, con la desventaja de que no aprovechaba bien el sistema de tipos de PHP ni el soporte del IDE.En Laravel 11 se pasa al estilo callback withMiddleware(function (Middleware $middleware) { ... }). Así se dispone de autocompletado por tipos y se pueden escribir configuraciones dinámicas de forma natural, con condicionales, bucles y otras estructuras.
De «convención sobre configuración» a «configuración explícita»
Hacer que api.php sea opt-in resuelve el hecho de que el grupo de middleware api se cargaba siempre incluso en aplicaciones que no usaban rutas de API. La idea es que las funcionalidades no utilizadas simplemente no existen por defecto.
En withMiddleware() y withExceptions() se recurre a afterResolving() para evitar problemas de orden en la configuración. Los métodos de ApplicationBuilder se llaman antes de que la aplicación se arranque por completo, pero la aplicación real de la configuración (aplicarla al kernel) queda diferida hasta que el kernel se resuelve por primera vez.
->withMiddleware(function (Middleware $middleware) { // Añadir un middleware de verificación a las rutas web $middleware->web(append: [ \App\Http\Middleware\EnsureEmailIsVerified::class, ]); // Excluir un middleware concreto en las rutas de API $middleware->api(remove: [ \Illuminate\Session\Middleware\StartSession::class, ]); // Excluir los endpoints de webhook del CSRF $middleware->validateCsrfTokens(except: [ 'webhook/*', 'stripe/webhook', ]); // Asignar un alias a un middleware específico $middleware->alias([ 'subscribed' => \App\Http\Middleware\EnsureUserIsSubscribed::class, ]);})
Los métodos registered(), booting() y booted() de ApplicationBuilder simplemente registran callbacks en Application. Normalmente no necesitas usarlos, pero permiten operaciones especiales como modificar el Kernel en booted() cuando ya está listo.