一、请求流程
1.载入Composer的自动加载autoload文件
2.实例化系统应用基础类think\App
3.获取应用目录等相关路径信息
4.加载全局的服务提供provider.php文件
5.设置容器实例及应用对象实例,确保当前容器对象唯一
6.从容器中获取HTTP应用类think\Http
7.执行HTTP应用类的run方法启动一个HTTP应用
8.获取当前请求对象实例(默认为 app\Request 继承think\Request)保存到容器
9.执行think\App类的初始化方法initialize
10.加载环境变量文件.env和全局初始化文件
11.加载全局公共文件、系统助手函数、全局配置文件、全局事件定义和全局服务定义
12.判断应用模式(调试或者部署模式)
13.监听AppInit事件
14.注册异常处理
15.服务注册
16.启动注册的服务
17.加载全局中间件定义
18监听HttpRun事件
19.执行全局中间件
20执行路由调度(Route类dispatch方法)
21.如果开启路由则检查路由缓存
22.加载路由定义
23监听RouteLoaded事件
24.如果开启注解路由则检测注解路由
25.路由检测(中间流程很复杂 略)
26.路由调度对象think\route\Dispatch初始化
27.设置当前请求的控制器和操作名
28.注册路由中间件
29.绑定数据模型
30.设置路由额外参数
31.执行数据自动验证
32.执行路由调度子类的exec方法返回响应think\Response对象
33.获取当前请求的控制器对象实例
34.利用反射机制注册控制器中间件
35.执行控制器方法以及前后置中间件
36.执行当前响应对象的send方法输出
37.执行HTTP应用对象的end方法善后
38.监听HttpEnd事件
39.执行中间件的end回调
40写入当前请求的日志信息
至此,当前请求流程结束。
二、架构总览
hinkPHP支持传统的MVC(Model-View-Controller)模式以及流行的MVVM(Model-View-ViewModel)模式的应用开发,下面的一些概念有必要做下了解,可能在后面的内容中经常会被提及。
入口文件
用户请求的PHP文件,负责处理请求(注意,不一定是HTTP请求)的生命周期,入口文件位于public目录下面,最常见的入口文件就是index.php,6.0支持多应用多入口,你可以给每个应用增加入口文件,例如给后台应用单独设置的一个入口文件admin.php。
如果开启自动多应用的话,一般只需要一个入口文件index.php。
应用
6.0版本提供了对多应用的良好支持,每个应用是一个app目录的子目录(或者指定的composer库),每个应用具有独立的路由、配置,以及MVC相关文件,这些应用可以公用框架核心以及扩展。而且可以支持composer应用加载。
容器
ThinkPHP使用(对象)容器统一管理对象实例及依赖注入。
容器类的工作由think\Container类完成,但大多数情况下我们都是通过应用类(think\App类)或是app助手函数来完成容器操作,容器中所有的对象实例都可以通过容器标识单例调用,你可以给容器中的对象实例绑定一个对象标识,如果没有绑定则使用类名作为容器标识。
系统服务
系统服务的概念是指在执行框架的某些组件或者功能的时候需要依赖的一些基础服务,服务类通常可以继承系统的think\Service类,但并不强制。
你可以在系统服务中注册一个对象到容器,或者对某些对象进行相关的依赖注入。由于系统服务的执行优先级问题,可以确保相关组件在执行的时候已经完成相关依赖注入。
路由
路由是用于规划(一般同时也会进行简化)请求的访问地址,在访问地址和实际操作方法之间建立一个路由规则 => 路由地址的映射关系。
ThinkPHP并非强制使用路由,如果没有定义路由,则可以直接使用“控制器/操作”的方式访问,如果定义了路由,则该路由对应的路由地址就被不能直接访问了。一旦开启强制路由参数,则必须为每个请求定义路由(包括首页)。
使用路由有一定的性能损失,但随之也更加安全,因为每个路由都有自己的生效条件,如果不满足条件的请求是被过滤的。你远比你在控制器的操作中进行各种判断要实用的多。
其实路由的作用远非URL规范这么简单,还可以实现验证、权限、参数绑定及响应设置等功能。
控制器
每个应用下面拥有独立的类库及配置文件,一个应用下面有多个控制器负责响应请求,而每个控制器其实就是一个独立的控制器类。
控制器主要负责请求的接收,并调用相关的模型处理,并最终通过视图输出。严格来说,控制器不应该过多的介入业务逻辑处理。
事实上,控制器是可以被跳过的,通过路由我们可以直接把请求调度到某个模型或者其他的类进行处理。
ThinkPHP的控制器类比较灵活,可以无需继承任何基础类库。
一个典型的Index控制器类(单应用模式)如下:
<?php
namespace app\controller;
class Index
{
public function index()
{
return 'hello,thinkphp!';
}
}
一般建议继承一个基础的控制器,方便扩展。系统默认提供了一个app\BaseController控制器类。
操作
一个控制器包含多个操作(方法),操作方法是一个URL访问的最小单元。
下面是一个典型的Index控制器的操作方法定义,包含了两个操作方法:
<?php
namespace app\controller;
class Index
{
public function index()
{
return 'index';
}
public function hello(string $name)
{
return 'Hello,'.$name;
}
}
操作方法可以不使用任何参数,如果定义了一个非可选参数,并且不是对象类型,则该参数必须通过用户请求传入,如果是URL请求,则通常是通过当前的请求传入,操作方法的参数支持依赖注入。
模型
模型类通常完成实际的业务逻辑和数据封装,并返回和格式无关的数据。
模型类并不一定要访问数据库,而且在ThinkPHP的架构设计中,只有进行实际的数据库查询操作的时候,才会进行数据库的连接,是真正的惰性连接。
ThinkPHP的模型层支持多层设计,你可以对模型层进行更细化的设计和分工,例如把模型层分为逻辑层/服务层/事件层等等。
模型类通常需要继承think\Model类,一个典型的User模型器类如下:
<?php
namespace app\model;
use think\Model;
class User extends Model
{
}
视图
控制器调用模型类后,返回的数据通过视图组装成不同格式的输出。视图根据不同的需求,来决定调用模板引擎进行内容解析后输出还是直接输出。
视图通常会有一系列的模板文件对应不同的控制器和操作方法,并且支持动态设置模板目录。
模板引擎
模板文件中可以使用一些特殊的模板标签,这些标签的解析通常由模板引擎负责实现。
新版不再内置think-template模板引擎,如果需要使用ThinkPHP官方模板引擎,需要单独安装think-view模板引擎驱动扩展。
驱动
系统很多的组件都采用驱动式设计,从而可以更灵活的扩展,驱动类的位置默认是放入核心类库目录下面,也可以重新定义驱动类库的命名空间而改变驱动的文件位置。
6.0版本的驱动采用Composer的方式安装和管理。
中间件
中间件主要用于拦截或过滤应用的HTTP请求,并进行必要的业务处理。
新版部分核心功能使用中间件处理,你可以灵活关闭。包括Session功能、请求缓存和多语言功能。
事件
6.0已经使用事件机制替代原来的行为和Hook机制,可以在应用中使用事件机制的特性来扩展功能。
此外数据库操作和模型操作在完成数据操作的回调机制,也使用了事件机制。
助手函数
系统为一些常用的操作提供了助手函数支持。使用助手函数和性能并无直接影响,只是某些时候无法享受IDE自动提醒的便利,但是否使用助手函数看项目自身规范,在应用的公共函数文件中也可以对系统提供的助手函数进行重写。
三、入口文件
ThinkPHP6.0采用单一入口模式进行项目部署和访问,一个应用都有一个统一(但不一定是唯一)的入口。如果采用自动多应用部署的话,一个入口文件还可以自动对应多个应用。
入口文件定义
默认的应用入口文件位于public/index.php,默认内容如下:
// [ 应用入口文件 ]
namespace think;
require __DIR__ . '/../vendor/autoload.php';
// 执行HTTP应用并响应
$http = (new App())->http;
$response = $http->run();
$response->send();
$http->end($response);
如果你没有特殊的自定义需求,无需对入口文件做任何的更改。
入口文件位置的设计是为了让应用部署更安全,请尽量遵循public目录为唯一的web可访问目录,其他的文件都可以放到非WEB访问目录下面。
控制台入口文件
除了应用入口文件外,系统还提供了一个控制台入口文件,位于项目根目录的think(注意该文件没有任何的后缀)。
该入口文件代码如下:
#!/usr/bin/env php
<?php
namespace think;
// 加载基础文件
require __DIR__ . '/vendor/autoload.php';
// 应用初始化
(new App())->console->run();
控制台入口文件用于执行控制台指令,例如:
php think version
系统内置了一些常用的控制台指令,如果你安装了额外的扩展,也会增加相应的控制台指令,都是通过该入口文件执行的。
四、多应用模式
多应用
安装后默认使用单应用模式部署,目录结构如下:
├─app 应用目录
│ ├─controller 控制器目录
│ ├─model 模型目录
│ ├─view 视图目录
│ └─ … 更多类库目录
│
├─public WEB目录(对外访问目录)
│ ├─index.php 入口文件
│ ├─router.php 快速测试文件
│ └─.htaccess 用于apache的重写
│
├─view 视图目录
├─config 应用配置目录
├─route 路由定义目录
├─runtime 应用的运行时目录
单应用模式的优势是简单灵活,URL地址完全通过路由可控。配合路由分组功能可以实现类似多应用的灵活机制。
如果要使用多应用模式,你需要安装多应用模式扩展think-multi-app。
composer require topthink/think-multi-app
然后你的应用目录结构需要做如下调整,主要区别在app目录增加了应用子目录,然后配置文件和路由定义文件都纳入应用目录下。
├─app 应用目录
│ ├─index 主应用
│ │ ├─controller 控制器目录
│ │ ├─model 模型目录
│ │ ├─view 视图目录
│ │ ├─config 配置目录
│ │ ├─route 路由目录
│ │ └─ … 更多类库目录
│ │
│ ├─admin 后台应用
│ │ ├─controller 控制器目录
│ │ ├─model 模型目录
│ │ ├─view 视图目录
│ │ ├─config 配置目录
│ │ ├─route 路由目录
│ │ └─ … 更多类库目录
│
├─public WEB目录(对外访问目录)
│ ├─admin.php 后台入口文件
│ ├─index.php 入口文件
│ ├─router.php 快速测试文件
│ └─.htaccess 用于apache的重写
│
├─config 全局应用配置目录
├─runtime 运行时目录
│ ├─index index应用运行时目录
│ └─admin admin应用运行时目录
从目录结构可以看出来,每个应用相对保持独立,并且可以支持多个入口文件,应用下面还可以通过多级控制器来维护控制器分组。
自动多应用部署
支持在同一个入口文件中访问多个应用,并且支持应用的映射关系以及自定义。如果你通过index.php入口文件访问的话,并且没有设置应用name,系统自动采用自动多应用模式。
自动多应用模式的URL地址默认使用
// 访问admin应用
http://serverName/index.php/admin
// 访问shop应用
http://serverName/index.php/shop
也就是说pathinfo地址的第一个参数就表示当前的应用名,后面才是该应用的路由或者控制器/操作。
如果直接访问:
http://serverName/index.php
访问的其实是index默认应用,可以通过app.php配置文件的default_app配置参数指定默认应用。
// 设置默认应用名称
'default_app' => 'home',
接着访问
http://serverName/index.php
其实访问的是home应用。
自动多应用模式下,路由是每个应用独立的,所以你没法省略URL里面的应用参数。但可以使用域名绑定解决。
多应用智能识别
如果没有绑定入口或者域名的情况下,URL里面的应用不存在,例如访问:
http://serverName/index.php/think
假设并不存在think应用,这个时候系统会自动切换到单应用模式,如果有定义全局的路由,也会进行路由匹配检查。
如果我们在route/route.php全局路由中定义了:
Route::get('think', function () {
return 'hello,ThinkPHP!';
});
访问上面的URL就会输出:
hello,ThinkPHP!
如果你希望think应用不存在的时候,直接访问默认应用的路由,可以在app.php中配置:
// 开启应用快速访问
'app_express' => true,
// 默认应用
'default_app' => 'home',
这个时候就会访问home应用下的路由。
增加应用入口
允许为每个应用创建单独的入口文件而不通过index.php入口文件访问多个应用,例如创建一个admin.php入口文件来访问admin应用。
// [ 应用入口文件 ]
namespace think;
require __DIR__ . '/../vendor/autoload.php';
// 执行HTTP应用并响应
$http = (new App())->http;
$response = $http->run();
$response->send();
$http->end($response);
多应用使用不同的入口的情况下,每个入口文件的内容都是一样的,默认入口文件名(不含后缀)就是应用名。
使用下面的方式访问admin应用
http://serverName/admin.php
如果你的入口文件名和应用不一致,例如你的后台admin应用,入口文件名使用了test.php,那么入口文件需要改成:
// [ 应用入口文件 ]
namespace think;
require __DIR__ . '/../vendor/autoload.php';
// 执行HTTP应用并响应
$http = (new App())->http;
$response = $http->name('admin')->run();
$response->send();
$http->end($response);
获取当前应用
如果需要获取当前的应用名,可以使用
app('http')->getName();
获取当前应用
如果需要获取当前的应用名,可以使用
app('http')->getName();
应用目录获取
单应用和多应用模式会影响一些系统路径的值,为了更好的理解本手册的内容,你可能需要理解下面几个系统路径所表示的位置。
注意:应用支持使用composer包,这个时候目录可能是composer包的类库所在目录。
对于非自动多应用部署的情况,如果要加载composer应用,需要在入口文件中设置应用路径:
// [ 应用入口文件 ]
namespace think;
require __DIR__ . '/../vendor/autoload.php';
// 执行HTTP应用并响应
$http = (new App())->http;
$response = $http->path('path/to/app')->run();
$response->send();
$http->end($response);
应用映射
自动多应用模式下,支持应用的别名映射,例如:
'app_map' => [
'think' => 'admin', // 把admin应用映射为think
],
应用映射后,原来的应用名将不能被访问,例如上面的admin应用不能直接访问,只能通过think应用访问。
应用映射支持泛解析,例如:
'app_map' => [
'think' => 'admin',
'home' => 'index',
'*' => 'index',
],
表示如果URL访问的应用不在当前设置的映射里面,则自动映射为index应用。
如果要使用composer加载应用,需要设置
'app_map' => [
'think' => function($app) {
$app->http->path('path/to/composer/app');
},
],
域名绑定应用
如果你的多应用使用多个子域名或者独立域名访问,你可以在config/app.php配置文件中定义域名和应用的绑定。
'domain_bind' => [
'blog' => 'blog', // blog子域名绑定到blog应用
'shop.tp.com' => 'shop', // 完整域名绑定
'*' => 'home', // 二级泛域名绑定到home应用
],
禁止应用访问
你如果不希望某个应用通过URL访问,例如,你增加了一个common子目录用于放置一些公共类库,你可以设置
'deny_app_list' => ['common']
多应用模式并非核心内置模式,官方提供的多应用扩展更多是抛砖引玉,你完全可以通过中间件来扩展适合自己的多应用模式
五、URL访问
URL设计
6.0的URL访问受路由影响,如果在没有定义或匹配路由的情况下(并且没有开启强制路由模式的话),则是基于:
http://serverName/index.php(或者其它入口文件)/控制器/操作/参数/值…
如果使用自动多应用模式的话,URL一般是
http://serverName/index.php/应用/控制器/操作/参数/值...
普通模式的URL访问不再支持,但参数可以支持普通方式传值
如果不支持PATHINFO的服务器可以使用兼容模式访问如下:
http://serverName/index.php?s=/控制器/操作/[参数名/参数值...]
URL重写
可以通过URL重写隐藏应用的入口文件index.php(也可以是其它的入口文件,但URL重写通常只能设置一个入口文件),下面是相关服务器的配置参考:
[ Apache ]
1.httpd.conf配置文件中加载了mod_rewrite.so模块
2.AllowOverride None 将None改为 All
3.把下面的内容保存为.htaccess文件放到应用入口文件的同级目录下
<IfModule mod_rewrite.c>
Options +FollowSymlinks -Multiviews
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ index.php/$1 [QSA,PT,L]
</IfModule>
[ IIS ]
如果你的服务器环境支持ISAPI_Rewrite的话,可以配置httpd.ini文件,添加下面的内容:
RewriteRule (.*)$ /index\.php\?s=$1 [I]
在IIS的高版本下面可以配置web.Config,在中间添加rewrite节点:
<rewrite>
<rules>
<rule name="OrgPage" stopProcessing="true">
<match url="^(.*)$" />
<conditions logicalGrouping="MatchAll">
<add input="{HTTP_HOST}" pattern="^(.*)$" />
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
<add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
</conditions>
<action type="Rewrite" url="index.php/{R:1}" />
</rule>
</rules>
</rewrite>
[ Nginx ]
在Nginx低版本中,是不支持PATHINFO的,但是可以通过在Nginx.conf中配置转发规则实现:
location / { // …..省略部分代码
if (!-e $request_filename) {
rewrite ^(.*)$ /index.php?s=/$1 last;
}
}
其实内部是转发到了ThinkPHP提供的兼容URL,利用这种方式,可以解决其他不支持PATHINFO的WEB服务器环境。
六、容器的依赖注入
容器和依赖注入
ThinkPHP使用容器来更方便的管理类依赖及运行依赖注入,新版的容器支持PSR-11规范。
容器类的工作由think\Container类完成,但大多数情况我们只需要通过app助手函数或者think\App类即可容器操作,如果在服务类中可以直接调用this->app进行容器操作。
依赖注入其实本质上是指对类的依赖通过构造器完成自动注入,例如在控制器架构方法和操作方法中一旦对参数进行对象类型约束则会自动触发依赖注入,由于访问控制器的参数都来自于URL请求,普通变量就是通过参数绑定自动获取,对象变量则是通过依赖注入生成。
<?php
namespace app\controller;
use think\Request;
class Index
{
protected $request;
public function __construct(Request $request)
{
$this->request = $request;
}
public function hello($name)
{
return 'Hello,' . $name . '!This is '. $this->request->action();
}
}
依赖注入的对象参数支持多个,并且和顺序无关。
支持使用依赖注入的场景包括(但不限于):
1.控制器架构方法;
2.控制器操作方法;
3.路由的闭包定义;
4.事件类的执行方法;
5.中间件的执行方法;
对于自定义的类以及方法,如果需要使用依赖注入,需要使用系统提供的invoke助手函数调用,例如:
class Foo
{
public function __construct(Bar $bar)
{
}
}
如果直接new的话,需要手动传入Bar对象实例
$bar = new Bar();
$foo = new Foo($bar);
如果使用容器来实例化的话,可以自动进行依赖注入。
$foo = invoke('Foo');
如果要对某个方法支持依赖注入,可以使用
class Foo
{
public function bar(Bar $bar)
{
// ...
}
}
$result = invoke(['Foo', 'bar']);
也支持对某个函数或者闭包使用依赖注入
$result = invoke(function(Bar $bar) {
// ...
});
绑定
依赖注入的类统一由容器进行管理,大多数情况下是在自动绑定并且实例化的。不过你可以随时进行手动绑定类到容器中(通常是在服务类的register方法中进行绑定),支持多种绑定方式。
绑定类标识
可以对已有的类库绑定一个标识(唯一),便于快速调用。
// 绑定类库标识
$this->app->bind('think\Cache', 'app\common\Cache');
或者使用助手函数
// 绑定类库标识
bind('cache', 'think\Cache');
绑定的类标识可以自己定义(只要不冲突)。
绑定闭包
可以绑定一个闭包到容器中
bind('sayHello', function ($name) {
return 'hello,' . $name;
});
绑定实例
也可以直接绑定一个类的实例
$cache = new think\Cache;
// 绑定类实例
bind('cache', $cache);
绑定至接口实现
对于依赖注入使用接口类的情况,我们需要告诉系统使用哪个具体的接口实现类来进行注入,这个使用可以把某个类绑定到接口
// 绑定think\LoggerInterface接口实现到think\Log
bind('think\LoggerInterface','think\Log');
使用接口作为依赖注入的类型
<?php
namespace app\index\controller;
use think\LoggerInterface;
class Index
{
public function hello(LoggerInterface $log)
{
$log->record('hello,world!');
}
}
批量绑定
在实际应用开发过程,不需要手动绑定,我们只需要在app目录下面定义provider.php文件(只能在全局定义,不支持应用单独定义),系统会自动批量绑定类库到容器中。
return [
'route' => \think\Route::class,
'session' => \think\Session::class,
'url' => \think\Url::class,
];
绑定标识调用的时候区分大小写,系统已经内置绑定了核心常用类库,无需重复绑定
系统内置绑定到容器中的类库包括
解析
使用app助手函数进行容器中的类解析调用,对于已经绑定的类标识,会自动快速实例化
$cache = app('cache');
带参数实例化调用
$cache = app('cache',['file']);
对于没有绑定的类,也可以直接解析
$arrayItem = app('org\utils\ArrayItem');
调用和绑定的标识必须保持一致(包括大小写)
容器中已经调用过的类会自动使用单例,除非你使用下面的方式强制重新实例化。
// 每次调用都会重新实例化
$cache = app('cache', [], true);
对象化调用
使用app助手函数获取容器中的对象实例(支持依赖注入)。
$app = app();
// 判断对象实例是否存在
isset($app->cache);
// 注册容器对象实例
$app->cache = think\Cache::class;
// 获取容器中的对象实例
$cache = $app->cache;
也就是说,你可以在任何地方使用app()方法调用容器中的任何类,但大多数情况下面,我们更建议使用依赖注入。
// 调用配置类
app()->config->get('app_name');
// 调用session类
app()->session->get('user_name');
自动注入
容器主要用于依赖注入,依赖注入会首先检查容器中是否注册过该对象实例,如果没有就会自动实例化,然后自动注入,例如:
我们可以给路由绑定模型对象实例
Route::get('user/:id','index/Index/hello')
->model('\app\index\model\User');
然后在操作方法中自动注入User模型
<?php
namespace app\index\controller;
use app\index\model\User;
class Index
{
public function hello(User $user)
{
return 'Hello,'.$user->name;
}
}
自定义实例化
容器中的对象实例化支持自定义,可以在你需要依赖注入的对象中增加__make方法定义,例如:
如果你希望User模型类在依赖注入的时候 使用自定义实例化的方式,可以用下面的方法。
<?php
namespace app\index\model;
use think\Model;
use think\db\Query;
class User extends Model
{
public static function __make(Query $query)
{
return (new self())->setQuery($query);
}
}
容器对象回调机制
容器中的对象实例化之后,支持回调机制,利用该机制可以实现诸如注解功能等相关功能。
你可以通过resolving方法注册一个全局回调
Container::getInstance()->resolving(function($instance,$container) {
// ...
});
回调方法支持两个参数,第一个参数是容器对象实例,第二个参数是容器实例本身。
或者单独注册一个某个容器对象的回调
Container::getInstance()->resolving(\think\Cache::class,function($instance,$container) {
// ...
});
七、服务
系统服务
系统服务的概念是指在执行框架的某些组件或者功能的时候需要依赖的一些基础服务,服务类通常可以继承系统的think\Service类,但并不强制(如果继承think\Service的话可以直接调用this->app获取应用实例)。
你可以在系统服务中注册一个对象到容器,或者对某些对象进行相关的依赖注入。由于系统服务的执行优先级问题,可以确保相关组件在执行的时候已经完成相关依赖注入。
服务定义
你可以通过命令行生成一个服务类,例如:
php think make:service FileSystemService
默认生成的服务类会继承系统的think\Service,并且自动生成了系统服务类最常用的两个空方法:register和boot方法。
注册方法
register方法通常用于注册系统服务,也就是将服务绑定到容器中,例如:
<?php
namespace app\service;
use my\util\FileSystem;
class FileSystemService extends Service
{
public function register()
{
$this->app->bind('file_system', FileSystem::class);
}
}
register方法不需要任何的参数,如果你只是简单的绑定容器对象的话,可以直接使用bind属性。
<?php
namespace app\service;
use my\util\FileSystem;
class FileSystemService extends Service
{
public $bind = [
'file_system' => FileSystem::class,
];
}
启动方法
boot方法是在所有的系统服务注册完成之后调用,用于定义启动某个系统服务之前需要做的操作。例如:
<?php
namespace think\captcha;
use think\Route;
use think\Service;
use think\Validate;
class CaptchaService extends Service
{
public function boot(Route $route)
{
$route->get('captcha/[:config]', "\\think\\captcha\\CaptchaController@index");
Validate::maker(function ($validate) {
$validate->extend('captcha', function ($value) {
return captcha_check($value);
}, ':attribute错误!');
});
}
}
boot方法支持依赖注入,你可以直接使用其它的依赖服务。
服务注册
定义好系统服务后,你还需要注册服务到你的应用实例中。
可以在应用的全局公共文件service.php中定义需要注册的系统服务,系统会自动完成注册以及启动。例如:
return [
'\app\service\ConfigService',
'\app\service\CacheService',
];
如果你需要在你的扩展中注册系统服务,首先在扩展中增加一个服务类,然后在扩展的composer.json文件中增加如下定义:
"extra": {
"think": {
"services": [
"think\\captcha\\CaptchaService"
]
}
},
在安装扩展后会系统会自动执行service:discover指令用于生成服务列表,并在系统初始化过程中自动注册。
内置服务
为了更好的完成核心组件的单元测试,框架内置了一些系统服务类,主要都是用于核心类的依赖注入,包括ModelService、PaginatorService和ValidateService类。这些服务不需要注册,并且也不能卸载。
八、门面
门面(Facade)
门面为容器中的(动态)类提供了一个静态调用接口,相比于传统的静态方法调用, 带来了更好的可测试性和扩展性,你可以为任何的非静态类库定义一个facade类。
系统已经为大部分核心类库定义了Facade,所以你可以通过Facade来访问这些系统类,当然也可以为你的应用类库添加静态代理。
下面是一个示例,假如我们定义了一个app\common\Test类,里面有一个hello动态方法。
<?php
namespace app\common;
class Test
{
public function hello($name)
{
return 'hello,' . $name;
}
}
调用hello方法的代码应该类似于:
$test = new \app\common\Test;
echo $test->hello('thinkphp'); // 输出 hello,thinkphp
接下来,我们给这个类定义一个静态代理类app\facade\Test(这个类名不一定要和Test类一致,但通常为了便于管理,建议保持名称统一)。
<?php
namespace app\facade;
use think\Facade;
class Test extends Facade
{
protected static function getFacadeClass()
{
return 'app\common\Test';
}
}
只要这个类库继承think\Facade,就可以使用静态方式调用动态类app\common\Test的动态方法,例如上面的代码就可以改成:
// 无需进行实例化 直接以静态方法方式调用hello
echo \app\facade\Test::hello('thinkphp');
结果也会输出 hello,thinkphp。
说的直白一点,Facade功能可以让类无需实例化而直接进行静态方式调用。
核心Facade类库
系统给内置的常用类库定义了Facade类库,包括:
所以你无需进行实例化就可以很方便的进行方法调用,例如:
use think\facade\Cache;
Cache::set('name','value');
echo Cache::get('name');
在进行依赖注入的时候,请不要使用Facade类作为类型约束,而是建议使用原来的动态类,下面是错误的用法:
<?php
namespace app\index\controller;
use think\facade\App;
class Index
{
public function index(App $app)
{
}
}
应当使用下面的方式:
<?php
namespace app\index\controller;
use think\App;
class Index
{
public function index(App $app)
{
}
}
事实上,依赖注入和使用Facade代理的效果大多数情况下是一样的,都是从容器中获取对象实例。例如:
<?php
namespace app\index\controller;
use think\Request;
class Index
{
public function index(Request $request)
{
echo $request->controller();
}
}
和下面的作用是一样的
<?php
namespace app\index\controller;
use think\facade\Request;
class Index
{
public function index()
{
echo Request::controller();
}
}
依赖注入的优势是支持接口的注入,而Facade则无法完成。
一定要注意两种方式的use引入类库的区别
九、中间件
中间件主要用于拦截或过滤应用的HTTP请求,并进行必要的业务处理。
新版部分核心功能使用中间件处理,你可以灵活关闭。包括Session功能、请求缓存和多语言功能。
定义中间件
可以通过命令行指令快速生成中间件
php think make:middleware Check
这个指令会 app/middleware目录下面生成一个Check中间件。
<?php
namespace app\middleware;
class Check
{
public function handle($request, \Closure $next)
{
if ($request->param('name') == 'think') {
return redirect('index/think');
}
return $next($request);
}
}
中间件的入口执行方法必须是handle方法,而且第一个参数是Request对象,第二个参数是一个闭包。
中间件handle方法的返回值必须是一个Response对象。
在这个中间件中我们判断当前请求的name参数等于think的时候进行重定向处理。否则,请求将进一步传递到应用中。要让请求继续传递到应用程序中,只需使用 $request 作为参数去调用回调函数 $next 。
在某些需求下,可以使用第三个参数传入额外的参数。
<?php
namespace app\middleware;
class Check
{
public function handle($request, \Closure $next, $name)
{
if ($name == 'think') {
return redirect('index/think');
}
return $next($request);
}
}
结束调度
中间件支持定义请求结束前的回调机制,你只需要在中间件类中添加end方法。
public function end(\think\Response $response)
{
// 回调行为
}
注意,在end方法里面不能有任何的响应输出。因为回调触发的时候请求响应输出已经完成了。
前置/后置中间件
中间件是在请求具体的操作之前还是之后执行,完全取决于中间件的定义本身。
下面是一个前置行为的中间件
<?php
namespace app\middleware;
class Before
{
public function handle($request, \Closure $next)
{
// 添加中间件执行代码
return $next($request);
}
}
下面是一个后置行为的中间件
<?php
namespace app\middleware;
class After
{
public function handle($request, \Closure $next)
{
$response = $next($request);
// 添加中间件执行代码
return $response;
}
}
中间件方法同样也可以支持依赖注入。
来个比较实际的例子,我们需要判断当前浏览器环境是在微信或支付宝
namespace app\middleware;
/**
* 访问环境检查,是否是微信或支付宝等
*/
class InAppCheck
{
public function handle($request, \Closure $next)
{
if (preg_match('~micromessenger~i', $request->header('user-agent'))) {
$request->InApp = 'WeChat';
} else if (preg_match('~alipay~i', $request->header('user-agent'))) {
$request->InApp = 'Alipay';
}
return $next($request);
}
}
然后在你的移动版的应用里添加一个middleware.php文件
例如:/path/app/mobile/middleware.php
return [
app\middleware\InAppCheck::class,
];
然后在你的controller中可以通过request()->InApp获取相关的值
定义中间件别名
可以直接在应用配置目录下的middleware.php中先预定义中间件(其实就是增加别名标识),例如:
return [
'alias' => [
'auth' => app\middleware\Auth::class,
'check' => app\middleware\Check::class,
],
];
可以支持使用别名定义一组中间件,例如:
return [
'alias' => [
'check' => [
app\middleware\Auth::class,
app\middleware\Check::class,
],
],
];
注册中间件
新版的中间件分为全局中间件、应用中间件(多应用模式下有效)、路由中间件以及控制器中间件四个组。执行顺序分别为:
全局中间件->应用中间件->路由中间件->控制器中间件
全局中间件
全局中间件在app目录下面middleware.php文件中定义,使用下面的方式:
<?php
return [
\app\middleware\Auth::class,
'check',
'Hello',
];
中间件的注册应该使用完整的类名,如果已经定义了中间件别名(或者分组)则可以直接使用。
全局中间件的执行顺序就是定义顺序。可以在定义全局中间件的时候传入中间件参数,支持两种方式传入。
<?php
return [
[\app\http\middleware\Auth::class, 'admin'],
'Check',
['hello','thinkphp'],
];
上面的定义表示 给Auth中间件传入admin参数,给Hello中间件传入thinkphp参数。
应用中间件
如果你使用了多应用模式,则支持应用中间件定义,你可以直接在应用目录下面增加middleware.php文件,定义方式和全局中间件定义一样,只是只会在该应用下面生效。
路由中间件
最常用的中间件注册方式是注册路由中间件
Route::rule('hello/:name','hello')
->middleware(\app\middleware\Auth::class);
支持注册多个中间件
Route::rule('hello/:name','hello')
->middleware([\app\middleware\Auth::class, \app\middleware\Check::class]);
然后,直接使用下面的方式注册中间件
Route::rule('hello/:name','hello')
->middleware('check');
支持对路由分组注册中间件
Route::group('hello', function(){
Route::rule('hello/:name','hello');
})->middleware('auth');
支持对某个域名注册中间件
Route::domain('admin', function(){
// 注册域名下的路由规则
})->middleware('auth');
如果需要传入额外参数给中间件,可以使用
Route::rule('hello/:name','hello')
->middleware('auth', 'admin');
如果需要定义多个中间件,使用数组方式
Route::rule('hello/:name','hello')
->middleware([Auth::class, 'Check']);
可以统一传入同一个额外参数
Route::rule('hello/:name','hello')
->middleware(['auth', 'check'], 'admin');
或者分开多次调用,指定不同的参数
Route::rule('hello/:name','hello')
->middleware('auth', 'admin')
->middleware('hello', 'thinkphp');
如果你希望某个路由中间件是全局执行(不管路由是否匹配),可以不需要在路由里面定义,支持直接在路由配置文件中定义,例如在config/route.php配置文件中添加:
'middleware' => [
app\middleware\Auth::class,
app\middleware\Check::class,
],
这样,所有该应用下的请求都会执行Auth和Check中间件。
使用闭包定义中间件
你不一定要使用中间件类,在某些简单的场合你可以使用闭包定义中间件,但闭包函数必须返回Response对象实例。
Route::group('hello', function(){
Route::rule('hello/:name','hello');
})->middleware(function($request,\Closure $next){
if ($request->param('name') == 'think') {
return redirect('index/think');
}
return $next($request);
});
控制器中间件
支持为控制器定义中间件,只需要在控制器中定义middleware属性,例如:
<?php
namespace app\controller;
class Index
{
protected $middleware = ['auth'];
public function index()
{
return 'index';
}
public function hello()
{
return 'hello';
}
}
当执行index控制器的时候就会调用auth中间件,一样支持使用完整的命名空间定义。
如果需要设置控制器中间的生效操作,可以如下定义:
<?php
namespace app\controller;
class Index
{
protected $middleware = [
'auth' => ['except' => ['hello'] ],
'check' => ['only' => ['hello'] ],
];
public function index()
{
return 'index';
}
public function hello()
{
return 'hello';
}
}
中间件向控制器传参
可以通过给请求对象赋值的方式传参给控制器(或者其它地方),例如
<?php
namespace app\middleware;
class Hello
{
public function handle($request, \Closure $next)
{
$request->hello = 'ThinkPHP';
return $next($request);
}
}
然后在控制器的方法里面可以直接使用
public function index(Request $request)
{
return $request->hello; // ThinkPHP
}
执行优先级
如果对中间件的执行顺序有严格的要求,可以定义中间件的执行优先级。在配置文件中添加
**
return [
'alias' => [
'check' => [
app\middleware\Auth::class,
app\middleware\Check::class,
],
],
'priority' => [
think\middleware\SessionInit::class,
app\middleware\Auth::class,
app\middleware\Check::class,
],
];
内置中间件
新版内置了几个系统中间件,包括:
这些内置中间件默认都没有定义,你可以在应用的middleware.php文件中、路由或者控制器中定义这些中间件,如果不需要使用的话,取消定义即可。
十、事件
新版的事件系统可以看成是5.1版本行为系统的升级版,事件系统相比行为系统强大的地方在于事件本身可以是一个类,并且可以更好的支持事件订阅者。
事件相比较中间件的优势是事件比中间件更加精准定位(或者说粒度更细),并且更适合一些业务场景的扩展。例如,我们通常会遇到用户注册或者登录后需要做一系列操作,通过事件系统可以做到不侵入原有代码完成登录的操作扩展,降低系统的耦合性的同时,也降低了BUG的可能性。
事件系统的所有操作都通过think\facade\Event类进行静态调用
V6.0.3+版本开始,事件机制不能关闭
定义事件
事件系统使用了观察者模式,提供了解耦应用的更好方式。在你需要监听事件的位置,例如下面我们在用户完成登录操作之后添加如下事件触发代码:
// 触发UserLogin事件 用于执行用户登录后的一系列操作
Event::trigger('UserLogin');
或者使用助手函数
event('UserLogin');
这里UserLogin表示一个事件标识,如果你定义了单独的事件类,你可以使用事件类名(甚至可以传入一个事件类实例)。
// 直接使用事件类触发
event('app\event\UserLogin');
事件类可以通过命令行快速生成
php think make:event UserLogin
默认会生成一个app\event\UserLogin事件类,也可以指定完整类名生成。
我们可以给事件类添加方法
namespace app\event;
use app\model\User;
class UserLogin
{
public $user;
public function __construct(User $user)
{
$this->user = $user;
}
}
一般事件类无需继承任何其它类。
你可以给事件类绑定一个事件标识,一般建议直接在应用的event.php事件定义文件中批量绑定。
return [
'bind' => [
'UserLogin' => 'app\event\UserLogin',
// 更多事件绑定
],
];
如果你需要动态绑定,可以使用
Event::bind(['UserLogin' => 'app\event\UserLogin']);
ThinkPHP的事件系统不依赖事件类,如果没有额外的需求,仅通过事件标识也可以使用,省去定义事件类的麻烦。
如果你没有定义事件类的话,则无需绑定。对于大部分的场景,可能确实不需要定义事件类。
你可以在event方法中传入一个事件参数
// user是当前登录用户对象实例
event('UserLogin', $user);
如果是定义了事件类,可以直接传入事件对象实例
// user是当前登录用户对象实例
event(new UserLogin($user));
事件监听
你可以手动注册一个事件监听
Event::listen('UserLogin', function($user) {
//
});
或者使用监听类来执行监听
Event::listen('UserLogin', 'app\listener\UserLogin');
可以通过命令行快速生成一个事件监听类
php think make:listener UserLogin
默认会生成一个app\listener\UserLogin事件监听类,也可以指定完整类名生成。
事件监听类只需要定义一个handle方法,支持依赖注入。
<?php
namespace app\listener;
class UserLogin
{
public function handle($user)
{
// 事件监听处理
}
}
在handle方法中如果返回了false,则表示监听中止,将不再执行该事件后面的监听。
一般建议直接在事件定义文件中定义对应事件的监听。
return [
'bind' => [
'UserLogin' => 'app\event\UserLogin',
// 更多事件绑定
],
'listen' => [
'UserLogin' => ['app\listener\UserLogin'],
// 更多事件监听
],
];
事件订阅
可以通过事件订阅机制,在一个监听器中监听多个事件,例如通过命令行生成一个事件订阅者类,
php think make:subscribe User
默认会生成app\subscribe\User类,或者你可以指定完整类名生成。
然后你可以在事件订阅类中添加不同事件的监听方法,例如。
<?php
namespace app\subscribe;
class User
{
public function onUserLogin($user)
{
// UserLogin事件响应处理
}
public function onUserLogout($user)
{
// UserLogout事件响应处理
}
}
监听事件的方法命名规范是on+事件标识(驼峰命名),如果希望统一添加事件前缀标识,可以定义eventPrefix属性。
<?php
namespace app\subscribe;
class User
{
protected $eventPrefix = 'User';
public function onLogin($user)
{
// UserLogin事件响应处理
}
public function onLogout($user)
{
// UserLogout事件响应处理
}
}
如果希望自定义订阅方式(或者方法规范),可以定义subscribe方法实现。
<?php
namespace app\subscribe;
use think\Event;
class User
{
public function onUserLogin($user)
{
// UserLogin事件响应处理
}
public function onUserLogout($user)
{
// UserLogout事件响应处理
}
public function subscribe(Event $event)
{
$event->listen('UserLogin', [$this,'onUserLogin']);
$event->listen('UserLogout',[$this,'onUserLogout']);
}
}
然后在事件定义文件注册事件订阅者
return [
'bind' => [
'UserLogin' => 'app\event\UserLogin',
// 更多事件绑定
],
'listen' => [
'UserLogin' => ['app\listener\UserLogin'],
// 更多事件监听
],
'subscribe' => [
'app\subscribe\User',
// 更多事件订阅
],
];
如果需要动态注册,可以使用
Event::subscribe('app\subscribe\User');
内置事件
内置的系统事件包括:
AppInit事件定义必须在全局事件定义文件中定义,其它事件支持在应用的事件定义文件中定义。
原来5.1的一些行为标签已经废弃,所有取消的标签都可以使用中间件更好的替代。可以把中间件看成处理请求以及响应输出相关的特殊事件。事实上,中间件的handler方法只是具有特殊的参数以及返回值而已。
数据库操作的回调也称为查询事件,是针对数据库的CURD操作而设计的回调方法,主要包括:
查询事件的参数就是当前的查询对象实例。
before_write和after_write事件无论是新增还是更新都会执行。
模型事件方法的参数就是当前的模型对象实例。