# 第一讲:认识控制器 本讲主要是了解`ThinkPHP5.0`的控制器的基本概念和使用方法,主要包括: [TOC=2,2] ## 什么是控制器 控制器就是`MVC`设计模式中的C(`Controller`),通常用于读取视图V(`View`)、完成用户输入以及处理模型数据M(`Model`)。 按照ThinkPHP的架构设计,所有的URL请求(无论是否采用了路由),最终都会定位到控制器(也许实际的类不一定是控制器类,但也属于广义范畴的控制器)。控制器的层可能有很多,为了便于区分就把通过URL访问的控制器称之为访问控制器(通常意义上我们所说的控制器就是指访问控制器)。 例如我们访问一个URL地址: ~~~ http://tp5.com/index/index/hello ~~~ > 本文档的所有示例都以`tp5.com`为应用测试域名,请首先配置`vhost`指向tp5的`public`目录(如不清楚请参考快速入门教程)。 实际上访问的是`index`模块下的`Index`控制器类的`hello`方法(在没有定义任何路由的情况下),`Index`控制器对应的类就是`app\index\controller\Index`(为什么控制器类名需要这样命名后面命名空间部分会详细描述),完成上面的URL访问,只需要定义如下的控制器类,看起来非常简单: ~~~ <?php namespace app\index\controller; class Index { public function hello() { return 'hello,world'; } } ~~~ 然后保存到: ~~~ application/index/controller/Index.php ~~~ 现在你可以正式测试前面提到的URL地址了。 ThinkPHP5的命名规范遵循`PSR-2`规范,并且约定了以下规则: * 目录名统一使用小写+下划线; * 类名使用驼峰(首字母大写)命名; * 类文件名和类名规范一致,并使用`.php`文件后缀; * 类的方法使用驼峰(首字母小写)命名; * 一个文件中只对应一个类; >[danger] 特别强调:模块名作为目录作用强制使用小写和下划线规范 遵循命名规范的目的是为了让框架可以根据类的命名空间快速定位类文件的位置,从而实现自动加载,这也是`PSR-4`规范的要求。 ## 命名空间 现在来分析下控制器的类名为什么是`app\index\controller\Index`而不是`Index`,首先就是要明白命名空间的概念。PHP从5.3版本开始引入命名空间的概念,其主要作用是确保类名不会冲突,因为在一个应用中,出现相同的类名的几率非常之大,并且你很难保证引入的第三方类库不冲突,而有了命名空间后,相当于给自己的类加了一个门牌号一样,一个类的组成包括: >[info]### 类的组成 = 根命名空间+子命名空间(可选)+类名 这样即使是相同的类名,只要在不同的命名空间下面就属于完全不同的类,所以下面的类都是不同的类库: ~~~ app\index\controller\Index app\admin\controller\Index app\controller\Index ~~~ 而当使用下面的代码实例化一个`Index`类的时候, ~~~ $controller = new Index(); ~~~ 系统其实并不知道你要实例化的是哪个类库,所以首先就会从当前文件所在的命名空间去实例化`Index`类,但这样经常会产生混淆,所以合理的办法是明确告诉系统你实例化的是哪个具体的类,通常我们会使用`use`来引入一个命名空间类库,例如: ~~~ use app\admin\controller\Index; $controller = new Index(); ~~~ 这个时候就会明确当前实例化的是`app\admin\controller\Index`类,而不会是`app\index\controller\Index`类或者`app\controller\Index`类。 在不使用`use`引入的情况下,可以直接使用完整的命名空间来实例化(但并不建议,完全不必担心`use`过多的类库会带来性能问题) ~~~ $controller = new \app\admin\controller\Index(); ~~~ >[danger] 完整命名空间实例化的时候必须加上开头的`\`表示从根命名空间开始。 命名空间的根命名是可以设置起始路径的(严格来说,不仅是根命名可以设置,比如有些扩展就可以单独设置自己的命名空间的对应路径,`composer`通常是这么设计的),系统默认设置了下面三个根命名: |根命名|描述|类库起始目录| |---|---|---| |think|系统核心类库|thinkphp/library/think| |traits|系统Trait类库|thinkphp/library/traits| |app|应用类库|application| 按照PSR-4的规范,子命名空间和目录**必须**是一一对应的,而且**大小写一致**。最后的类名部分实际对应的是一个和类名一致(包括大小写)的文件,保持一致规范约束的目的是为了实现类的自动加载(`ThinkPHP`开发过程中一定要明白大小写是严格区分的,即使是在`windows`下面)。 综上分析,前面的类库对应的类文件分别是: ~~~ application/index/controller/Index.php application/admin/controller/Index.php application/controller/Index.php ~~~ 现在我们来回答为什么控制器类的名称是`app\index\controller\Index`,这是ThinkPHP框架制定的规范,`app`是应用类库的根命名空间,也就是所有的应用类库都应该用`app`作为根命名空间定义。`index`是表示模块目录,`controller`表示的是控制器(确切的说是访问控制器)目录,`Index`是实际的控制器类名,所以要表示`index`模块的`Index`控制器类,使用的就是`app\index\controller\Index`,如果是`admin`模块的`Index`控制器类,使用的就是`app\admin\controller\Index`类,如果使用的是单一模块的话,那么`Index`控制器类就变成了`app\controller\Index`,现在明白了么? 核心类库都是以`think`开头的命名空间,应用类库都是以`app`开头的命名空间,核心类库一般也不需要更改命名空间,但应用类库是可以单独定义命名空间的,有些新手总有困惑按照目录一致的规范为什么应用类库的根命名空间不是`application`而是`app`(我能说是框架的好意么),下面的配置可以治疗这种纠结,将应用的命名空间改为`application`: ~~~ // 应用命名空间 'app_namespace' => 'application', ~~~ > 不要问我配置文件在哪里修改^_^ 说好的学好基础再来呢 修改后,前面对应类的命名空间需要调整为 ~~~ application\index\controller\Index application\admin\controller\Index application\controller\Index ~~~ 但对应的类文件实际位置仍然保持不变。 > 在后面的教程内容里面不会每次都说明一个类文件的实际位置,大家看到一个类的命名空间后就应该可以定位类文件的位置,否则说明你对命名空间还不够理解,请再次阅读下前面的内容。 ## 控制器继承 前面是一个很简单的例子,没有继承任何的类(这样并没有任何不对,5.0的控制器设计如此,事实上也非常高效),控制器可以继承系统内置的控制器基类`think\Controller`或者应用自己的控制器基类,来扩展更多的功能和方法。 继承系统控制器基类: ~~~ <?php namespace app\index\controller; use think\Controller; class Index extends Controller { public function hello() { return 'hello,world'; } } ~~~ > 系统控制器基类提供了一些额外的方法,我们会在后面陆续讲解。 或者自定义一个基础控制器类`Base`: ~~~ <?php namespace app\index\controller; use think\Controller; class Base extends Controller { } ~~~ 可以在`Base`控制器类中定义一些公共方法(如果对类的基本知识不够熟悉的话,参考PHP的类与对象部分说的非常清楚,在此不做深入了)。 然后应用下面的所有控制器类都继承`Base`: ~~~ <?php namespace app\index\controller; use app\index\controller\Base; class Index extends Base { public function hello() { return 'hello,world'; } } ~~~ 建议给应用统一定义一个自己的控制器基类,方便后期扩展。 > PHP不支持多继承,如果需要继承多个类,可以通过引入`trait`。 ## 操作方法 控制器类的每一个`public`类型方法(包括继承的)都是一个可访问的操作,也是URL访问的最小单元,`private`和`protected`类型的方法都不能被访问(只能在控制器内部被调用)。 下面举个简单的例子: ~~~ <?php namespace app\index\controller; use think\Controller; class Base extends Controller { public function base() { return 'base'; } } ~~~ Index控制器的测试代码如下: ~~~ <?php namespace app\index\controller; use app\index\controller\Base; class Index extends Base { public function hello() { return 'hello,world'; } private function far() { return 'far'; } protected function boo() { return 'boo'; } public function test() { return 'test'; } } ~~~ 下面的URL访问可以正常访问 ~~~ http://tp5.com/index/index/hello http://tp5.com/index/index/test http://tp5.com/index/index/base ~~~ 下面的URL访问会报错 ~~~ http://tp5.com/index/index/far http://tp5.com/index/index/boo ~~~ 虽然使用`echo`方法也能正常输出,但`ThinkPHP5`的操作方法建议统一使用`return`返回值的方式进行响应输出(除非你使用`echo`或者`dump`进行调试输出),优势是系统可以自动判断当前的响应输出类型进行自动转换处理,以及可以享受请求缓存的便利。 ## 驼峰命名 控制器类名的规范是驼峰法(并且首字母大写),不过URL的访问地址并非如此,假设定义了一个`HelloWorld`控制器如下: ~~~ <?php namespace app\index\controller; class HelloWorld { public function index() { return 'hello,world'; } } ~~~ 实际的URL访问并非是下面的 ~~~ http://tp5.com/index/HelloWorld/index ~~~ 实际会被系统解析成`Helloworld`控制器类而不是`HelloWorld`控制器类(虽然只是大小写的区别但按照`PSR-4`自动加载规范无法自动加载,因此会报`Helloworld`控制器类不存在的错误)。 正确的URL访问应该是 ~~~ http://tp5.com/index/hello_world/index ~~~ > 注意`hello_world`并不会自动对应`hello_world`控制器(因为不符合控制器类的命名规范),仍然会自动对应`HelloWorld`控制器类。 这一切因果缘由就是框架的URL自动转换功能,由于系统的URL自动转换功能,ThinkPHP5的URL地址默认是不区分大小写的(也就是说都会强制转换成小写)。但事情没有绝对,我们可以设置关闭URL自动转换: ~~~ 'url_convert' => false, ~~~ 一旦关闭`url_convert`自动转换,就意味着URL地址中的控制器名不会自动转换,必须严格使用实际的控制器名(区分大小写)。 这个时候,你就可以通过 ~~~ http://tp5.com/index/HelloWorld/index ~~~ 正常访问`HelloWorld`控制器了^_^ ## 控制器后缀 为什么会有控制器后缀的概念呢?有两个原因,首先是如果控制器类不带后缀,容易产生和关键字冲突的情况,例如无法使用`public`控制器,其次,控制器类和模型类容易产生混淆,例如`User`控制器类和`User`模型类,默认不使用控制器后缀,要使用的话开启下面的参数: ~~~ // 控制器类后缀 'controller_suffix' => true, ~~~ `controller_suffix`参数配置的是布尔值,而不是具体的控制器后缀,开启后,会自动使用`url_controller_layer`配置值作为访问控制器后缀,这个参数默认值是`controller`,所以再次访问 ~~~ http://tp5.com/index/index/hello ~~~ 的时候,指向的访问控制器为: ~~~ application/index/controller/IndexController.php ~~~ 控制器类定义修改如下: ~~~ <?php namespace app\index\controller; class IndexController { public function hello() { return 'hello,world'; } } ~~~ > 注意:开启了控制器类后缀的话,类名和类文件名依然要保持大小写一致。 开启了控制器类后缀的好处是控制器类的命名不受任何关键字约束,例如我们可以定义一个`public`控制器类用于继承, ~~~ <?php namespace app\index\controller; class PublicController { public function base() { return 'base'; } } ~~~ 开启了控制器类后缀,并不会影响当前的控制器名称的获取,当前访问的控制器名称还是`Public`而不是`PublicController`,要注意`控制器名`和`控制器类名`的区别。 ## 方法后缀 同样的,为了避免操作方法名和关键字混淆,我们也可以给操作方法统一添加方法后缀,例如设置操作方法后缀为`Action`: ~~~ // 设置操作方法后缀 'action_suffix' => 'Action', ~~~ 接下来,所有的操作方法都必须带上`Action`后缀才能正常访问: ~~~ <?php namespace app\index\controller; class Index { public function helloAction() { return 'hello'; } public function publicAction() { return 'public'; } public function test() { return 'test'; } } ~~~ 当我们访问下面的URL地址 ~~~ http://tp5.com/index/index/hello http://tp5.com/index/index/public ~~~ 都可以正常访问,而 ~~~ http://tp5.com/index/index/test ~~~ 则会报错: ![](https://box.kancloud.cn/ea85319c0444f570a508841a7b93da2d_1254x598.png) ## 总结 现在我们已经了解了控制器的基本概念和定义方法,下面一讲会深入了解一些高级的控制器用法。