Saltar al contenido principal

Comparación con Laravel 10 y anteriores

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.
ElementoLaravel 10 y anterioresLaravel 11 y posteriores
Kernel HTTPapp/Http/Kernel.phpEliminado (integrado en el framework)
Kernel de consolaapp/Console/Kernel.phpEliminado (migrado a routes/console.php)
Handler de excepcionesapp/Exceptions/Handler.phpEliminado (unificado en bootstrap/app.php)
Service providers5 archivosUn único archivo AppServiceProvider.php
Archivos de rutasweb.php y api.php por defectoSolo web.php por defecto, api.php opt-in
BootstrapConfiguración dispersa en varios archivosConcentrada en bootstrap/app.php
BD por defectoMySQL/PostgreSQLSQLite
Este cambio es para proyectos nuevos. Si actualizas una aplicación existente de Laravel 10, la estructura antigua seguirá funcionando sin problemas.

Nueva estructura de directorios y archivos

Estructura de directorios del skeleton

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/

bootstrap/app.php — el centro de la configuración de la aplicación

<?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();
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í.

bootstrap/providers.php — lista de service providers

<?php

use App\Providers\AppServiceProvider;

return [
    AppServiceProvider::class,
];
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.

Cambios en el directorio routes/

routes/
├── web.php      ← rutas web (se cargan por defecto)
└── console.php  ← se definen aquí los comandos Artisan y el schedule
Por defecto no existen api.php ni channels.php. Si los necesitas, los generas con comandos Artisan.
# Añadir rutas de API (instala api.php + Sanctum)
php artisan install:api

# Añadir broadcasting (instala channels.php + Reverb, etc.)
php artisan install:broadcasting
También puedes definir el schedule en routes/console.php.
<?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();

Archivos eliminados

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.
// Laravel 10 y anteriores: app/Http/Kernel.php
protected $middleware = [
    \Illuminate\Http\Middleware\TrustProxies::class,
    // ...
];

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

    $middleware->validateCsrfTokens(except: ['stripe/*']);
})
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.php
protected function schedule(Schedule $schedule): void
{
    $schedule->command('emails:send')->daily();
}
// Laravel 11 y posteriores: routes/console.php
use Illuminate\Support\Facades\Schedule;

Schedule::command('emails:send')->daily();
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.php
public function register(): void
{
    $this->reportable(function (InvalidOrderException $e) {
        // ...
    });
}
// Laravel 11 y posteriores: bootstrap/app.php
->withExceptions(function (Exceptions $exceptions) {
    $exceptions->report(function (InvalidOrderException $e) {
        // ...
    });
})

Cómo funciona Application::configure()

Implementación interna en el framework

Application::configure() es un método estático de Illuminate\Foundation\Application.
// De 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();
}
Este método realiza lo siguiente:
  1. Determina el directorio raíz de la aplicación a partir de basePath.
  2. Crea una instancia de Application.
  3. La envuelve con ApplicationBuilder y aplica la configuración por defecto.
  4. 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.

Flujo de configuración con method chaining

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.

Métodos principales de ApplicationBuilder en profundidad

withRouting() — el proceso interno del registro de rutas

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 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.

withMiddleware() — personalización de middlewares

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);
    });
})

withProviders() — registro de service providers

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.

Otros métodos principales

MétodoDescripción
withKernels()Registra como singleton los kernels HTTP / Console. configure() lo llama automáticamente.
withEvents()Activa la event discovery. configure() lo llama automáticamente.
withCommands(array $commands)Registra clases o directorios adicionales de comandos Artisan.
withSchedule(callable $callback)Define el schedule en bootstrap/app.php.
withBroadcasting(string $channels)Registra el archivo de canales de broadcasting.
withBindings(array $bindings)Registra bindings del contenedor.
withSingletons(array $singletons)Registra bindings singleton.
registered(callable $callback)Añade una callback que se ejecuta tras registrar los service providers.
booting(callable $callback)Añade una callback que se ejecuta durante el booting.
booted(callable $callback)Añade una callback que se ejecuta tras el booted.
create()Devuelve la instancia de Application.

Intención del diseño: por qué es así

Configuración «code first»

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.

El uso del hook afterResolving()

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.

Ejemplos prácticos de personalización

Convivencia de API y 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',  // Cambio respecto al /api por defecto
    )
    ->withMiddleware(function (Middleware $middleware): void {
        //
    })
    ->withExceptions(function (Exceptions $exceptions): void {
        //
    })->create();

Personalización de middlewares

->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,
    ]);
})

Centralizar el schedule en bootstrap/app.php

El schedule también puede escribirse en routes/console.php, pero con withSchedule() puedes concentrarlo en 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();

Personalización del manejo de excepciones

->withExceptions(function (Exceptions $exceptions) {
    // Devolver siempre JSON para las peticiones de 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);
        }
    });

    // Notificar a Slack solo en producción
    if (app()->isProduction()) {
        $exceptions->report(function (Throwable $e) {
            app(SlackNotifier::class)->notify($e);
        })->stop();
    }
})

Gestionar bindings del contenedor en bootstrap/app.php

En aplicaciones pequeñas puedes escribir bindings sencillos directamente en bootstrap/app.php en lugar de en 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();

Orden del proceso de arranque de Laravel

Al arrancar una aplicación de Laravel, los service providers y los hooks de Application se ejecutan en este orden:
  1. register() de todos los ServiceProvider.
  2. registered() de Application.
  3. booting() de Application.
  4. boot() de todos los ServiceProvider.
  5. booted() de Application.
Puedes comprobar el orden añadiendo el siguiente código a AppServiceProvider.
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');
        });
    }
}
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.
// 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();

Próximos pasos

Contenedor de servicios

Entiende cómo funciona el contenedor de servicios que utiliza internamente ApplicationBuilder.

FAQ de la nueva estructura de aplicación

Reúne preguntas frecuentes y respuestas sobre la nueva estructura de aplicación.
Última modificación el 13 de julio de 2026