> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Laravel 11 以後的應用程式結構

> 解說 Laravel 11 導入的 Slim Application Skeleton 全貌，以及 Application::configure() 到 ApplicationBuilder 的內部實作。

## 與 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`）   |
| 服務提供者          | 5 個檔案                        | `AppServiceProvider.php` 1 個檔案 |
| 路由檔            | 預設有 `web.php` / `api.php`    | 預設僅 `web.php`，`api.php` 為選用    |
| Bootstrap      | 設定分散於多個檔案                    | 集中於 `bootstrap/app.php`        |
| 預設 DB          | MySQL/PostgreSQL             | SQLite                         |

<Info>
  此變更**針對新專案**。既有的 Laravel 10 應用程式升級後，舊有結構仍可原樣運作。
</Info>

## 新的目錄與檔案結構

### Skeleton 的目錄結構

```
laravel-app/
├── app/
│   ├── Http/
│   │   └── Controllers/
│   ├── Models/
│   │   └── User.php
│   └── Providers/
│       └── AppServiceProvider.php
├── bootstrap/
│   ├── app.php          ← 應用程式設定的中心
│   ├── cache/
│   └── providers.php    ← 服務提供者清單
├── config/
├── database/
├── public/
│   └── index.php        ← 進入點
├── resources/
├── routes/
│   ├── web.php          ← Web 路由（預設）
│   └── console.php      ← Artisan 指令與排程
├── storage/
└── tests/
```

### `bootstrap/app.php` — 應用程式設定的中心

```php theme={null}
<?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` — 服務提供者清單

```php theme={null}
<?php

use App\Providers\AppServiceProvider;

return [
    AppServiceProvider::class,
];
```

此檔案為服務提供者的註冊位置。在 Laravel 10 中原本寫在 `config/app.php` 的 `providers` 陣列，現已分離至 `bootstrap/providers.php`。Laravel 11 的預設僅有 `AppServiceProvider`。

<Tip>
  以 `composer require` 安裝套件時，該套件可能會自動更新 `bootstrap/providers.php`。並不是 `config/app.php` 已不再被參考，而是新的註冊建議寫在 `bootstrap/providers.php`。
</Tip>

### `routes/` 目錄的變更

```
routes/
├── web.php      ← Web 路由（預設會被載入）
└── console.php  ← 定義 Artisan 指令與排程
```

預設不存在 `api.php` 與 `channels.php`。可依需求以 Artisan 指令產生。

```shell theme={null}
# 加入 API 路由（安裝 api.php + Sanctum）
php artisan install:api

# 加入 Broadcasting（安裝 channels.php + Reverb 等）
php artisan install:broadcasting
```

`routes/console.php` 中也可以定義排程。

```php theme={null}
<?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();
```

### 已廢除的檔案

<AccordionGroup>
  <Accordion title="app/Http/Kernel.php 的廢除">
    HTTP Kernel 已整合至框架內部的 `Illuminate\Foundation\Http\Kernel`。中介層的自訂改在 `bootstrap/app.php` 的 `withMiddleware()` 中進行。

    ```php theme={null}
    // Laravel 10 以前：app/Http/Kernel.php
    protected $middleware = [
        \Illuminate\Http\Middleware\TrustProxies::class,
        // ...
    ];

    protected $middlewareGroups = [
        'web' => [
            \App\Http\Middleware\EncryptCookies::class,
            // ...
        ],
    ];
    ```

    ```php theme={null}
    // Laravel 11 以後：bootstrap/app.php
    ->withMiddleware(function (Middleware $middleware) {
        $middleware->web(append: [
            EnsureUserIsSubscribed::class,
        ]);

        $middleware->validateCsrfTokens(except: ['stripe/*']);
    })
    ```
  </Accordion>

  <Accordion title="app/Console/Kernel.php 的廢除">
    Console Kernel 的兩項職責已被分離。Artisan 指令置於 `app/Console/Commands/` 會被自動偵測，排程則寫在 `routes/console.php`。

    ```php theme={null}
    // Laravel 10 以前：app/Console/Kernel.php
    protected function schedule(Schedule $schedule): void
    {
        $schedule->command('emails:send')->daily();
    }
    ```

    ```php theme={null}
    // Laravel 11 以後：routes/console.php
    use Illuminate\Support\Facades\Schedule;

    Schedule::command('emails:send')->daily();
    ```
  </Accordion>

  <Accordion title="app/Exceptions/Handler.php 的廢除">
    例外處理器已整合至框架內部的 `Illuminate\Foundation\Exceptions\Handler`。自訂改在 `bootstrap/app.php` 的 `withExceptions()` 中進行。

    ```php theme={null}
    // Laravel 10 以前：app/Exceptions/Handler.php
    public function register(): void
    {
        $this->reportable(function (InvalidOrderException $e) {
            // ...
        });
    }
    ```

    ```php theme={null}
    // Laravel 11 以後：bootstrap/app.php
    ->withExceptions(function (Exceptions $exceptions) {
        $exceptions->report(function (InvalidOrderException $e) {
            // ...
        });
    })
    ```
  </Accordion>
</AccordionGroup>

## `Application::configure()` 的運作機制

### 框架內部的實作

`Application::configure()` 是 `Illuminate\Foundation\Application` 的靜態方法。

```php theme={null}
// 出自 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();
}
```

該方法會執行下列處理：

1. 依 `basePath` 決定應用程式的根目錄
2. 建立 `Application` 實例
3. 以 `ApplicationBuilder` 包裝，套用預設設定
4. 回傳 `ApplicationBuilder` 實例

要注意的是，在 `configure()` 內**已呼叫了 `withKernels()` / `withEvents()` / `withCommands()` / `withProviders()`**。在 `bootstrap/app.php` 中無需再次呼叫。

### 以方法鏈進行設定的流程

```php theme={null}
Application::configure(basePath: dirname(__DIR__))  // 回傳 ApplicationBuilder
    ->withRouting(...)       // 設定路由並回傳 $this
    ->withMiddleware(...)    // 設定中介層並回傳 $this
    ->withExceptions(...)    // 設定例外處理並回傳 $this
    ->create();              // 回傳 Application 實例
```

呼叫 `create()` 後從 `ApplicationBuilder` 取出 `Application` 實例，而 `bootstrap/app.php` 所 `return` 的即是此 `Application` 實例。

## 從請求到應用程式啟動的流程

```mermaid theme={null}
flowchart TD
    A["public/index.php<br>進入點"] --> B["require bootstrap/app.php<br>取得 Application"]
    B --> C["Application::configure()<br>建立 ApplicationBuilder"]
    C --> D["withKernels()<br>註冊 HTTP/Console Kernel"]
    D --> E["withEvents()<br>設定事件發現"]
    E --> F["withCommands()<br>設定 Artisan 指令路徑"]
    F --> G["withProviders()<br>載入 bootstrap/providers.php"]
    G --> H["withRouting()<br>設定路由"]
    H --> I["withMiddleware()<br>設定中介層"]
    I --> J["withExceptions()<br>設定例外處理"]
    J --> K["create()<br>回傳 Application 實例"]
    K --> L["HTTP Kernel 處理請求<br>中介層 → Router → Controller"]
    L --> M["回傳 Response"]
```

`public/index.php` 為進入點，載入 `bootstrap/app.php` 以取得 `Application`。之後 HTTP Kernel 將請求通過中介層的 Pipeline，Router 再派送至 Controller。

## `ApplicationBuilder` 主要方法深入探討

### `withRouting()` — 路由註冊的內部處理

```php theme={null}
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
)
```

內部會將 callback 註冊至 `AppRouteServiceProvider::loadRoutesUsing()`，並在應用程式 booting 時註冊 `AppRouteServiceProvider`。

```php theme={null}
// 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](https://github.com/laravel/folio) 的路由

### `withMiddleware()` — 中介層自訂

```php theme={null}
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` 被解析**之後**執行 callback。這是因為使用了 `afterResolving()` hook。傳入 callback 的 `Middleware` 物件擁有豐富的自訂方法。

```php theme={null}
->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()` — 例外處理設定

```php theme={null}
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` 類別註冊為 singleton，再以 `afterResolving()` 設定 callback。傳給 callback 的是 `Exceptions` wrapper 物件。

```php theme={null}
->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()` — 服務提供者註冊

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

    return $this;
}
```

因為 `withProviders()` 於 `Application::configure()` 內已預設呼叫，`bootstrap/providers.php` 會自動被載入。若要加入額外的提供者，需在 `bootstrap/app.php` 中明確呼叫。

```php theme={null}
Application::configure(basePath: dirname(__DIR__))
    ->withProviders([
        // 於預設的 bootstrap/providers.php 之外新增提供者
        App\Providers\CustomServiceProvider::class,
    ])
    ->withRouting(...)
    ->create();
```

<Warning>
  若傳入 `withBootstrapProviders: false`，`bootstrap/providers.php` 將不會被載入。若無特殊理由請予以省略。
</Warning>

### 其他主要方法

| 方法                                   | 說明                                                        |
| ------------------------------------ | --------------------------------------------------------- |
| `withKernels()`                      | 以 singleton 註冊 HTTP / Console Kernel。由 `configure()` 自動呼叫 |
| `withEvents()`                       | 啟用事件發現。由 `configure()` 自動呼叫                               |
| `withCommands(array $commands)`      | 追加註冊 Artisan 指令類別或目錄                                      |
| `withSchedule(callable $callback)`   | 在 `bootstrap/app.php` 定義排程                                |
| `withBroadcasting(string $channels)` | 註冊 Broadcasting 頻道檔案                                      |
| `withBindings(array $bindings)`      | 註冊 Container 綁定                                           |
| `withSingletons(array $singletons)`  | 註冊 singleton 綁定                                           |
| `registered(callable $callback)`     | 追加服務提供者註冊後執行的 callback                                    |
| `booting(callable $callback)`        | 追加 booting 時執行的 callback                                  |
| `booted(callable $callback)`         | 追加 booted 時執行的 callback                                   |
| `create()`                           | 回傳 `Application` 實例                                       |

## 設計意圖：為什麼如此設計

### 「Code First」的設定

Laravel 10 以前的 `app/Http/Kernel.php` 採以陣列列舉中介層的方式。這種寫法接近設定檔，缺點是不易獲得 PHP 型別系統及 IDE 的支援。

Laravel 11 改為 `withMiddleware(function (Middleware $middleware) { ... })` 的 callback 風格。如此便可享有型別補完，也能自然地寫出使用條件分支或迴圈等邏輯的動態設定。

### 從「慣例優於設定」到「明確的設定」

`api.php` 之所以改為選用，是為了解決即便應用程式未使用 API 路由，`api` 中介層群組仍會一直被載入的問題。方針是：預設不存在不使用的功能。

### `afterResolving()` hook 的活用

`withMiddleware()` 與 `withExceptions()` 中使用 `afterResolving()`，是為了避免設定順序上的問題。`ApplicationBuilder` 的方法會在應用程式完全啟動前呼叫，實際的處理（對 Kernel 套用設定）則在 Kernel 首次被解析時延遲執行。

## 自訂範例

### 讓 API 與 Web 並存

```php theme={null}
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();
```

### 中介層的自訂

```php theme={null}
->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`。

```php theme={null}
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();
```

### 例外處理的自訂

```php theme={null}
->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` 管理 Container 綁定

若為小型應用程式，也可將簡單的綁定寫在 `bootstrap/app.php` 而非 `AppServiceProvider`。

```php theme={null}
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 應用程式啟動時，服務提供者與 Application 的 hook 按下列順序執行。

1. 執行所有 ServiceProvider 的 `register()`
2. Application 的 `registered()`
3. Application 的 `booting()`
4. 執行所有 ServiceProvider 的 `boot()`
5. Application 的 `booted()`

於 `AppServiceProvider` 加入下列程式碼即可確認執行順序。

```php theme={null}
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 註冊 callback 的方法。通常不需使用，但可用於例如在 `booted()` 中對啟動準備完成的 Kernel 進行變更等特殊操作。

```php theme={null}
// 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();
```

## 下一步

<Card title="服務容器" icon="box" href="/zh-TW/service-container">
  了解 `ApplicationBuilder` 內部所使用的服務容器機制。
</Card>

<Card title="新應用結構 FAQ" icon="circle-question" href="/zh-TW/advanced/app-structure-faq">
  彙整新應用程式結構的常見疑問與解答。
</Card>


## Related topics

- [Laravel 11 以後的新應用程式結構 FAQ](/zh-TW/advanced/app-structure-faq.md)
- [從舊結構到新結構的遷移指南](/zh-TW/advanced/app-structure-migration.md)
- [從 Laravel 10 升級到 11](/zh-TW/blog/upgrade-10-to-11.md)
- [目錄結構](/zh-TW/directory-structure.md)
- [Laravel Boost](/zh-TW/boost.md)
