ThinkPHP5.1
从2018年初发布到现在,一路小步快跑,快速迭代的同时也更新了大量的新特性。
那么到底是否值得升级到新版呢?如果你还在纠结选用5.0的商城系统还是选用thinkphp 5.1的系统,本文就和大家分享下值得升级到
5.1
的18个理由。
新手服务器环境不会搭?pathinfo
不支持?告别这些烦恼吧,5.1
内置了一个快速启动测试服务器的指令,你只需要在命令行下切换到项目根目录下面输入:
php think run
即可在localhost:8000
启动一个测试服务器,而无需任何其它的WEB服务器环境,告别新手搭建环境各种问题的困惑。
相比较而言,在5.0
中,必须使用
php -S localhost:8000 public/router.php
当然,该指令也支持指定端口和地址(域名),下面就是指定通过tp.com
(需要自己添加host)域名的80端口访问。
php think run -H tp.com -p 80
5.1
正式引入了对象容器,可以更方便的快速存取对象和管理依赖注入。这是5.1
基础架构的一大调整,5.0没有容器的概念,所有的依赖对象都在请求对象中,依赖注入虽有支持但不够规范也不通用化。有了容器对象,所有的依赖对象都可以在容器里面进行统一的实例化或者绑定,需要的时候可以直接拿出来,对于解耦和减少直接依赖很有帮助。
5.1
的应用类think\App
本身就是容器,同时还充当了容器的管理对象,你不需要直接操作容器类就能对容器中的对象实例进行存取。而App类通常是在入口文件中实例化的,也就是说在App类实例化的同时,这个容器对象就已经准备好了。
而App类本身也提供了门面方式的静态调用,因此,你可以像下面一样方便调用。
use think\facade\App;
// 绑定对象实例到容器
App::bindTo('myClass',$obj);
// 为了避免容器过大 通常只是绑定一个类而不是对象
App::bindTo('myClass','namespace\ClassName');
// 快速获取对象实例
$obj = App::make('myClass');
// 无需绑定操作即可获取对象实例
$obj = App::make('namespace\ClassName');
容器对象中的实例默认都是单例的,如果你需要获取新的对象实例,可以在make的时候指定总是重新实例化
// 创建一个新的对象实例
$obj = App::make('namespace\ClassName',true);
// 如果需要实例化的时候传入参数,则使用
$obj = App::make('namespace\ClassName', ['name' => 'think'], true);
当然,这个只是容器内部的调用机制,实际到应用层面,大多数时候容器对象实例的获取是在依赖注入的时候自动完成的,因此你可能根本不需要用到上面的代码或者说关心容器的存在。下面就是一个典型的依赖注入的例子:
fun();
}
}
ThinkPHP5.1
没有服务提供者的概念,如果你需要对容器中的对象进行自定义实例化,可以在你的对象中添加__make
静态方法来完成。
use think\App;
class ClassName
{
/**
* 名称
* @var string
*/
protected $name;
/**
* 构造方法
* @access public
*/
public function __construct($name = '')
{
$this->name = $name;
}
public static function __make(App $app)
{
$name = $app['config']->get('app_name');
return new static($name);
}
}
现在当你在操作方法中依赖注入ClassName对象(或者通过容器的make方法获取对象实例)的时候,就会自动调用__make
方法进行自定义实例化。
正因为容器的引入,使得
5.1
的依赖注入得以更加完善,并且支持了更多的依赖注入的场景,包括控制器架构函数,操作方法,事件响应,路由定义,模型事件等等。
5.1
的核心类基本上都改造成了动态类,对需要提供静态调用的类则改为提供门面对象支持,好处是可以更好的支持单元测试,由于容器的支持而使得门面对象天生具有单例的特性。
但因为用法的改变同时也让门面变成了
5.0
升级到5.1
最大的障碍,因为你不得不调整类库的引入。
很典型的5.0
中经常使用的Route
类、Session
类在5.1
中类的方法本身没有变化,只是不再是原来的think\Route
和think\Session
静态类,必须改成门面静态代理类:
use think\facade\Route;
use think\facade\Session;
Route::rule('hello/:name','index/hello');
Session::get('user_name');
和依赖注入一样,通过门面对象操作的时候会自动获取容器中的对象实例(如果不存在的话则会自动实例化),因此你不用关心对象是如何实例化的。你操作think\facade\Route
就和操作think\Route
类是一个效果(除了使用的是静态方法之外)。门面对象唯一的不足是对IDE的自动提示不够,但可以通过给门面类添加注释解决,系统内置的门面类均有自动提示。
5.1
的路由基本上完全重构了,但最大程度的保留(或者说兼容)了5.0
的用法,把原本的一个Route
类拆分成独立的多个职责明确的类库,并且优化了路由匹配的算法,而且支持路由解析的缓存,极大提升了路由的性能。
路由定义方面主要对域名路由和路由分组的功能进行了强化,以及路由规则的更灵活定义。
之前的路由规则中变量分两种,普通变量和组合变量定义,新版把这两种合二为一了(也就是说两种变量没有任何区别,仅仅是表现方式不同,而且是出于兼容考虑)。你现在完全可以在路由规则中以任何方式定义路由变量。
例如你可以使用下面的路由规则而不用管目前的URL分隔符是什么:
Route::rule('item/:name_:id', 'order/index');
Route::rule('product-:name', 'product/item');
另外,新版路由定义两种变量方式:name
和
可以混合使用,但建议统一使用
,在性能上略有优势(实际上,最终解析的时候系统会统一解析成后者)。
路由检测其实是最耗性能的,尤其是路由匹配这块,因为基本上都是采用的正则匹配。在5.0
版本之前,如果定义了100个路由,那么最后的那个路由规则可能需要遍历100次才能正确匹配到路由,而且每个路由规则中的路由变量都是单独匹配的,所以这个路由匹配的性能开销是随着路由定义的数量指数上升的。
5.1
版本系统对路由的匹配这块进行了算法优化,灵感来自于fastRoute
。基本思想是分两个步骤优化(思想其实很容易懂,但技术层面比较复杂,所以想了解怎么实现的话还是仔细看代码吧~本文只阐述思想)。
第一个步骤是对单个路由规则的匹配算法进行调整,不按照变量进行多次匹配,而是把路由变量的正则合并到一起,然后整个路由规则只匹配一次(如果这个路由规则都是静态的,那么基本上不需要正则匹配,采用的是更快的非正则检测方式)。这个是新版默认就开启的,相比较之前的版本,性能已经提升明显。
但路由的性能提速远非如此简单,第二个步骤的优化策略是如果当前匹配的路由分组下面有多个路由规则(确切的说是满足当前请求类型的),则把这些满足条件的路由规则合并成一个正则表达式进行路由匹配。如果你的路由分组下面有100个满足条件的路由规则,如果要访问最后的路由规则,之前的方式可能需要遍历100次,而新版只需要进行一次匹配。就算加上路由规则的合并开销,仍然是值得的。
而且新版路由支持开启路由缓存(仅在部署模式下有效),在定义了大量的路由规则之后,开启该缓存可以明显提升路由解析性能,而且路由规则越多越明显。
由于更加对象化,新版的路由更建议采用方法定义,5.0
的路由参数在5.1
中都可以用链式方法设置,还可以享受IDE的自动提示功能。
// 给路由规则添加参数
Route::get('hello/:name','index/hello')
->ext('html')
->ajax()
->https()
->domain('thinkphp.cn');
新版一个很有用的参数是可以用filter
方法检测当前请求的参数是否匹配
// 给路由规则添加参数
Route::get('hello/:name','index/hello')
// 只有当前请求的type参数为vip的时候才匹配路由
->filter('type', 'vip');
5.1支持注解路由,可以在你的控制器文件中通过注解的方式快速定义路由,在调试模式下实时生效,在部署模式下则需要运行一下指令生成注解路由。注解路由可以和路由定义文件同时定义,并且注解路由优先。
开启注解路由后,你只需要直接在控制器类的方法注释中定义(通常称之为注解路由),例如:
就相当于注册了一条下面的路由规则
Route::rule('hello/:name','index/hello');
默认注册的路由规则是支持所有的请求,如果需要指定请求类型,可以在第二个参数中指定请求类型:
相当于注册了一条下面的路由规则
Route::get('hello/:name','index/hello');
如果有路由参数和变量规则需要定义,可以直接在后面添加方法,例如:
https()
* ->pattern(['name' => '\w+'])
*
* @return mixed
*/
public function hello($name)
{
return 'hello,'.$name;
}
}
就相当于注册了一条下面的路由规则
Route::get('hello/:name','index/hello')
->https()
->pattern(['name' => '\w+']);
如果是资源路由则更为简单,在类的开头注释中添加注解就行了,例如:
就相当于注册了一条下面的路由规则
Route::resource('blog','blog');
如果某个路由或者分组需要支持跨域请求,可以使用
Route::get('new/:id', 'News/read')
->ext('html')
->allowCrossDomain();
跨域请求一般会发送一条
OPTIONS
的请求,一旦设置了跨域请求的话,不需要自己定义OPTIONS
请求的路由,系统会自动加上。
跨域请求系统会默认带上一些Header,包括:
Access-Control-Allow-Origin:*
Access-Control-Allow-Methods:GET, POST, PATCH, PUT, DELETE
Access-Control-Allow-Headers:Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, X-Requested-With
你可以添加或者更改Header信息,使用
Route::get('new/:id', 'News/read')
->ext('html')
->header('Access-Control-Allow-Origin','thinkphp.cn')
->header('Access-Control-Allow-Credentials', 'true')
->allowCrossDomain();
5.1
正式引入中间件,包括路由中间件和控制器中间件。例如通过对路由分组或者路由单独设置中间件,可以灵活的控制访问权限,并且把中间件的逻辑和你的控制器逻辑可以分离设计,最大程度的复用中间件。
中间件的执行是在控制器的操作方法执行前后(所以能替代之前的路由后置行为,控制器的初始化操作和前置操作行为,但不能替代所有的行为,这点一点要区分清楚),所以包含前置中间件和后置中间件两种类型,你可以根据需求定义不同的中间件。路由中间件其实就是在路由中定义好要执行哪个中间件,而控制器中间件是当你没有使用路由的情况下,在控制器里面直接定义要执行的中间件。
可以通过命令行快速生成一个中间件类,然后加上你的业务逻辑。
php think make:middleware CheckAuth
会自动生成一个 app\http\middleware\CheckAuth
的中间件类,然后修改handle
方法代码如下:
';
$result = $next($request);
echo 'after check
';
}
}
你可以在你的路由定义中指定中间件
Route::group('hello', function(){
// 定义分组路由
// 。。。
Route::get(':name', 'index/hello');
})->middleware('CheckAuth');
如果要指定多个中间件,可以使用数组的方式
Route::group('hello', function(){
// 定义分组路由
// 。。。
})->middleware(['CheckAuth', 'CheckName']);
当你没有使用路由的情况下,可以在控制器里面直接定义中间件,如下:
如果你需要在中间件和控制器之间传递参数,可以通过给Request
对象赋值
auth = 'admin';
return $next($request);
}
}
注意,传递的变量名称不要和
param
变量有冲突。
然后在控制器的方法里面可以直接使用
public function index(Request $request)
{
return $request->auth; // admin
}
搜索器功能对于规范和简化数据的表单搜索非常有效,甚至还能支持排序和多表查询,让你的控制器代码减少到最低。
你不需要在控制器中或者模型的方法中使用大量繁琐而且重复的查询代码,只需要在模型类中添加类似下面的搜索器方法,化繁为简。
where('name','like', $value . '%');
}
public function searchCreateTimeAttr($query, $value, $data)
{
$query->whereBetweenTime('create_time', $value[0], $value[1]);
}
}
然后,我们可以使用下面的查询
User::withSearch(['name','create_time'], [
'name' => 'think',
'create_time' => ['2018-8-1','2018-8-5'],
'status' => 1
])
->select();
自动会调用name
和create_time
字段对应的搜索器方法完成查询条件。
事实上,除了在搜索器中使用查询表达式外,还可以使用其它的任何查询构造器以及链式操作。
例如,你需要通过表单定义的排序字段进行搜索结果的排序,可以使用
where('name','like', $value . '%');
if (isset($data['sort'])) {
$query->order($data['sort']);
}
}
public function searchCreateTimeAttr($query, $value, $data)
{
$query->whereBetweenTime('create_time', $value[0], $value[1]);
}
}
然后,我们可以使用下面的查询
User::withSearch(['name','create_time', 'status'], [
'name' => 'think',
'create_time' => ['2018-8-1','2018-8-5'],
'status' => 1,
'sort' => ['status'=>'desc'],
])
->select();
你可以在查询的时候使用PHP的生成器特性,使用cursor
查询可以让你在不耗费内存的情况下处理大量数据集结果。
$users = User::where('status', 1)->limit(10000)->curosr();
// users 是一个Generator对象
foreach($users as $user) {
// 对数据进行处理
// 。。。
}
如果不使用生成器对象,从数据库里面获取1万条数据就有可能让内存暴增。
5.1
的模型查询统一返回模型对象或者包含模型的数据集对象,你可以和模型一样操作数据集对象。并且改进了数据集对象对数据的处理能力。
其实数据集的用法和数组基本一样,例如可以遍历和直接获取某个元素。
// 模型查询返回数据集对象
$list = User::where('id', '>', 0)->select();
// 获取数据集的数量
echo count($list);
// 直接获取其中的某个元素
dump($list[0]);
// 遍历数据集对象
foreach ($list as $user) {
dump($user);
}
// 删除某个元素
unset($list[0]);
唯一的不同恐怕就是判断数据集是否为空,不能直接使用empty
判断,而必须使用数据集对象的isEmpty
方法判断,例如:
$users = User::select();
if ($users->isEmpty()) {
echo '数据集为空';
}
不过通用的办法是判断下数据集的元素
$users = User::select();
if (count($users)==0) {
echo '数据集为空';
}
你可以使用模型的hidden
/visible
/append
/withAttr
方法进行数据集的输出处理,例如:
// 模型查询返回数据集对象
$list = User::where('id', '>', 0)->select();
// 对输出字段进行处理
$list->hidden(['password'])
->append(['status_text'])
->withAttr('name', function($value, $data) {
return strtolower($value);
});
dump($list);
如果需要对数据集的结果进行筛选和排序,可以使用
// 模型查询返回数据集对象
$list = User::where('status', 1)->select()
// 对数据集进行数据过滤以及排序
->where('name', 'think')
->where('score', '>', 80)
->order('create_time', 'desc');
dump($list);
还有大量的实用方法等你去挖掘~
你可以像使用对象一样来操作JSON字段,并且字段不必是JSON类型,支持varchar类型,只要你存入的数据格式是JSON类型。
定义User模型类
定义后,写入JSON数据。
$user = new User;
$user->name = 'thinkphp';
// 写入JSON类型字段数据
$user->info = [
'email' => '[email protected]',
'nickname '=> '流年',
];
// 或者用对象方式
$info = new StdClass();
$info->email = '[email protected]';
$info->nickname = '流年';
$user->info = $info;
$user->save();
查询JSON数据
$user = User::get(1);
echo $user->name; // thinkphp
echo $user->info->email; // [email protected]
echo $user->info->nickname; // 流年
查询条件为JSON数据
$user = User::where('info->nickname','流年')->find();
echo $user->name; // thinkphp
echo $user->info->email; // [email protected]
echo $user->info->nickname; // 流年
支持设置模型的JSON
数据返回数组。
设置后,查询代码调整为:
$user = User::get(1);
echo $user->name; // thinkphp
echo $user->info['email']; // [email protected]
echo $user->info['nickname']; // 流年
可以单独设置JSON字段属性的类型
'int'
];
}
没有定义类型的属性默认为字符串类型,因此字符串类型的属性可以无需定义。
更新JSON数据也是类似
$user = User::get(1);
$user->name = 'kancloud';
$user->info->email = '[email protected]';
$user->info->nickname = 'kancloud';
$user->save();
你可以不用在模型里面定义获取器方法,而是可以使用withAttr
方法动态定义获取器(而且适用于Db类的查询结果)。
User::withAttr('name', function($value, $data) {
return strtolower($value);
})->select();
如果同时还在模型里面定义了相同字段的获取器,则动态获取器优先,也就是可以临时覆盖定义某个字段的获取器。
支持对关联模型的字段使用动态获取器,例如:
User::with('profile')->withAttr('profile.name', function($value, $data) {
return strtolower($name);
})->select();
并且支持对JSON
字段使用获取器,例如在模型中定义了JSON
字段的话:
可以使用下面的代码定义JSON字段的获取器。
User::withAttr('info.name', function($value, $data) {
return strtolower($name);
})->select();
可以在查询之后使用withAttr
方法。
User::select()->withAttr('name', function($value, $data) {
return strtolower($value);
});
Db类也可以支持获取器定义。
Db::name('user')->withAttr('name', function($value, $data) {
return strtolower($value);
})->select();
上面的代码,查询的数据集数据中的name
字段的值会统一进行小写转换。
支持对JSON字段定义获取器。
$user = Db::name('user')
->json(['info'])
->withAttr('info.name', function($value, $data) {
return strtolower($value);
})->find(1);
dump($user);
查询结果返回的时候,会自动对info
字段(JSON
字段)的name
属性使用获取器操作。
由于5.1的数组查询方式改变导致很多人不敢升级,对于习惯或者重度依赖5.0数组查询条件的用户来说,可以选择数组对象查询,该对象完成了普通数组方式查询和系统的查询表达式之间的桥接。
使用方法如下:
use think\db\Where;
// 这个数组条件用法完全和5.0一样
// 区别只是在传入where方法的时候使用 new Where($map) 代替 $map
$map = [
'name' => ['like', 'thinkphp%'],
'title' => ['like', '%think%'],
'id' => ['>', 10],
'status' => 1,
];
// 或者先创建Where对象(数组查询对象)
$where = new Where;
$where['id'] = ['in', [1, 2, 3]];
$where['title'] = ['like', '%php%'];
Db::table('think_user')
->where(new Where($map))
->whereOr($where->enclose())
->select();
enclose
方法表示该查询条件两边会加上括号包起来。现在你可以毫不犹豫的升级到5.1
版本而不用担心花费大量时间重构你的查询代码了。
很多新手都有一个困惑,使用find或者模型的get方法查询的时候,都忽略要先判断下数据是否存在,才能进行后续操作,现在方便了,你只需要在查询之前调用allowEmpty
方法,或者直接使用findOrEmpty
方法进行查询,当查询数据不存在的时候返回空数组或者空的模型对象。
// 始终返回一个User模型对象
$user = User::findOrEmpty(1024);
// 始终返回数组,而不是null
$user = Db::name('user')->findOrEmpty(1024);
5.1
核心调整后可以更简单的支持Swoole
,使用官方的扩展(topthink/think-swoole
)的话,还可以直接通过命令行指令启动Swoole
服务,让你的应用可以直接部署在Swoole
上,从而带来性能的再次提升。如果要在5.0
上面支持这些,几乎不可能或者会非常困难。
直接在命令行下启动Swoole HTTP Server服务端。
php think swoole
启动完成后,会在0.0.0.0:9501
启动一个HTTP Server,可以直接访问当前的应用。
swoole
的参数可以在应用配置目录下的swoole.php
里面配置(具体参考配置文件内容)。
如果需要使用守护进程方式运行,可以使用
php think swoole -d
支持reload
/stop
/restart
等操作
php think swoole reload
并且可以支持文件监控,项目代码修改会自动reload
服务,而无需手动重启,方便代码调试。
如果你要启动其它的服务(例如websocket服务),可以使用
php think swoole:server
会在0.0.0.0:9508
启动一个Websocket服务。
同样通过topthink/think-worker
扩展提供了对workerman
和GatewayWorker
的支持。
验证规则的定义也可以告别数组方式,而改用对象化的方式,而且也新增了一些内置验证规则。
$validate = new \think\Validate;
$validate->rule('age', 'number|between:1,120')
->rule([
'name' => 'require|max:25',
'email' => 'email'
]);
$data = [
'name' => 'thinkphp',
'email' => '[email protected]'
];
if (!$validate->check($data)) {
dump($validate->getError());
}
上面的写法在5.1中可以改为对象化的方式
use think\validate\ValidateRule as Rule;
$validate = new \think\Validate;
$validate->rule('age', Rule::isNumber()->between([1,120]))
->rule([
'name' => Rule::isRequire()->max(25),
'email' => Rule::isEmail(),
]);
$data = [
'name' => 'thinkphp',
'email' => '[email protected]'
];
if (!$validate->check($data)) {
dump($validate->getError());
}
原来5.0
的模型自动验证已经取消了,必须改为控制器自动验证或者使用路由验证。总结一句话,验证操作应该尽可能的提前。
5.1
配置文件和路由定义的目录层级提升(在应用同级目录),配置文件支持统一管理,不用在各个模块目录下面寻找,但仍然支持在模块的config
目录下面单独定义,便于模块化开发。
配置全部使用二级方式,每个配置文件的文件名就是一级配置名,清晰直观,需要增加新的配置文件直接放到配置目录下即可,无需任何的扩展配置。如果你安装了Yaconf
扩展,那么还支持使用Yaconf
定义配置。
路由定义文件则统一放到route
目录,支持分开多个文件进行路由管理。
5.1
的日志遵循PSR-3
日志规范,相比较5.0
版本,增加如下支持:
其实上面提到的特性没法完全涵盖5.1的特性,尤其是路由、查询以及模型这块做了大量的细节改进,更多的5.1
改进,我建议你参考下详细的更新日志。
总结来说,
5.1
版本比5.0
版本更加规范和更接近现代化开发,所以如果你还在5.0
和5.1
之间犹豫不决的话,还是推荐更优秀的5.1
版本。虽然5.1
本身性能提升可能不至于很高,如果你大量使用路由的话,性能提升还是相当显著的。而且益于对Swoole
和Workerman
的支持,性能还可以提升一个层次,不过5.0
升级到5.1
并不是无缝升级的,有一定工作量(官方提供了详细的升级指导),但长期来说,这点成本仍然是值得的。最后,祝大家早日掌握5.1开发~如果你还在选择thinkphp 5.0的商城系统还是5.1的商城系统,我建议你还是选择ThinkPHP 5.1框架的商淘多用户商城系统吧。