Saltar al contenido principal

Introducción

Esta guía cubre los pasos para migrar la estructura antigua de aplicación (con clases Kernel y varios service providers) de Laravel 10 y anteriores al Slim Application Skeleton de Laravel 11 en adelante.
La documentación oficial no recomienda esta migración.
  • La estructura antigua de Laravel 10 sigue funcionando en Laravel 11 y posteriores. No hay planes de retirar su soporte hasta la versión actual, incluida Laravel 13.
  • La migración es completamente opcional. Ni es obligatoria ni está recomendada.
  • Si no conoces en profundidad las diferencias entre la estructura antigua y la nueva, es muy recomendable no realizar la migración. Es un trabajo pensado para desarrolladores familiarizados con las tripas del framework.
  • Antes de migrar, haz siempre una copia de seguridad y comprueba que todos los tests pasan.

Cuándo puede ser necesario migrar

Puedes plantearte la migración en casos como los siguientes:
  • Quieres que la estructura del proyecto coincida con la estándar más reciente para que los nuevos miembros del equipo puedan contrastarla fácilmente con la documentación oficial.
  • Quieres mantener la coherencia con paquetes o starter kits creados en Laravel 11 o posterior.
  • Quieres reducir los archivos y clases de configuración heredados de la estructura antigua y simplificar tu código base.

Requisitos previos

Esta guía asume lo siguiente:

Ejemplo de migración

A continuación se muestran los pasos para migrar únicamente la estructura de la aplicación de un proyecto creado con Laravel 10 + Breeze (stack Blade), manteniendo Breeze intacto.
1

Sustituir bootstrap/app.php

El antiguo bootstrap/app.php creaba la instancia $app y registraba los kernels. Sustitúyelo por la cadena Application::configure().Antiguo (Laravel 10):
<?php

$app = new Illuminate\Foundation\Application(
    $_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)
);

$app->singleton(
    Illuminate\Contracts\Http\Kernel::class,
    App\Http\Kernel::class
);

$app->singleton(
    Illuminate\Contracts\Console\Kernel::class,
    App\Console\Kernel::class
);

$app->singleton(
    Illuminate\Contracts\Debug\ExceptionHandler::class,
    App\Exceptions\Handler::class
);

return $app;
Nuevo (Laravel 11 y posteriores):
<?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) {
        //
    })
    ->withExceptions(function (Exceptions $exceptions) {
        //
    })->create();
Las callbacks de withMiddleware() y withExceptions() son el sitio donde deberás migrar la configuración de los archivos de kernel que eliminarás en el siguiente paso. De momento déjalas vacías; añadirás la configuración en pasos posteriores.
2

Eliminar el kernel HTTP (app/Http/Kernel.php)

app/Http/Kernel.php definía los middlewares globales, los grupos de middleware y los alias de middleware.Antiguo (app/Http/Kernel.php):
<?php

namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel
{
    protected $middleware = [
        \App\Http\Middleware\TrustProxies::class,
        \Illuminate\Http\Middleware\HandleCors::class,
        \App\Http\Middleware\PreventRequestsDuringMaintenance::class,
        \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
        \App\Http\Middleware\TrimStrings::class,
        \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
    ];

    protected $middlewareGroups = [
        'web' => [
            \App\Http\Middleware\EncryptCookies::class,
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
            \Illuminate\Session\Middleware\StartSession::class,
            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
            \App\Http\Middleware\VerifyCsrfToken::class,
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],
        'api' => [
            \Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],
    ];

    protected $middlewareAliases = [
        'auth' => \App\Http\Middleware\Authenticate::class,
        // ...
    ];
}
En Laravel 11 estos middlewares están integrados como valores por defecto dentro del framework. Si no los has personalizado, puedes eliminar app/Http/Kernel.php directamente.Si tienes personalizaciones (añades o excluyes middlewares propios), migra esa configuración a withMiddleware() en bootstrap/app.php antes de eliminarlo.
->withMiddleware(function (Middleware $middleware) {
    // Añadir un middleware global
    $middleware->append(\App\Http\Middleware\MyCustomMiddleware::class);

    // Añadir un middleware al grupo web
    $middleware->web(append: [
        \App\Http\Middleware\HandleInertiaRequests::class,
    ]);

    // Añadir alias de middleware
    $middleware->alias([
        'role' => \App\Http\Middleware\CheckRole::class,
    ]);
})
Cuando hayas terminado la migración, elimina app/Http/Kernel.php.
En Laravel 11, las clases de middleware por defecto como TrustProxies, EncryptCookies o VerifyCsrfToken también se pueden eliminar de app/Http/Middleware/. Como el framework las incorpora internamente, si no necesitas personalizarlas, sus archivos ya no hacen falta.
3

Eliminar el kernel de consola (app/Console/Kernel.php)

app/Console/Kernel.php se encargaba de definir el schedule y de cargar automáticamente los comandos.Antiguo (app/Console/Kernel.php):
<?php

namespace App\Console;

use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;

class Kernel extends ConsoleKernel
{
    protected function schedule(Schedule $schedule): void
    {
        // $schedule->command('inspire')->hourly();
    }

    protected function commands(): void
    {
        $this->load(__DIR__.'/Commands');
        require base_path('routes/console.php');
    }
}
La carga automática de comandos en Laravel 11 y posteriores es automática: el directorio app/Console/Commands/ se escanea solo, por lo que no necesitas $this->load().La definición del schedule se migra a routes/console.php o al withSchedule() de bootstrap/app.php.
// routes/console.php (Laravel 11 y posteriores)
use Illuminate\Support\Facades\Schedule;

Schedule::command('emails:send')->daily();
Tras la migración, elimina app/Console/Kernel.php.
No elimines los archivos de comandos Artisan personalizados de app/Console/. Deja los archivos de comandos tal cual y elimina únicamente el archivo de la clase Kernel.
4

Eliminar el handler de excepciones (app/Exceptions/Handler.php)

app/Exceptions/Handler.php se encargaba de la configuración del reporte y del renderizado de excepciones.Antiguo (app/Exceptions/Handler.php):
<?php

namespace App\Exceptions;

use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Throwable;

class Handler extends ExceptionHandler
{
    protected $dontFlash = [
        'current_password',
        'password',
        'password_confirmation',
    ];

    public function register(): void
    {
        $this->reportable(function (Throwable $e) {
            //
        });
    }
}
Si tienes personalizaciones, migra su contenido a withExceptions() en bootstrap/app.php antes de eliminarlo.
use Throwable;
use Illuminate\Http\Request;

->withExceptions(function (Exceptions $exceptions) {
    // Personalizar el reporte de excepciones
    $exceptions->report(function (Throwable $e) {
        // ...
    });

    // Personalizar el renderizado de excepciones
    $exceptions->render(function (\App\Exceptions\InvalidOrderException $e, Request $request) {
        return response()->view('errors.invalid-order', status: 500);
    });
})
Si habías añadido elementos propios a $dontFlash, puedes migrarlos de la misma forma.
->withExceptions(function (Exceptions $exceptions) {
    $exceptions->dontFlash([
        'my_sensitive_field',
    ]);
})
Cuando termine la migración, elimina app/Exceptions/Handler.php.
5

Eliminar RouteServiceProvider y migrar el registro de rutas

app/Providers/RouteServiceProvider.php cargaba los archivos de rutas y definía los rate limits.Antiguo (app/Providers/RouteServiceProvider.php):
<?php

namespace App\Providers;

use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Route;

class RouteServiceProvider extends ServiceProvider
{
    public const HOME = '/home';

    public function boot(): void
    {
        RateLimiter::for('api', function (Request $request) {
            return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
        });

        $this->routes(function () {
            Route::middleware('api')
                ->prefix('api')
                ->group(base_path('routes/api.php'));

            Route::middleware('web')
                ->group(base_path('routes/web.php'));
        });
    }
}
Migra el registro de los archivos de rutas a withRouting() en bootstrap/app.php.
->withRouting(
    web: __DIR__.'/../routes/web.php',
    api: __DIR__.'/../routes/api.php', // si utilizas rutas de API
    commands: __DIR__.'/../routes/console.php',
    health: '/up',
)
Migra los rate limits a AppServiceProvider::boot().
// app/Providers/AppServiceProvider.php
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;

public function boot(): void
{
    RateLimiter::for('api', function (Request $request) {
        return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
    });
}
Si usas la constante HOME en algún sitio, reemplázala por la URL directamente en forma de cadena o mueve la constante a AppServiceProvider.Cuando termines la migración, elimina app/Providers/RouteServiceProvider.php.
6

Consolidar los service providers

Laravel 10 traía por defecto 5 service providers. Los consolidaremos en un único AppServiceProvider.php.Providers a eliminar (migra su contenido a AppServiceProvider antes de borrarlos):
ArchivoDestino de la migración
app/Providers/AuthServiceProvider.phpGate::policy(), etc., en AppServiceProvider::boot()
app/Providers/BroadcastServiceProvider.phpSi usas broadcasting, withBroadcasting() de bootstrap/app.php
app/Providers/EventServiceProvider.phpEvent::listen(), etc., en AppServiceProvider::boot()
app/Providers/RouteServiceProvider.phpYa tratado en el paso anterior
Ejemplo de migración del contenido antiguo de app/Providers/AuthServiceProvider.php:
// Antiguo: app/Providers/AuthServiceProvider.php
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;

class AuthServiceProvider extends ServiceProvider
{
    protected $policies = [
        Post::class => PostPolicy::class,
    ];

    public function boot(): void
    {
        $this->registerPolicies();
    }
}
// Nuevo: app/Providers/AppServiceProvider.php
// Si sigues las convenciones de nomenclatura estándar de Laravel, se detecta automáticamente y no es necesario
use Illuminate\Support\Facades\Gate;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Gate::policy(Post::class, PostPolicy::class);
    }
}
Cuando hayas eliminado los archivos de providers innecesarios, elimina también el array providers de config/app.php.
// Array providers en config/app.php
// Se puede eliminar completo
'providers' => ServiceProvider::defaultProviders()->merge([
App\Providers\AppServiceProvider::class,
// Elimina también las entradas de los providers que has borrado
// App\Providers\AuthServiceProvider::class, // eliminar
// App\Providers\EventServiceProvider::class, // eliminar
// App\Providers\RouteServiceProvider::class, // eliminar
])->toArray(),
Además, crea bootstrap/providers.php para adaptarte a la nueva estructura.
<?php

// bootstrap/providers.php
return [
    App\Providers\AppServiceProvider::class,
];
Si existe bootstrap/providers.php, Laravel dará prioridad a este archivo como lista de providers al cargarlos.
7

Actualizar la clase base de controlador

La clase base Controller de Laravel 10 usaba los traits AuthorizesRequests y ValidatesRequests. La nueva clase base de Laravel 11 es una clase abstracta simple sin esos traits.Antiguo (Laravel 10):
<?php

namespace App\Http\Controllers;

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

class Controller extends BaseController
{
    use AuthorizesRequests, ValidatesRequests;
}
Nuevo (Laravel 11 y posteriores):
<?php

namespace App\Http\Controllers;

abstract class Controller
{
    //
}
Sustituye las funcionalidades que aportaban los traits del siguiente modo.
Método antiguo (vía trait)Método nuevo
$this->validate($request, $rules)$request->validate($rules)
$this->authorize('update', $post)Gate::authorize('update', $post)
$this->authorizeResource(Post::class)En Laravel 11 no hay una alternativa sencilla, así que deja App\Http\Controllers\Controller con las especificaciones de Laravel 10
Si tus controladores existentes usan los métodos de esos traits, puedes elegir entre modificar cada controlador o dejar los traits en la clase base Controller. No es necesario cambiarlo todo a la vez para que funcione.
Si los controladores de autenticación generados por Breeze o Jetstream llaman a $this->validate() o $this->authorize(), comprueba siempre su funcionamiento antes de cambiar la clase base.
8

Eliminar archivos config innecesarios

Los archivos como config/cors.php, config/hashing.php o config/view.php que no hayas modificado respecto al valor por defecto se pueden eliminar. Mantén los que hayas cambiado.
9

Actualizar public/index.php

Ha cambiado para adaptarse a la nueva estructura, así que reescríbelo por completo.
<?php

use Illuminate\Foundation\Application;
use Illuminate\Http\Request;

define('LARAVEL_START', microtime(true));

// Determine if the application is in maintenance mode...
if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {
require $maintenance;
}

// Register the Composer autoloader...
require __DIR__.'/../vendor/autoload.php';

// Bootstrap Laravel and handle the request...
/** @var Application $app */
$app = require_once __DIR__.'/../bootstrap/app.php';

$app->handleRequest(Request::capture());
10

Actualizar artisan

Reescribe también el archivo artisan por completo del mismo modo.
#!/usr/bin/env php
<?php

use Illuminate\Foundation\Application;
use Symfony\Component\Console\Input\ArgvInput;

define('LARAVEL_START', microtime(true));

// Register the Composer autoloader...
require __DIR__.'/vendor/autoload.php';

// Bootstrap Laravel and handle the command...
/** @var Application $app */
$app = require_once __DIR__.'/bootstrap/app.php';

$status = $app->handleCommand(new ArgvInput);

exit($status);
11

Actualizar tests/TestCase.php

El trait CreatesApplication ya no es necesario, así que ajústalo. Puedes borrar tests/CreatesApplication.php.
<?php

namespace Tests;

use Illuminate\Foundation\Testing\TestCase as BaseTestCase;

abstract class TestCase extends BaseTestCase
{
    //
}
12

Actualizar .env, .env.example y phpunit.xml

Se ha renombrado CACHE_DRIVER a CACHE_STORE y se han añadido algunas entradas, así que ajústalos si es necesario. Estos cambios pueden afectar a los archivos de config y al entorno de producción, así que hazlos con cuidado. No es imprescindible seguir todos los cambios al pie de la letra.
13

Comprobación de funcionamiento

Cuando termine la migración, comprueba el funcionamiento en este orden.
# Limpiar la caché de configuración
php artisan config:clear
php artisan route:clear
php artisan cache:clear

# Verificar que las rutas se han registrado correctamente
php artisan route:list

# Ejecutar los tests
php artisan test
Si aparecen problemas, restaura los archivos eliminados desde la copia de seguridad y revisa los mensajes de error.

Estructura de archivos tras la migración

Tras migrar, el proyecto tendrá una estructura similar a esta (se indican las diferencias frente a la estructura antigua).
app/
├── Console/
│   └── Commands/          ← mantén tus comandos personalizados
│   (Kernel.php eliminado)
├── Exceptions/
│   (Handler.php eliminado)
├── Http/
│   ├── Controllers/
│   │   └── Controller.php ← cambiado a una clase abstracta simple
│   └── Middleware/        ← mantén tus middlewares personalizados
│   (Kernel.php eliminado)
├── Models/
└── Providers/
    └── AppServiceProvider.php ← consolidado en un único archivo
    (AuthServiceProvider.php eliminado)
    (BroadcastServiceProvider.php eliminado)
    (EventServiceProvider.php eliminado)
    (RouteServiceProvider.php eliminado)
bootstrap/
├── app.php                ← sustituido por la forma Application::configure()
└── providers.php          ← creado
routes/
├── web.php
├── api.php                ← solo si es necesario
└── console.php            ← las definiciones del schedule van aquí

Resumen

Elemento migradoUbicación antiguaUbicación nueva
Middleware HTTPapp/Http/Kernel.phpwithMiddleware() en bootstrap/app.php
Manejo de excepcionesapp/Exceptions/Handler.phpwithExceptions() en bootstrap/app.php
Definición del scheduleapp/Console/Kernel.phproutes/console.php
Registro de rutasapp/Providers/RouteServiceProvider.phpwithRouting() en bootstrap/app.php
Lista de service providersArray providers en config/app.phpbootstrap/providers.php
Registro de policiesapp/Providers/AuthServiceProvider.phpRegistro automático. Registro manual en AppServiceProvider::boot()
Event listenersapp/Providers/EventServiceProvider.phpRegistro automático. Registro manual en AppServiceProvider::boot()

Enlaces de referencia

Última modificación el 13 de julio de 2026