与 Laravel 10 及之前的对比
Laravel 11 以「Slim Application Skeleton」为名,对应用结构进行了大规模改造。最大的变化是消除了配置的分散,将其汇总到 bootstrap/app.php 一个位置。
| 项目 | Laravel 10 及之前 | Laravel 11 之后 |
|---|
| HTTP Kernel | app/Http/Kernel.php | 废弃(整合进框架内部) |
| Console Kernel | app/Console/Kernel.php | 废弃(迁移到 routes/console.php) |
| 异常处理器 | app/Exceptions/Handler.php | 废弃(汇总到 bootstrap/app.php) |
| Service Provider | 5 个文件 | 仅 AppServiceProvider.php 一个 |
| 路由文件 | 默认包含 web.php / api.php | 默认仅 web.php,api.php 需 opt-in |
| Bootstrap | 配置分散在多个文件 | 汇总到 bootstrap/app.php |
| 默认数据库 | MySQL/PostgreSQL | SQLite |
该变更面向新项目。已有的 Laravel 10 应用即使升级到新版本,旧结构仍可继续正常运行。
新的目录与文件结构
Skeleton 的目录构成
laravel-app/
├── app/
│ ├── Http/
│ │ └── Controllers/
│ ├── Models/
│ │ └── User.php
│ └── Providers/
│ └── AppServiceProvider.php
├── bootstrap/
│ ├── app.php ← 应用配置的中心
│ ├── cache/
│ └── providers.php ← Service Provider 列表
├── config/
├── database/
├── public/
│ └── index.php ← 入口
├── resources/
├── routes/
│ ├── web.php ← Web 路由(默认)
│ └── console.php ← Artisan 命令与计划任务
├── storage/
└── tests/
bootstrap/app.php — 应用配置的中心
<?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();
仅通过这一个文件,就可以配置路由、中间件与异常处理。Laravel 10 及之前分散在 app/Http/Kernel.php、app/Console/Kernel.php、app/Exceptions/Handler.php 三处的配置,都被汇总到这里。
bootstrap/providers.php — Service Provider 列表
<?php
use App\Providers\AppServiceProvider;
return [
AppServiceProvider::class,
];
该文件是 Service Provider 的注册位置。Laravel 10 中在 config/app.php 的 providers 数组里定义,现在被分离到了 bootstrap/providers.php。Laravel 11 的默认内容只有 AppServiceProvider。
通过 composer require 安装包时,该包可能会自动更新 bootstrap/providers.php。config/app.php 并非不再被引用,但新增注册的推荐位置已变为 bootstrap/providers.php。
routes/ 目录的变化
routes/
├── web.php ← Web 路由(默认加载)
└── console.php ← 定义 Artisan 命令与计划任务
api.php 与 channels.php 默认不存在。有需要时可以用 Artisan 命令生成。
# 添加 API 路由(安装 api.php 与 Sanctum)
php artisan install:api
# 添加广播(安装 channels.php 与 Reverb 等)
php artisan install:broadcasting
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();
被废弃的文件
HTTP Kernel 被整合进框架内部的 Illuminate\Foundation\Http\Kernel。中间件的自定义在 bootstrap/app.php 的 withMiddleware() 中完成。// Laravel 10 及之前: app/Http/Kernel.php
protected $middleware = [
\Illuminate\Http\Middleware\TrustProxies::class,
// ...
];
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
// ...
],
];
// Laravel 11 之后: bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
$middleware->web(append: [
EnsureUserIsSubscribed::class,
]);
$middleware->validateCsrfTokens(except: ['stripe/*']);
})
废弃 app/Console/Kernel.php
Console Kernel 的两项职责被拆分。Artisan 命令放在 app/Console/Commands/ 中会被自动发现,计划任务写在 routes/console.php 中。// Laravel 10 及之前: app/Console/Kernel.php
protected function schedule(Schedule $schedule): void
{
$schedule->command('emails:send')->daily();
}
// Laravel 11 之后: routes/console.php
use Illuminate\Support\Facades\Schedule;
Schedule::command('emails:send')->daily();
废弃 app/Exceptions/Handler.php
异常处理器被整合进框架内部的 Illuminate\Foundation\Exceptions\Handler。自定义在 bootstrap/app.php 的 withExceptions() 中完成。// Laravel 10 及之前: app/Exceptions/Handler.php
public function register(): void
{
$this->reportable(function (InvalidOrderException $e) {
// ...
});
}
// Laravel 11 之后: bootstrap/app.php
->withExceptions(function (Exceptions $exceptions) {
$exceptions->report(function (InvalidOrderException $e) {
// ...
});
})
框架内部的实现
Application::configure() 是 Illuminate\Foundation\Application 上的静态方法。
// 摘自 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();
}
该方法执行了以下处理:
- 从
basePath 确定应用根目录
- 创建
Application 实例
- 用
ApplicationBuilder 包装并应用默认配置
- 返回
ApplicationBuilder 实例
关键在于,configure() 内部已经调用了 withKernels() / withEvents() / withCommands() / withProviders()。你不需要在 bootstrap/app.php 中再次调用这些方法。
通过链式方法进行配置
Application::configure(basePath: dirname(__DIR__)) // 返回 ApplicationBuilder
->withRouting(...) // 配置路由,返回 $this
->withMiddleware(...) // 配置中间件,返回 $this
->withExceptions(...) // 配置异常处理,返回 $this
->create(); // 返回 Application 实例
调用 create() 时会从 ApplicationBuilder 取出 Application 实例,bootstrap/app.php 最终 return 的正是这个 Application 实例。
请求到应用启动的流程
public/index.php 是入口,读取 bootstrap/app.php 获得 Application。之后 HTTP Kernel 将请求送入中间件管道,路由分发到 Controller。
ApplicationBuilder 主要方法深入
withRouting() — 路由注册的内部处理
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
)
内部通过 AppRouteServiceProvider::loadRoutesUsing() 注册回调,并在应用 booting 时注册 AppRouteServiceProvider。
// withRouting() 的内部处理(简化版)
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);
}
};
}
要点:
api 路由会自动应用 api 中间件组与 /api 前缀
- 向
health 传入字符串会自动注册健康检查端点(默认 /up)
health 路径在维护模式下也会被排除(通过 PreventRequestsDuringMaintenance::except() 配置)
- 注意
api 会先于 web 注册。若同一路径同时定义了 Web 和 API 路由,API 路由会优先匹配
- 向
pages 传入字符串会启用 Laravel Folio 的路由
withMiddleware() — 中间件自定义
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() 在 HttpKernel 被解析之后执行回调,这是因为使用了 afterResolving() 钩子。传给回调的 Middleware 对象提供了丰富的自定义方法。
->withMiddleware(function (Middleware $middleware) {
// 添加全局中间件
$middleware->append(MyGlobalMiddleware::class);
// 向 web 组添加中间件
$middleware->web(append: [EnsureUserIsSubscribed::class]);
// 替换 api 组中的中间件
$middleware->api(replace: [
OldMiddleware::class => NewMiddleware::class,
]);
// 配置 CSRF 排除路径
$middleware->validateCsrfTokens(except: ['stripe/*', 'webhook/*']);
// 修改未认证用户的跳转目标
$middleware->redirectGuestsTo('/custom-login');
// 配置中间件优先级
$middleware->priority([
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
]);
})
withExceptions() — 异常处理的配置
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() 首先将框架的 Handler 类作为单例注册,然后通过 afterResolving() 挂接回调。回调会收到一个 Exceptions 包装对象。
->withExceptions(function (Exceptions $exceptions) {
// 不上报特定异常
$exceptions->dontReport(MissedFlightException::class);
// 特定异常的自定义上报
$exceptions->report(function (InvalidOrderException $e) {
// 通知 Slack 等
});
// 自定义特定异常的 HTTP 响应
$exceptions->render(function (NotFoundHttpException $e, Request $request) {
if ($request->is('api/*')) {
return response()->json(['message' => 'Not Found'], 404);
}
});
// 节流(避免同一异常连续上报)
$exceptions->throttle(function (Throwable $e) {
return Limit::perMinute(20);
});
})
withProviders() — Service Provider 注册
public function withProviders(array $providers = [], bool $withBootstrapProviders = true)
{
RegisterProviders::merge(
$providers,
$withBootstrapProviders
? $this->app->getBootstrapProvidersPath()
: null
);
return $this;
}
由于 withProviders() 已在 Application::configure() 中被默认调用,bootstrap/providers.php 会自动加载。如需追加额外 Provider,需在 bootstrap/app.php 中显式调用。
Application::configure(basePath: dirname(__DIR__))
->withProviders([
// 除默认的 bootstrap/providers.php 之外再追加 Provider
App\Providers\CustomServiceProvider::class,
])
->withRouting(...)
->create();
若传入 withBootstrapProviders: false,将不再加载 bootstrap/providers.php。除非有特殊理由,否则请省略该参数。
其他主要方法
| 方法 | 说明 |
|---|
withKernels() | 单例注册 HTTP / Console Kernel。由 configure() 自动调用 |
withEvents() | 启用事件发现。由 configure() 自动调用 |
withCommands(array $commands) | 追加注册 Artisan 命令类或目录 |
withSchedule(callable $callback) | 在 bootstrap/app.php 中定义计划任务 |
withBroadcasting(string $channels) | 注册广播 channels 文件 |
withBindings(array $bindings) | 注册容器绑定 |
withSingletons(array $singletons) | 注册单例绑定 |
registered(callable $callback) | 添加在 Service Provider 注册后执行的回调 |
booting(callable $callback) | 添加在 booting 时执行的回调 |
booted(callable $callback) | 添加在 booted 后执行的回调 |
create() | 返回 Application 实例 |
设计意图:为何这样设计
「代码优先」的配置
Laravel 10 及之前的 app/Http/Kernel.php 采用以数组列举中间件的写法。这种写法接近配置文件,但难以享受 PHP 的类型系统与 IDE 支持。
Laravel 11 改为 withMiddleware(function (Middleware $middleware) { ... }) 这样的回调风格。这样能获得类型补全,也可以自然地使用条件分支、循环等逻辑进行动态配置。
从「约定优于配置」到「显式配置」
将 api.php 改为 opt-in,是为了避免应用不使用 API 路由时仍然始终加载 api 中间件组的问题。方针是:默认不存在未使用的功能。
afterResolving() 钩子的活用
withMiddleware() 和 withExceptions() 使用 afterResolving() 是为了避免配置顺序的问题。ApplicationBuilder 的方法在应用完全启动之前被调用,而实际处理(对 Kernel 应用配置)延迟到 Kernel 第一次被解析时执行。
自定义示例
API 与 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', // 从默认的 /api 修改
)
->withMiddleware(function (Middleware $middleware): void {
//
})
->withExceptions(function (Exceptions $exceptions): void {
//
})->create();
中间件的自定义
->withMiddleware(function (Middleware $middleware) {
// 为 Web 路由添加认证检查中间件
$middleware->web(append: [
\App\Http\Middleware\EnsureEmailIsVerified::class,
]);
// 从 API 路由中移除特定中间件
$middleware->api(remove: [
\Illuminate\Session\Middleware\StartSession::class,
]);
// 将 Webhook 端点从 CSRF 排除
$middleware->validateCsrfTokens(except: [
'webhook/*',
'stripe/webhook',
]);
// 为特定中间件设置别名
$middleware->alias([
'subscribed' => \App\Http\Middleware\EnsureUserIsSubscribed::class,
]);
})
将计划任务统一到 bootstrap/app.php
计划任务可以写在 routes/console.php 中,也可以用 withSchedule() 统一放到 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();
异常处理的自定义
->withExceptions(function (Exceptions $exceptions) {
// 对 API 请求始终返回 JSON
$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);
}
});
// 仅生产环境通知 Slack
if (app()->isProduction()) {
$exceptions->report(function (Throwable $e) {
app(SlackNotifier::class)->notify($e);
})->stop();
}
})
在 bootstrap/app.php 中管理容器绑定
对小型应用,简单的绑定也可以写在 bootstrap/app.php 而非 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();
Laravel 启动流程的顺序
Laravel 应用启动时,Service Provider 与 Application 的钩子按以下顺序执行。
- 所有 ServiceProvider 的
register() 执行
- Application 的
registered()
- Application 的
booting()
- 所有 ServiceProvider 的
boot() 执行
- Application 的
booted()
在 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');
});
}
}
ApplicationBuilder 上的 registered()、booting()、booted() 只是向 Application 注册回调的方法。通常无需使用,但可用于在启动准备完成的 Kernel 上通过 booted() 进行特殊变更。
// 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();
下一步
服务容器
理解 ApplicationBuilder 内部所使用的服务容器机制。
新应用结构 FAQ
汇总关于新应用结构的常见问题与解答。