控制器

新建控制器檔案 app/controller/FooController.php

<?php
namespace app\controller;

use support\Request;

class FooController
{
    public function index(Request $request)
    {
        return response('hello index');
    }

    public function hello(Request $request)
    {
        return response('hello webman');
    }
}

當存取 http://127.0.0.1:8787/foo 時,頁面會回傳 hello index

當存取 http://127.0.0.1:8787/foo/hello 時,頁面會回傳 hello webman

當然你可以透過路由設定來變更路由規則,詳見路由

提示
如果出現 404 無法存取,請開啟 config/app.php,將 controller_suffix 設為 Controller,並重新啟動。

控制器後綴

從 webman 1.3 版本開始,支援在 config/app.php 設定控制器後綴。若 config/app.php 中的 controller_suffix 設為空字串 '',則控制器類似如下:

app\controller\Foo.php

<?php
namespace app\controller;

use support\Request;

class Foo
{
    public function index(Request $request)
    {
        return response('hello index');
    }

    public function hello(Request $request)
    {
        return response('hello webman');
    }
}

強烈建議將控制器後綴設為 Controller,這樣能避免控制器與模型類名衝突,同時增加安全性。

說明

  • 框架會自動向控制器傳遞 support\Request 物件,透過它可以獲取用戶輸入資料(get、post、header、cookie 等),詳見請求
  • 控制器可以回傳數字、字串或 support\Response 物件,但不能回傳其他類型的資料。
  • support\Response 物件可以透過 response()json()xml()jsonp()redirect() 等助手函數建立。

控制器參數綁定

例子

webman 支援透過控制器方法參數自動綁定請求參數,例如:

<?php
namespace app\controller;
use support\Response;

class UserController
{
    public function create(string $name, int $age): Response
    {
        return json(['name' => $name, 'age' => $age]);
    }
}

你可以透過 GETPOST 傳遞 nameage 的值,也可以透過路由參數傳遞,例如:

Route::any('/user/{name}/{age}', [app\controller\UserController::class, 'create']);

優先級為:路由參數 > GET > POST 參數

預設值

假設我們存取 /user/create?name=tom,會得到以下錯誤:

Missing input parameter age

原因是我們沒有傳遞 age 參數,可以透過為參數設定預設值來解決,例如:

<?php
namespace app\controller;
use support\Response;

class UserController
{
    public function create(string $name, int $age = 18): Response
    {
        return json(['name' => $name, 'age' => $age]);
    }
}

參數類型

當我們存取 /user/create?name=tom&age=not_int,會得到以下錯誤:

提示
這裡為方便測試,我們直接在瀏覽器網址列輸入參數,實際開發中應該透過 POST 方式傳遞參數

Input age must be of type int, string given

這是因為接收的資料會按類型進行轉換,若無法轉換則會拋出 support\exception\InputTypeException 例外。由於傳遞的 age 參數無法轉換為 int 類型,所以會得到上述錯誤。

自訂錯誤

我們可以利用多語言自訂 Missing input parameter ageInput age must be of type int, string given 這類錯誤訊息,請參考以下指令:

composer require symfony/translation
mkdir resource/translations/zh_CN/ -p
echo "<?php
return [
    'Input :parameter must be of type :exceptType, :actualType given' => '輸入參數 :parameter 必須是 :exceptType 類型,傳遞的類型是 :actualType',
    'Missing input parameter :parameter' => '缺少輸入參數 :parameter',
];" > resource/translations/zh_CN/messages.php
php start.php restart

其他類型

webman 支援的參數類型有 intfloatstringboolarrayobject類實例 等,例如:

<?php
namespace app\controller;
use support\Response;

class UserController
{
    public function create(string $name, int $age, float $balance, bool $vip, array $extension): Response
    {
        return json([
            'name' => $name,
            'age' => $age,
            'balance' => $balance,
            'vip' => $vip,
            'extension' => $extension,
        ]);
    }
}

當我們存取 /user/create?name=tom&age=18&balance=100.5&vip=true&extension[foo]=bar,會得到以下結果:

{
  "name": "tom",
  "age": 18,
  "balance": 100.5,
  "vip": true,
  "extension": {
    "foo": "bar"
  }
}

類實例

webman 支援透過參數類型提示傳遞類實例,例如:

app\service\Blog.php

<?php
namespace app\service;
class Blog
{
    private $title;
    private $content;
    public function __construct(string $title, string $content)
    {
        $this->title = $title;
        $this->content = $content;
    }
    public function get()
    {
        return [
            'title' => $this->title,
            'content' => $this->content,
        ];
    }
}

app\controller\BlogController.php

<?php
namespace app\controller;
use app\service\Blog;
use support\Response;

class BlogController
{
    public function create(Blog $blog): Response
    {
        return json($blog->get());
    }
}

當我們存取 /blog/create?blog[title]=hello&blog[content]=world,會得到以下結果:

{
  "title": "hello",
  "content": "world"
}

模型實例

app\model\User.php

<?php
namespace app\model;
use support\Model;
class User extends Model
{
    protected $connection = 'mysql';
    protected $table = 'user';
    protected $primaryKey = 'id';
    public $timestamps = false;
    // 這裡需要添加可填充欄位,防止前端傳入不安全欄位
    protected $fillable = ['name', 'age'];
}

app\controller\UserController.php

<?php
namespace app\controller;
use app\model\User;
class UserController
{
    public function create(User $user): int
    {
        $user->save();
        return $user->id;
    }
}

當我們存取 /user/create?user[name]=tom&user[age]=18,會得到類似以下的結果:

1

控制器生命週期

config/app.phpcontroller_reusefalse 時,每個請求都會初始化一次對應的控制器實例,請求結束後控制器實例銷毀,這與傳統框架的執行機制相同。

config/app.phpcontroller_reusetrue 時,所有請求會複用控制器實例,也就是控制器實例一旦建立便常駐記憶體,所有請求複用。

注意
開啟控制器複用時,請求不應該更改控制器的任何屬性,因為這些更改會影響後續請求,例如:

<?php
namespace app\controller;

use support\Request;

class FooController
{
    protected $model;

    public function update(Request $request, $id)
    {
        $model = $this->getModel($id);
        $model->update();
        return response('ok');
    }

    public function delete(Request $request, $id)
    {
        $model = $this->getModel($id);
        $model->delete();
        return response('ok');
    }

    protected function getModel($id)
    {
        // 該方法會在第一次請求 update?id=1 後保留 model
        // 若再次請求 delete?id=2 時,會刪除 id=1 的資料
        if (!$this->model) {
            $this->model = Model::find($id);
        }
        return $this->model;
    }
}

提示
在控制器 __construct() 建構函數中 return 資料不會有任何效果,例如:

<?php
namespace app\controller;

use support\Request;

class FooController
{
    public function __construct()
    {
        // 建構函數中 return 資料沒有任何效果,瀏覽器不會收到此回應
        return response('hello'); 
    }
}

控制器不複用與複用區別

區別如下:

不複用控制器

每個請求都會重新 new 一個新的控制器實例,請求結束後釋放該實例並回收記憶體。不複用控制器和傳統框架一樣,符合大部分開發者習慣。由於控制器反覆建立與銷毀,效能會比複用控制器略差(helloworld 壓測約差 10%,帶業務可以基本忽略)。

複用控制器

複用的話一個進程只 new 一次控制器,請求結束後不釋放這個控制器實例,當前進程的後續請求會複用這個實例。複用控制器效能更好,但不符合大部分開發者習慣。

以下情況不能使用控制器複用

當請求會改變控制器的屬性時,不能開啟控制器複用,因為這些屬性的更改會影響後續請求。

有些開發者喜歡在控制器建構函數 __construct() 裡針對每個請求做一些初始化,此時就不能複用控制器,因為當前進程建構函數只會調用一次,並不是每個請求都會調用。