Calvert's murmur

Laravel 路由群組:6 個組織路由的技巧

2022-10-27

約 6873 字 / 需 38 分鐘閱讀

原文:Povilas KoropLaravel Route Grouping: 6 Techniques to Organize Routes

Laravel 路由是開發者從一開始就學到的功能。但是隨著專案的成長,管理不斷增加的路由檔案及找到正確的 Route::get() 定義變得越來越困難。幸運的是,有一些技巧可以讓路由變得更簡短、易讀,用不同的方式對路由及其設定組成群組。讓我們來看看吧!

不過,我們會更深入一點,而不是只探討初學者等級的 Route::group()

群組 1. Route::resource 和 Route::apiResource

讓我們從一個常被忽視的問題開始吧,這應該是個眾所皆知的群組。如果你對模型有一組典型的 CRUD 操作,可以將它們用 resource 控制器組成群組。

這個控制器最多可以涵蓋 7 種方法(實際應用上可能更少):

  • index()
  • create()
  • store()
  • show()
  • edit()
  • update()
  • destroy()

所以,如果你有一組路由對應到這些方法,請不要這樣寫:

Route::get('books', [BookController::class, 'index'])->name('books.index');
Route::get('books/create', [BookController::class, 'create'])->name('books.create');
Route::post('books', [BookController::class, 'store'])->name('books.store');
Route::get('books/{book}', [BookController::class, 'show'])->name('books.show');
Route::get('books/{book}/edit', [BookController::class, 'edit'])->name('books.edit');
Route::put('books/{book}', [BookController::class, 'update'])->name('books.update');
Route::delete('books/{book}', [BookController::class, 'destroy'])->name('books.destroy');

你只需要一行:

Route::resource('books', BookController::class);

如果你的是 API 專案,則不需要用於建立或編輯的視覺化表單。因此, 你可以使用 apiResource() 語法,它涵蓋了 7 種方法中的 5 種:

Route::apiResource('books', BookController::class);

另外,即便你只有使用 2 到 4 種方法而不是完整的 7 個,我依然建議你使用 resource 控制器。因為它保留了用於 URL、方法和路由名稱的標準命名慣例。例如,在這種情況下,你不需要手動提供名稱:

Route::get('books/create', [BookController::class, 'create'])->name('books.create');
Route::post('books', [BookController::class, 'store'])->name('books.store');
 
// 相反地,這裡的 "books.create" 和 "books.store" 名稱是自動分配的
Route::resource('books', BookController::class)->only(['create', 'store']);

群組 2. 群組中的群組

當然,一般的路由群組大家都知道。但對於更複雜的專案,單一階層的群組可能不夠。

舉個實際的例子,你想要使用 auth 中介層對需要授權的路由組成群組,但是在裡面你需要區隔更多的子群組,例如管理員和一般用戶。

Route::middleware('auth')->group(function() {
 
    Route::middleware('is_admin')->prefix('admin')->group(function() {
        Route::get(...) // 管理員路由
    });
 
    Route::middleware('is_user')->prefix('user')->group(function() {
        Route::get(...) // 一般用戶路由
    });
});

群組 3. 將重複的中介層組成群組

如果你有很多中介層,其中一些在幾個路由群組重複出現怎麼辦?

Route::prefix('students')->middleware(['auth', 'check.role', 'check.user.status', 'check.invoice.status', 'locale'])->group(function () {
    // ... 學生路由
});
 
Route::prefix('managers')->middleware(['auth', 'check.role', 'check.user.status', 'locale'])->group(function () {
    // ... 管理員路由
});

如你所見,5 個中介層有 4 個重複。因此,我們可以在 app/Http/Kernel.php 檔案內將這 4 個中介層移動到獨立的中介層群組中:

protected $middlewareGroups = [
    // Laravel 預設群組
    '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,
    ],
 
    // Laravel 預設群組
    'api' => [
        // \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
        'throttle:api',
        \Illuminate\Routing\Middleware\SubstituteBindings::class,
    ],
 
    // 這是我們新的中介層群組
    'check_user' => [
        'auth',
        'check.role',
        'check.user.status',
        'locale'
    ],
];

我們將中介層群組命名為 check_user,現在我們可以縮短路由:

Route::prefix('students')->middleware(['check_user', 'check.invoice.status'])->group(function () {
    // ... 學生路由
});
 
Route::prefix('managers')->middleware(['check_user'])->group(function () {
    // ... 管理員路由
});

群組 4. 相同名稱但不同命名空間的控制器

還有一個常見的狀況是,用於不同用戶角色的 HomeController,例如 Admin/HomeControllerUser/HomeController。如果你在路由中使用完整路徑,看起來會像這樣:

Route::prefix('admin')->middleware('is_admin')->group(function () {
    Route::get('home', [\App\Http\Controllers\Admin\HomeController::class, 'index']);
});
 
Route::prefix('user')->middleware('is_user')->group(function () {
    Route::get('home', [\App\Http\Controllers\User\HomeController::class, 'index']);
});

使用完整路徑需要打很多字,對吧?這就是為什麼許多開發者更偏好在路由清單中只有一個 HomeController::class,並在最上面這樣寫:

use App\Http\Controllers\Admin\HomeController;

但問題是我們有相同的控制器類別名稱!所以,這樣行不通:

use App\Http\Controllers\Admin\HomeController;
use App\Http\Controllers\User\HomeController;

哪個才是主要的呢?好吧,其中一種方法是更改名稱並將別名指定給其中一個:

use App\Http\Controllers\Admin\HomeController as AdminHomeController;
use App\Http\Controllers\User\HomeController;
 
Route::prefix('admin')->middleware('is_admin')->group(function () {
    Route::get('home', [AdminHomeController::class, 'index']);
});
 
Route::prefix('user')->middleware('is_user')->group(function () {
    Route::get('home', [HomeController::class, 'index']);
});

但是,就我個人而言,在上面更改名稱會讓我很困惑,我喜歡另一種方法:為控制器的子資料夾加上一個 namespace()

Route::prefix('admin')->namespace('App\Http\Controllers\Admin')->middleware('is_admin')->group(function () {
    Route::get('home', [HomeController::class, 'index']);
    // ... Admin 命名空間中的其他控制器
});
 
Route::prefix('user')->namespace('App\Http\Controllers\User')->middleware('is_user')->group(function () {
    Route::get('home', [HomeController::class, 'index']);
    // ... User 命名空間中的其他控制器
});

群組 5. 獨立的路由檔案

如果你覺得 routes/web.phproutes/api.php 變得太大,你可以把一些路由放到隨你命名的獨立檔案中,例如 routes/admin.php

然後,你有兩種方式可以使用該檔案:我稱之為「Laravel 的方式」和「PHP 的方式」。

如果你想遵循 Laravel 建構其預設路由檔案的結構,它在 app/Providers/RouteServiceProvider.php 中:

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

可以看到,routes/api.phproutes/web.php 都在這,但設定略有不同。因此,你需要做的就是在這邊加上 admin 檔案:

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

但是如果你不想深入到服務提供者的話,有個簡單的方法,只需要將你的路由檔案 include 或 require 到另一個檔案中,就像在 Laravel 框架之外的任何 PHP 檔案中所做的那樣。

事實上,這是 Taylor Otwell 所做的,直接將 routes/auth.php 檔案放到 Laravel Breeze 路由中:

routes/web.php
Route::get('/', function () { return view('welcome'); }); Route::get('/dashboard', function () { return view('dashboard'); })->middleware(['auth', 'verified'])->name('dashboard'); require __DIR__.'/auth.php';

群組 6. Laravel 9 的新功能:Route::controller()

如果控制器中有些方法未遵循標準的資源結構,你仍然可以將它們組成群組,而無需為每個方法重複控制器名稱。

原本這樣的寫法:

Route::get('profile', [ProfileController::class, 'getProfile']);
Route::put('profile', [ProfileController::class, 'updateProfile']);
Route::delete('profile', [ProfileController::class, 'deleteProfile']);

可以改成以下寫法:

Route::controller(ProfileController::class)->group(function() {
    Route::get('profile', 'getProfile');
    Route::put('profile', 'updateProfile');
    Route::delete('profile', 'deleteProfile');
});

可以在 Laravel 9 和 Laravel 8 的最新次要版本中使用此功能。


就是這樣!無論你的專案有多大,希望這些群組技巧將有助於你組織和維護路由。