一、请求流程
对于一个HTTP应用来说,从用户发起请求到响应输出结束,大致的标准请求流程如下:
- 载入Composer的自动加载autoload文件
- 实例化系统应用基础类think\App
- 获取应用目录等相关路径信息
- 加载全局的服务提供provider.php
- 设置容器实例及应用对象实例,确保当前容器对象唯一
- 从容器中获取HTTP应用类think\Http
- 执行HTTP应用类的run方法启动一个HTTP应用
- 获取当前请求对象实例(默认为app\Request继承think\Request)保存到容器
- 执行think\App类的初始化方法initialize
- 加载环境变量文件.env和全局初始化文件
- 加载全局公共文件、系统助手函数、全局配置文件、全局事件定义和全局服务定义
- 判断应用模式(调试或者部署模式)
- 监听AppInit事件
- 注册异常处理
- 服务注册
- 启动注册的服务
- 加载全局中间件定义
- 监听HttpRun事件
- 执行全局中间件
- 执行路由调度(Route类dispatch方法)
- 如果开启路由则检查路由缓存
- 加载路由定义
- 监听RouteLoaded事件
- 如果开启注解路由则检测注解路由
- 路由检测
- 路由调度对象think\route\Dispatch初始化
- 设置当前请求的控制器和操作名
- 注册路由中间件
- 绑定数据模型
- 设置路由额外参数
- 执行数据自动验证
- 执行路由调度子类exec方法返回响应think\Response对象
- 获取当前请求的控制器对象实例
- 利用反射机制注册控制器中间件
- 执行控制器方法以及前后置中间件
- 执行当前响应对象的send方法输出
- 执行HTTP应用对象的end方法善后
- 监听HttpEnd事件
- 执行中间件的end回调
- 写入当前请求的日志信息
至此,当前请求流程结束。
二、架构总览
ThinkPHP支持传统的MVC(Model-View-Controller)模式以及流行的MVVM(Model-View-ViewModel)模式的应用开发,下面的一些概念有必要做下了解,可能在后面的内容中经常会被提及。
1、入口文件
用户请求的PHP文件,负责处理请求(注意,不一定是HTTP请求)的生命周期,入口文件位于public目录下面,最常见的入口文件就是index.php,6.0支持多应用多入口,你可以给每个应用增加入口文件,例如给后台应用单独设置的一个入口文件admin.php。
2、应用
6.0版本提供了对多应用的良好支持,每个应用是一个app目录的子目录(或者指定的composer库),每个应用具有独立的路由、配置,以及MVC相关文件,这些应用可以公用框架核心以及扩展。而且可以支持composer应用加载。
3、容器
ThinkPHP使用(对象)容器统一管理对象实例及依赖注入。
容器类的工作由think\Container类完成,但大多数情况下我们都是通过应用类(think\App类)或是app助手函数来完成容器操作,容器中所有的对象实例都可以通过容器标识单例调用,你可以给容器中的对象实例绑定一个对象标识,如果没有绑定则使用类名作为容器标识。
4、系统服务
系统服务的概念是指在执行框架的某些组件或者功能的时候需要依赖的一些基础服务,服务类通常可以继承系统的think\Service类,但并不强制。
你可以在系统服务中注册一个对象到容器,或者对某些对象进行相关的依赖注入。由于系统服务的执行优先级问题,可以确保相关组件在执行的时候已经完成相关依赖注入。
5、路由
路由是用于规划请求的访问地址,在访问地址和实际操作方法之间建立一个路由规则=> 路由地址的映射关系。
ThinkPHP并非强制使用路由,如果没有定义路由,则可以直接使用“控制器/操作”的方式访问,如果定义了路由,则该路由对应的路由地址就不能直接访问了。一旦开启强制路由参数,则必须为每个请求定义路由(包括首页)。
使用路由有一定的性能损失,但随之也更加安全,因为每个路由都有自己的生效条件,如果不满足条件的请求是被过滤的。远比你在控制器的操作中进行各种判断要实用得多。
其实路由的作用远非URL规范这么简单,还可以实现验证、权限、参数绑定及响应设置等功能。
6、控制器
每个应用下面拥有独立的类库及配置文件,一个应用下面有多个控制器负责响应请求,而每个控制器其实就是一个独立的控制器类。
控制器主要负责请求的接收,并调用相关的模型处理,并最终通过视图输出。严格来说,控制器不应该过多的介入业务逻辑处理。
事实上,控制器是可以被跳过的,通过路由我们可以直接把请求调度到某个模型或者其他的类进行处理。
ThinkPHP的控制器是可以被跳过的,通过路由我们可以直接把请求调度到某个模型或者其他的类进行处理。
ThinkPHP的控制器类比较灵活,可以无需继承任何基础类库。
一个典型的Index控制器类(单应用模式)如下:
<?php
namespace app\controller;
class Index
{
public function index()
{
return 'hello,thinkphp!';
}
}
一般建议继承一个基础的控制器,方便扩展。系统默认提供了一个app\BaseController控制器类。
7、操作
一个控制器包含多个操作(方法),操作方法是一个URL访问的最小单元。
下面是一个典型的Index控制器的操作方法定义,包含了两个操作方法:
<?php
namespace app\controller;
class Index
{
public function index()
{
return 'index';
}
public function hello(string $name)
{
return 'Hello,'.$name;
}
}
操作方法可以不使用任何参数,如果定义了一个非可选参数,并且不是对象类型,则该参数必须通过用户请求传入,如果是URL请求,则通常是通过当前的请求传入,操作方法的参数支持依赖注入。
8、模型
模型类通常完成实际的业务逻辑和数据封装,并返回和格式无关的数据。
模型类并不一定要访问数据库,而且在ThinkPHP的架构设计中,只有进行实际的数据库查询操作的时候,才会进行数据库的连接,是真正的惰性连接。
ThinkPHP的模型层支持多层设计,你可以对模型层进行更细化的设计和分工,例如把模型层分为逻辑层/服务层/事件层等等。
模型类通常需要继承think\Model类,一个典型的User模型器类如下:
<?php
namespace app\model;
use think\Model;
class User extends Model
{
}
9、视图
控制器调用模型类后,返回的数据通过视图组装成不同格式的输出。视图根据不同的需求,来决定调用模板引擎进行内容解析后输出还是直接输出。
视图通常会有一系列的模板文件对应不同的控制器和操作方法,并且支持动态设置模板目录。
10、模板引擎
模板文件中可以使用一些特殊的模板标签,这些标签的解析通常由模板引擎负责实现。
新版不再内置think-template模板引擎,如果需要使用ThinkPHP官方模板引擎,需要单独安装think-view模板引擎驱动扩展。
11、驱动
系统很多的组件都采用驱动式设计,从而可以更灵活的扩展,驱动类的位置默认是放入核心类库目录下面,也可以重新定义驱动类库的命名空间而改变驱动的文件位置。
6.0版本的驱动采用Composer的方式安装和管理。
12、中间件
中间件主要用于拦截或过滤应用的HTTP请求,并进行必要的业务处理。
新版部分核心功能使用中间件处理,你可以灵活关闭。包括Session功能、请求缓存和多语言功能。
13、事件
6.0已经使用事件机制代替原来的行为和Hook机制,可以在应用中使用事件机制的特性来扩展功能。
此外数据库操作和模型操作在完成数据操作的回调机制,也使用了事件机制。
14、助手函数
系统为一些常用的操作提供了助手函数支持。使用助手函数和性能并无直接影响,只是某些时候无法享受IDE自动提醒的便利,但是是否使用助手函数看项目自身规范,在应用的公共函数文件也可以对系统提供的助手函数进行重写。
三、入口文件
ThinkPHP6.0采用单一入口模式进行项目部署和访问,一个应用都有一个统一(但不一定是唯一)的入口。如果采用自动多应用部署的话,一个入口文件还可以自动对应多个应用。
1、入口文件定义
默认的应用入口文件位于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访问目录下面。
2、控制台入口文件
除了应用入口文件外,系统还提供了一个控制台入口文件,位于项目根目录的think(注意该文件没有任何的后缀)。
该入口文件代码如下:
#!/usr/bin/env php
<?php
namespace think;
// 加载基础文件
require __DIR__ . '/vendor/autoload.php';
// 应用初始化
(new App())->console->run();
控制台入口文件用于执行控制台指令,例如:
php think version
系统内置了一些常用的控制台指令,如果你安装了额外的扩展,也会增加相应的控制台指令,都是通过该入口文件执行的。
四、多应用模式
1、多应用
安装后默认使用单应用模式部署,目录结构如下:
├─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应用运行时目录
从目录结构可以看出,每个应用相对保持独立,并且可以支持多个入口文件,应用下面还可以通过多级控制器来维护控制器分组。
2、自动多应用部署
支持在同一个入口文件中访问多个应用,并且支持应用的映射关系以及自定义。如果你通过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里面的应用参数。但可以使用域名绑定解决。
3、多应用智能识别
如果没有绑定入口或者域名的情况下,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应用下的路由。
4、增加应用入口
允许为每个应用创建单独的入口文件而不通过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);
5、获取当前应用
如果需要获取当前的应用名,可以使用
app('http')->getName();
6、应用目录获取
单应用和多应用模式会影响一些系统路径的值,为了更好的理解本手册的内容,你可能需要理解下面几个系统路径所表示的位置。
目录位置 | 目录说明 | 获取方法(助手函数) |
---|---|---|
根目录 | 项目所在的目录,默认自动获取,可以在入口文件实例化App类的时候传入 | root_path() |
基础目录 | 根目录下的app目录 | base_path() |
应用目录 | 当前应用所在的目录,如果是单应用模式则同基础目录,如果是多应用模式,则是app/应用子目录 | app_path() |
配置目录 | 根目录下的config目录 | config_path() |
运行时目录 | 框架运行时的目录,单应用模式就是根目录的runtime目录,多应用模式为runtime/应用子目录 | runtime_path() |
注意:应用支持使用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);
7、应用映射
自动多应用模式下,支持应用的别名映射,例如:
'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');
},
],
8、域名绑定应用
如果你的多应用使用多个子域名或者独立域名访问,你可以在config/app.php配置文件中定义域名和应用的绑定。
'domain_bind'