# 第一講:認識控制器
本講主要是了解`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
~~~
則會報錯:

## 總結
現在我們已經了解了控制器的基本概念和定義方法,下面一講會深入了解一些高級的控制器用法。