Skip to content

控制器

新建控制器文件 app/controller/FooController.php

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.phpcontroller_suffix 设置为空 '' ,则控制器类似如下

app\controller\Foo.php

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
<?php
namespace app\controller;
use support\Response;

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

你可以通过 GET POST 方式传递 nameage 的值,也可以通过路由参数传递 nameage 参数,例如

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

优先级为 路由参数 > GET > POST 参数

默认值

假设我们访问 /user/create?name=tom ,我们将得到如下的错误

html
Missing input parameter age

原因是我们没有传递 age 参数,可以通过给参数设置默认值来解决这个问题,例如

php
<?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 方式传递参数

html
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 支持的参数类型有 int float string bool array object 类实例 等,例如

php
<?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 ,我们将得到如下的结果

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

类实例

webman 支持通过参数类型提示传递类实例,例如

app\service\Blog.php

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
<?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 ,我们将得到如下的结果

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

模型实例

app\model\User.php

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
<?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 ,我们将得到类似如下的结果

json
1

控制器生命周期

config/app.phpcontroller_reusefalse 时,每个请求都会初始化一次对应的控制器实例,请求结束后控制器实例销毁,这与传统框架运行机制相同。

config/app.phpcontroller_reusetrue 时,所有请求将复用控制器实例,也就是控制器实例一旦创建便常驻内存,所有请求复用。

注意 开启控制器复用时,请求不应该更改控制器的任何属性,因为这些更改将影响后续请求,例如

php
<?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 时,会删除 1 的数据
        if (!$this->model) {
            $this->model = Model::find($id);
        }
        return $this->model;
    }
}

提示 在控制器 __construct() 构造函数中 return 数据不会有任何效果,例如

php
<?php
namespace app\controller;

use support\Request;

class FooController
{
    public function __construct()
    {
        // 构造函数中return数据没有任何效果,浏览器不会收到此响应
        return response('hello'); 
    }
}

控制器不复用与复用区别

区别如下

不复用控制器

每个请求都会重新 new 一个新的控制器实例,请求结束后释放该实例,并回收内存。不复用控制器和传统框架一样,符合大部分开发者习惯。由于控制器反复的创建销毁,所以性能会比复用控制器略差 (helloworld 压测性能差 10% 左右,带业务可以基本忽略)

复用控制器

复用的话一个进程只 new 一次控制器,请求结束后不释放这个控制器实例,当前进程的后续请求会复用这个实例。复用控制器性能更好,但是不符合大部分开发者习惯。

以下情况不能使用控制器复用

当请求会改变控制器的属性时,不能开启控制器复用,因为这些属性的更改会影响后续请求。

有些开发者喜欢在控制器构造函数 __construct() 里针对每个请求做一些初始化,这时候就不能复用控制器,因为当前进程构造函数只会调用一次,并不是每个请求都会调用。

基于 MIT 许可发布