【Laravel入門】20分鐘搞懂Laravel的控制器Controller

【Laravel入門】20分鐘搞懂Laravel的控制器Controller

在 MVC 架構中所有的流程控制以及視圖和資料都由控制器來進行處理,除此之外為了之後的快取以提升請求效率,應盡量避免在路由檔案中以 Closure 形式定義所有的請求處理邏輯,而多使用控制器類別來組織這些行為

控制器能將相關的請求處理邏輯統整成一個單獨的類別。比如 UserController 類別能處理所有關於使用者的請求,包含了顯示. 創建. 更新以及刪除使用者。預設情況下,控制器被存放在 app/Http/Controllers 資料夾中

撰寫控制器

基本型控制器

下面是一個基礎控制器類別的範例。需注意該控制器繼承了 Laravel 的基礎控制器 App\Http\Controllers\Controller。該控制器類別提供了一些便利的方法,如 middleware(),可為控制器行為添加中介層:

//app\Http\Controllers\UserController.php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use App\Models\User;

class UserController extends Controller
{
    /**
     * Show the profile for a given user.
     *
     * @param  int  $id
     * @return \Illuminate\View\View
     */
    public function show($id)
    {
        return view('user.profile', [
            'user' => User::findOrFail($id)
        ]);
    }
}

把焦點拉到路由檔案,你能夠定義一個路由指向到控制器的方法,像這樣:

//routes\web.php

use App\Http\Controllers\UserController;
//8.x寫法
Route::get('/user/{id}', [UserController::class, 'show']);
//7.x寫法
Route::get('/user/{id}','App\Http\Controllers\UserController@show');

當一個請求與指定路由的 URI 相符時, UserController中的 show() 將會執行,路由參數也將會被傳遞進去。所謂相符指的是請求動詞與網址路徑都相符

技巧:

控制器並非必須繼承基礎類別,但如果控制器沒有繼承基礎類別,你將無法使用一些好用的功能,比如 middleware,validate,和 dispatch 等方法。結論:沒事別作死!

單一方法控制器

假如一個控制器的方法內容過於複雜,你也許會想讓整個控制器只放一個方法。 為此,你可以在控制器中放置一個 __invoke() :

//app\Http\Controllers\ProvisionServer.php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use App\Models\User;

class ProvisionServer extends Controller
{
    /**
     * Provision a new web server.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function __invoke()
    {
        // ...
    }
}

當為只有一個方法的控制器去註冊路由時,不需要指名控制器的方法。相對的,你只需要傳入控制器的名稱到路由:

\\routes\web.php

use App\Http\Controllers\ProvisionServer;

Route::post('/server', ProvisionServer::class);

你可以通過 Artisan 命令裡的 make:controller 命令中的 --invokable 選項來生成一個只有單個方法的控制器

php artisan make:controller ProvisionServer --invokable

技巧:

可以使用Stub自定義來修改控制器模板,詳情請參考 Stub 快速入門

控制器中介層

中介層可以在路由檔案中分配給控制器的路由:

//routes\web.php

Route::get('profile', [UserController::class, 'show'])->middleware('auth');

不過,在控制器的建構子中指定中介層更方便。使用控制器建構子中的 middleware() ,可以輕鬆地將中介層分配給控制器。你甚至可以將中介層設定為只針對控制器中的某些方法有效:

//app\Http\Controllers\UserController.php

class UserController extends Controller
{
    /**
     * Instantiate a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('auth'); //為所有控制器方法加入 auth 中介層
        $this->middleware('log')->only('index');  //為 index 控制器方法加入 log 中介層
        $this->middleware('subscribed')->except('store'); //為除 store 之外的控制器方法加入 subscribed 中介層
    }
}

同時,控制器還允許你直接使用一個 Closure 來註冊中介層。這為不定義整個中介層類別的情況下(通常是因為這個中介層只有這裡會用到)為單個控制器定義中介層提供了一種便捷的方法:

$this->middleware(function ($request, $next) {
    return $next($request);
});

資源型控制器

假如你把每一個 Eloquent 模型視為你應用的資源,也就是資料庫的表格。那麼它們普遍就像是資源一樣被執行相同的操作。例如,想像一下你的應用有 Photo 和 Movie 這兩個模型,對應到 photos 和 movies 表格,那麼很可能使用者會需要能夠去新增.閱讀.變更或刪除這些資源

Laravel 的資源路由通過單行代碼即可將典型的「CURD (增刪改查)」路由分配給控制器。例如,你希望創建一個控制器來處理保存 「照片」 應用的所有 HTTP 請求。使用 Artisan 命令 make:controller 可以快速創建這樣一個控制器:

因為這種常見的需求,我們可以使用 make:controller 命令並加上 --resource 選項來快速生成一個資源型控制器,裡頭包含了處理 CRUD 的7個方法

php artisan make:controller PhotoController --resource

這個命令將生成 app/Http/Controllers/PhotoController.php,而這個控制器裡的每個方法都將對應一個資源操作。

下一步,你需要註冊一個資源路由來指定到這個控制器:

//routes\web.php

use App\Http\Controllers\PhotoController;

Route::resource('photos', PhotoController::class);

這個單一的路由聲明創建了多個路由來處理資源上的各種行為。而剛生成的控制器也為每種行為保留了對應方法。記住,你總是可以透過執行 route:list 命令來瀏覽當前應用的所有路由規則

你也可以通過將陣列傳入到 resources() 的方式來一次性的註冊多個資源控制器:

//routes\web.php

Route::resources([
    'photos' => PhotoController::class,
    'posts' => PostController::class,
]);

資源控制器操作處理

這裡以資源名 photo 來舉例

請求動詞 URI 方法名稱Action 路由名稱
GET /photos index photos.index
GET /photos/create create photos.create
POST /photos store photos.store
GET /photos/{photo} show photos.show
GET /photos/{photo}/edit edit photos.edit
PUT/PATCH /photos/{photo} update photos.update
DELETE /photos/{photo} destroy photos.destroy

指定模型資源(進階技巧)

如果你使用了路由模型綁定,並且想在資源控制器的方法中使用型別提示,你可以在生成控制器的時候加入 --model 選項,如下例:

php artisan make:controller PhotoController --resource --model=Photo

部分資源路由(進階技巧)

當註冊資源路由時,你可以指定控制器只處理部分行為,而不是預設的所有行為:

//routes\web.php

use App\Http\Controllers\PhotoController;

Route::resource('photos', PhotoController::class)->only([
    'index', 'show'
]);

Route::resource('photos', PhotoController::class)->except([
    'create', 'store', 'update', 'destroy'
]);

API 資源路由

當註冊用於 APIs 的資源路由時,通常需要排除顯示 HTML 畫面的路由(如 create 和 edit )。為方便起見,你可以使用 apiResource() 自動排除這兩個路由:

//routes\api.php

use App\Http\Controllers\PhotoController;

Route::apiResource('photos', PhotoController::class);

和剛才一樣,你也可以傳遞一個陣列給 apiResources() 來同時註冊多個 API 資源控制器:

//routes\api.php
use App\Http\Controllers\PhotoController;
use App\Http\Controllers\PostController;

Route::apiResources([
    'photos' => PhotoController::class,
    'posts' => PostController::class,
]);

要快速生成不包含 create 或 edit 方法的用於開發介面的資源控制器,請在執行 make:controller 命令以生成控制器時加入 --api 選項:

php artisan make:controller API/PhotoController --api

架構子依賴注入

Laravel服務容器被用於解析所有的 Laravel 控制器。因此你可以在控制器的建構子中使用型別提示需要的依賴。聲明的依賴將會被自動解析並注入到控制器實例中:

//
namespace App\Http\Controllers;

use App\Repositories\UserRepository;

class UserController extends Controller
{
    /**
     * The user repository instance.
     */
    protected $users;

    /**
     * Create a new controller instance.
     *
     * @param  \App\Repositories\UserRepository  $users
     * @return void
     */
    public function __construct(UserRepository $users)
    {
        $this->users = $users;
    }
}

方法注入

除了建構子注入以外,你也可以在控制器的方法中做型別提示依賴。最常見的用法便是注入 Illuminate\Http\Request 到控制器方法中:

像下面這個例子,透過方法注入,store() 就能得到 Request 物件,裏頭包含請求相關的資料,比如使用者所輸入的資料

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class UserController extends Controller
{

    public function store(Request $request)
    {
        $name = $request->name;

        //
    }
}

如果你的控制器方法要取得路由參數,請務必在依賴之後再列出路由參數。例如像這樣定義路由:

//routes\web.php

use App\Http\Controllers\UserController;

Route::put('/user/{id}', [UserController::class, 'update']);

你就能夠像以下的寫法來用依賴注入解析 Illuminate\Http\Request 並取得路由參數:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class UserController extends Controller
{
    /**
     * Update the given user.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  string  $id
     * @return \Illuminate\Http\Response
     */
    public function update(Request $request, $id)
    {
        //
    }
}

進階技巧

以下內容適合已經入門的人來閱讀

多層資源

有時你可能需要定義一個多層的資源型路由。例如,每張照片資源可以被添加了多個評論。那麼可以在路由中使用點語法來註冊資源型控制器:

use App\Http\Controllers\PhotoCommentController;

Route::resource('photos.comments', PhotoCommentController::class);

該路由會註冊一個多層資源,可使用如下 URI 訪問:

/photos/{photo}/comments/{comment}

多層資源範圍限定

Laravel 的 隱式模型綁定 特性可以自動限定多層綁定的範圍,因此已解析的子模型會自動屬於父模型 ,比如剛才的例子所抓取的評論都會是該張照片的。定義多層資源路由時,使用 scoped() 可以開啟自動範圍限定,也可以指定 Laravel 應該要用哪個欄位去查詢子模型資源

淺層多層

通常並不完全需要在 URI 中同時擁有父 ID 和子 ID ,因為子 ID 已經是唯一的辨識符。當使用辨識符(如自動遞增的主鍵)來指定 URI 中的模型時,可以選擇使用「淺層多層」的方式定義路由:

use App\Http\Controllers\CommentController;

Route::resource('photos.comments', CommentController::class)->shallow();

上面的路由定義方式會定義以下路由:

請求動詞 URI 方法 路由名稱
GET /photos/{photo}/comments index photos.comments.index
GET /photos/{photo}/comments/create create photos.comments.create
POST /photos/{photo}/comments store photos.comments.store
GET /comments/{comment} show comments.show
GET /comments/{comment}/edit edit comments.edit
PUT/PATCH /comments/{comment} update comments.update
DELETE /comments/{comment} destroy comments.destroy

命名資源路由

預設所有的資源控制器行為都有一個路由名稱,但你可以傳入 names 陣列來複寫這些名稱:

use App\Http\Controllers\PhotoController;

Route::resource('photos', PhotoController::class)->names([
    'create' => 'photos.build'
]);

命名資源路由參數

預設 Route::resource() 會根據資源名稱的「單數」形式去創建資源路由的路由參數。你可以在選項陣列中傳入 parameters 參數來輕鬆地覆寫每個資源。parameters 陣列應該是資源名稱和參數名稱的的鍵值對陣列:

use App\Http\Controllers\AdminUserController;

Route::resource('users', AdminUserController::class)->parameters([
    'users' => 'admin_user'
]);

上面這個例子將會將資源的 show 路由轉成以下的 URI :

/users/{admin_user}

限定欄位的多層資源路由

Laravel的範圍隱式模型綁定功能能夠自動限縮多層綁定讓被解析出來的子模型必定屬於父模型。透過在定義多層資源時使用 scoped() ,就能啟動自動限定欄位並告知 Laravel 該用哪個欄位來查找子資源

use App\Http\Controllers\PhotoCommentController;

Route::resource('photos.comments', PhotoCommentController::class)->scoped([
    'comment' => 'slug',
]);

這個路由將會註冊一個限定欄位的多層資源路由,可透過以下的 URIs 來進行訪問

/photos/{photo}/comments/{comment:slug}

當使用一個自定義鍵的隱式綁定作為多層路由參數時,Laravel 會自動限定查詢範圍,按照預設的命名方式去父類別中查找關聯方法名稱,然後檢索到對應的多層模型。在這種情況下將假定 Photo 模型有一個叫 comments(路由參數名的複數)的關聯方法,通過這個方法可以查找到 Comment 模型

資源 URIs 本地化

預設 Route::resource() 將會用英文常用動詞來創建資源 URI。如果希望能自定義 create 和 edit 行為的方法名稱,可以在 app/Providers/AppServiceProvider.php 的 boot() 中使用 Route::resourceVerbs() 來修改。但因為無法改成中文,對我們基本沒用

//app\Providers\AppServiceProvider.php

public function boot()
{
    Route::resourceVerbs([
        'create' => 'crear',
        'edit' => 'editar',
    ]);

    // ...
}

一旦動詞完成自定義,當像這樣 Route::resource('fotos', 'PhotoController') 的資源路由被註冊時就會生成以下的 URIs:

/fotos/crear

/fotos/{foto}/editar

補充資源控制器

如果需要增加額外的路由到預設的資源路由組合之中,需要在呼叫 Route::resource() 前先定義它們;否則 resource() 定義的路由可能會優先於你定義的路由。

懶人包

自定義的路由一律寫在 resource() 之前

use App\Http\Controller\PhotoController;

Route::get('/photos/popular', [PhotoController::class, 'popular']);
Route::resource('photos', PhotoController::class);

技巧

記得保持控制器的最小化。如果您需要典型的資源操作以外的方法,請考慮將控制器分割為兩個更小的控制器


分享這篇文章:

關聯文章:

訂閱電子報,索取 Laravel 學習手冊

價值超過 3000 元,包含常用 Laravel 語法與指令!

Laravel 百萬年薪特訓營

從最基礎的 PHP 語法開始,包含所有你該知道的網頁基礎知識,連同 Laravel 從零開始一直到實戰,最後還將告訴你如何找好工作,讓你及早擁有百萬年薪