2020年我们也做了一次thinkphp6.0的试用报告,上个月我们又使用了thinkphp6.0.7版本,在原试用报告的基础上补充与调整,得到一份所有模块基础报告,一方面可以让我们快速了解TP6中的一些重要概念,也能快速了解框架的优缺点。

目录

环境要求

  • php > 7.1

  • 必须使用composer安装及更新

安装

PS I:\src\tp6> composer create-project topthink/think tp
Installing topthink/think (v6.0.0)
- Installing topthink/think (v6.0.0): Downloading (100%)
Created project in tp
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 14 installs, 0 updates, 0 removals
- Installing psr/container (1.0.0): Downloading (100%)
- Installing topthink/think-helper (v3.1.3): 

...

Writing lock file
Generating autoload files
> @php think service:discover
Succeed!
> @php think vendor:publish
Succeed!

测试运行,采用内置web服务器运行测试,默认8000端口

PS I:\src\tp6\tp6> php think version
v6.0.7
PS I:\src\tp6\tp6> php think run

目录结构

相对于5.1来说,6.0版本目录结构的主要变化是核心框架纳入vendor目录,然后原来的application目录变成app目录。

  • 默认单应用模式

  • 多应用模式

    需要安装模式拓展

    composer require topthink/think-multi-app
    
    www  
    ├─app           应用目录
    │  ├─app_name           应用目录
    │  │  ├─common.php      函数文件
    │  │  ├─controller      控制器目录
    │  │  ├─model           模型目录
    │  │  ├─view            视图目录
    │  │  ├─config          配置目录
    │  │  ├─route           路由目录
    │  │  └─ ...            更多类库目录
    │  │
    │  ├─common.php         公共函数文件
    │  └─event.php          事件定义文件
    │
    ├─config                全局配置目录
    │  ├─app.php            应用配置
    │  ├─cache.php          缓存配置
    │  ├─console.php        控制台配置
    │  ├─cookie.php         Cookie配置
    │  ├─database.php       数据库配置
    │  ├─filesystem.php     文件磁盘配置
    │  ├─lang.php           多语言配置
    │  ├─log.php            日志配置
    │  ├─middleware.php     中间件配置
    │  ├─route.php          URL和路由配置
    │  ├─session.php        Session配置
    │  ├─trace.php          Trace配置
    │  └─view.php           视图配置
    │
    ├─public                WEB目录(对外访问目录)
    │  ├─index.php          入口文件
    │  ├─router.php         快速测试文件
    │  └─.htaccess          用于apache的重写
    │
    ├─extend                扩展类库目录
    ├─runtime               应用的运行时目录(可写,可定制)
    ├─vendor                Composer类库目录
    ├─.example.env          环境变量示例文件
    ├─composer.json         composer 定义文件
    ├─LICENSE.txt           授权说明文件
    ├─README.md             README 文件
    ├─think 
    

配置

这里以多应用模式为例:

注意:官方说除了一级配置外,严格区分大小写。显然很容易让人混淆,不如建议自己约定统一采用小写。

  • 全局配置

    app/config/*

    对所有应用有效。

  • 应用配置

    app/app*/config/*

    只对当前应用有效.

  • 环境配置

    官方只强调了怎么配置,没有说环境配置的目的和使用,对于TP6小白,可能不够友好。

    可以在应用的根目录下定义一个特殊的.env环境变量文件,用于在开发过程中模拟环境变量配置(该文件建议在服务器部署的时候忽略),.env文件中的配置参数定义格式采用ini方式,例如:

    APP_DEBUG =  true
    
    配置文件名	     描述
    app.php	        应用配置
    cache.php	    缓存配置
    console.php	    控制台配置
    cookie.php	    Cookie配置
    database.php	数据库配置
    filesystem.php	磁盘配置
    lang.php	    多语言配置
    log.php	        日志配置
    middleware.php	中间件配置
    route.php	    路由和URL配置
    session.php	    Session配置
    trace.php	    页面Trace配置
    view.php	    视图配置
    

概念总览

  • 入口文件

    入口文件位于public目录下面,最常见的入口文件就是index.php,6.0支持多应用多入口,你可以给每个应用增加入口文件,例如给后台应用单独设置的一个入口文件admin.php。

    除了应用入口文件外,系统还提供了一个控制台入口文件,位于项目根目录的think。

  • 应用

    如果开启多应用的话,每个应用是一个app目录的子目录。

  • 容器

    tp也引入了容器、依赖注入等思想。容器可以用于注入其他业务类实例,并管理这些实例。方便统一管理实例,避免实例散乱,避免业务逻辑高度耦合。

  • 系统服务

  • 路由

    路由,是用于规划(一般同时也会进行简化)请求的访问地址,在访问地址和实际操作方法之间建立一个路由规则 => 路由地址的映射关系。

    路由牺牲性能及url简便解析,来避免url规律的暴露,还能实现url的单独控制,比如生效条件、验证、权限、参数绑定、响应设置等特殊处理。

    路由分组功能可以实现类似多应用的灵活机制。

  • 控制器

    mvc可以看做一种设计模式:命令模式。

    用户发出请求,控制器接收请求,并负责发出获取数据命令;模型处理命令,返回数据;控制器发出渲染数据命令;视图层进行渲染输出。

  • 模型

    模型类通常完成实际的业务逻辑和数据封装,并返回和格式无关的数据。

    模型类并不一定要访问数据库,而且在ThinkPHP的架构设计中,只有进行实际的数据库查询操作的时候,才会进行数据库的连接,是真正的惰性连接。

    ThinkPHP的模型层支持多层设计,你可以对模型层进行更细化的设计和分工,例如把模型层分为逻辑层/服务层/事件层等等。

  • 视图

  • 驱动

    tp6以后的驱动通过composer进行安装

  • 中间件

    中间件主要用于拦截或过滤应用的HTTP请求,并进行必要的业务处理。比如官方session功能、请求缓存、多语言功能就是通过中间件实现。

  • 事件

    tp6提出了事件,事件实际上就是之前的行为和hook钩子机制。回调在以前可以通过钩子实现,现在就叫事件。

  • 助手函数

    助手函数有利于IDE的语法提醒,助手函数大多还是调用的类等

请求流程

流程环节多,但官方说效率甚至比5.1更高

也看出确实采用了更多的成熟的设计模式和技巧,更好的TP将会更靠近laravel,毕竟框架设计模式及开发方法都有一套成熟的理念

官方文档:请求流程 · ThinkPHP6.0完全开发手册 · 看云

由于设计模式的运用,现代PHP框架的请求流程基本越来越相似,TP6和Laravel框架的流程越来越相似。

多应用模式

如果要使用多应用模式,你需要安装多应用模式扩展think-multi-app:

PS I:\src\tp6\tp> composer require topthink/think-multi-app
Using version ^1.0 for topthink/think-multi-app
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 1 install, 0 updates, 0 removals
- Installing topthink/think-multi-app (v1.0.11): Downloading (100%)
Writing lock file
Generating autoload files
> @php think service:discover
Succeed!
> @php think vendor:publish
File I:\src\tp6\tp\config\trace.php exist!
Succeed!
  • 多应用智能识别URL对应的应用,找不到则切换到单应用模式,否则检查全局路由。

  • 允许为每个应用增加应用入口文件(public/index.php),比如admin应用使用admin.php入口文件

  • 应用映射,支持应用的别名映射,映射支持泛解析

  • 支持使用composer加载应用,设置虽然相对复杂

    应用支持使用composer包,这个时候目录可能是composer包的类库所在目录。

  • 支持域名绑定应用

  • 支持禁止应用访问

几年前也许我们会更愿意使用多应用模式,如今webservice及微服务架构的成熟,我们觉得默认单应用模式可能会更好,况且还可以通过路由分组达到多应用的URL效果。

当然在一些初创团队或小团队,多应用模式可能是一种比较低成本的方式。

官方文档:多应用模式 · ThinkPHP6.0完全开发手册 · 看云

tp6的多应用模式现在很多吐槽点:

番茄小沙拉:

这种多应用模式有啥用呢,一般应用都是分开部署在不同的机器上,你这都在app目录下,难道我部署应用一的时候还要连着应用二的代码一起部署上去吗,
正常操作应该不需要外面那层app目录,类似于yii2那种front和backend,你这和laravel的多应用一样,啥console,多应用都放在一个app目录下,
我们实际开发都是console单独的服务器部署,多应用都是分别不同的服务器部署,多个应用还有脚本都混在一起部署那是小公司做的事。

freate@freate

@番茄小沙拉" 那如果是这种分布式部署的话,还需要使用多应用模式吗?创建多个 tp 工程,
每个都使用单应用模式,每台机器上放一个应用,应用之间通信不就行了?

URL访问

  • pathinfo的支持

    http://serverName/index.php/应用/控制器/操作/参数/值...
    
  • apache、iis、nginx 重写支持单一入口

    对于不支持pathinfo的服务器,可以使用兼容模式:

    http://serverName/index.php?s=/控制器/操作/[参数名/参数值...]
    

    通过兼容模式,可以对url进行重写,实现单入口,比如nginx:

    location / { // …..省略部分代码
    if (!-e $request_filename) {
            rewrite  ^(.*)$  /index.php?s=/$1  last;
        }
    }
    

容器和依赖注入

部分开发者对于官方文档中的容器和依赖注入难以消化,一方面两个概念过于术语化,不好理解,文档范例也不太接地气。

首先我们要弄明白依赖注入是什么,为什么需要依赖注入,可以看看:03-依赖倒置原则 - 9ong

如果还不是很清楚,可以看下:

IOC/DI通俗解释

控制反转与依赖注入的理解

容器

ThinkPHP开始引入的容器,到底是什么意思?

官方是这么说的:ThinkPHP使用容器来更方便的管理类依赖及运行依赖注入。容器和依赖注入 · ThinkPHP6.0完全开发手册 · 看云

通俗点说就是:tp6实现了个容器类,这个类专门管理依赖注入的实例对象,比如框架内置绑定到容器中的类有:

内置类库 容器绑定标识
think\Cache cache
think\Config config
think\Cookie cookie
think\Request request
think\Response response
think\Filesystem filesystem
think\Route route
think\Session session
- -

当然我们也可以将自定义类注入到容器中,供业务逻辑类使用,举个例子:

class A(){

    private $b;
    public function __construct(Request $request, B $b){
        $this->b = $b;
    }
}

class B(){
    //...
}

//没有使用依赖注入之前,我们需要手动实例化B对象,并传参给类A,进行实例化:
$a = new A(new think\Request(), new B());

//如果使用tp6的依赖注入,通过助手函数invoke:
$a = invoke('A');//框架会通过依赖注入,将实例化B,并注册到框架容器中,省去了手动实例化,及可以在容器类中统一管理这些注入对象

依赖注入的类统一由容器进行管理,大多数情况下是在自动绑定并且实例化的。

绑定与解析类库

除了调用时自动注入容器外,我们还可以手动绑定类库到容器中。

可以对已有的类库绑定一个标识(唯一),便于快速调用。

// 绑定类库标识
$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);

//使用助手函数app解析使用容器中的类或对象或函数等
$cache = app('cache');

批量绑定:

在实际应用开发过程,不需要手动绑定,我们只需要在app目录下面定义provider.php文件(只能在全局定义,不支持应用单独定义),系统会自动批量绑定类库到容器中。

return [
    'route'      => \think\Route::class,
    'session'    => \think\Session::class,
    'url'        => \think\Url::class,
];

注意:绑定标识主要是为了方便记住与快速使用,我们也可以解析一个未绑定的类,只是没有简短的标识,需要类完整命名空间路径:

$arrayItem = app('org\utils\ArrayItem');

依赖注入场景

了解了依赖注入后,再去看TP官方关于依赖注入的使用

依赖注入的对象参数支持多个,且与顺序无关

以下场景支持依赖注入:

  • 控制器构造方法
  • 控制器操作方法
  • 路由的闭包定义
  • 事件类的执行方法
  • 中间件的执行方法等

服务

官方文档更多的是说服务定义、服务注册、启动,没有更多关于服务的使用和目的。

文档碎片化较多,小白很难理解。(好的开发者文档可以参考微信团队的文档,都会说明为什么做,理论怎么做,范例怎么做,最佳实践是怎么样的等)

为什么要注册系统服务,也就是将服务绑定到容器中?

服务更多停留在框架内置服务,后续有时间展开。

门面Facade

官方:门面为容器中的(动态)类提供了一个静态调用接口,相比于传统的静态方法调用, 带来了更好的可测试性和扩展性,你可以为任何的非静态类库定义一个facade类。

看完官方描述,我们都有个疑问门面的应用场景是什么,为什么要用门面?可测试性和扩展性优势在哪里?

依赖注入和门面的使用方式不同,但可以达到同一个效果,当然在编码方式也会有一些差异,这就是框架给小白带来的额外学习负担和烦恼:

依赖注入:


namespace app\index\controller;

use think\Request;

class Index
{
    public function index(Request $request)
    {
        echo $request->controller();
    }
}

facade门面:


namespace app\index\controller;

use think\facade\Request;

class Index
{
    public function index()
    {
        echo Request::controller();
    }
}

两段代码效果一样,但写法不一样

依赖注入的优势是支持接口的注入,而Facade则无法完成。说的直白一点,Facade功能可以让类无需实例化而直接进行静态方式调用

前面框架内置绑定到容器的类,大都定义了对应的Facade类。比如think\Cache、think\Request等等。

注意:think\facade\Response并没有实现facade模式。

小结:

真正实践中,很少会这么用,而且外观模式的设计模式,用在框架上,使得代码更加简短,对于IDE的语法自动补充不是很友好,需要专门的tp6的ide-helper,像swoole有自己的helper,有了IDE helper对于编码会更友好,编码效率会更高。

实际上官方在第一次安装think framework时,也安装了think-helper组件(vendor/topthink/think-helper)

Facade设计模式,一方面为了方便自己扩展一些类,而不去修改原始/框架内置类;二来使得框架核心更方便的无缝升级。不要随便吐槽人家辛辛苦苦设计出来的东西,存在即有其考虑的缘由。

中间件

中间件 · ThinkPHP6.0完全开发手册 · 看云

  • 定义中间件

    PS I:\src\tp6\tp> php .\think make:middleware Check
    Middleware:app\middleware\Check created successfully.
    
    PS I:\src\tp6\tp\app> ls
    
    
        目录: I:\src\tp6\tp\app
    
    
    Mode                LastWriteTime         Length Name
    ----                -------------         ------ ----
    d-----       2019/11/27     11:58                index
    d-----       2019/11/27     15:59                middleware
    -a----       2019/11/27     10:30             13 .htaccess
    -a----       2019/11/27     10:30           2086 BaseController.php
    -a----       2019/11/27     10:30             28 common.php
    -a----       2019/11/27     10:30            256 event.php
    -a----       2019/11/27     10:30           1398 ExceptionHandle.php
    -a----       2019/11/27     10:30            263 middleware.php
    -a----       2019/11/27     10:30            195 provider.php
    -a----       2019/11/27     10:30             89 Request.php    
    

    生成middleware/Check.php:

    declare (strict_types = 1);
    
    namespace app\middleware;
    
    class Check
    {
        /**
        * 处理请求
        *
        * @param \think\Request $request
        * @param \Closure       $next
        * @return Response
        */
        public function handle($request, \Closure $next)
        {
            //前置行为 do something before controller   
            common_debug_log(__METHOD__."=>before controller",true);
            $request->myname = "tsingchan";//这里修改request对象,在回到控制器中,就可以使用最新的request对象
              
            $response = $next($request);//回到控制器 ,之后对当前请求的一些环境变量的调整都不再影响请求结果
              
            //后置行为 do something after controller
            common_debug_log(__METHOD__."=>after controller",true);//这里可以做一些请求结束的一些后续操作,比如异步或日志等
            return $response;
        }
          
        public function end(\think\Response $response)
        {
            //在end方法里面不能有任何的响应输出。因为end回调触发的时候请求响应输出已经完成了。
            //结束调度
            common_debug_log(__METHOD__,true);
        }
    }
      
    
    

    测试输出:

    
    ##2021-03-20 23:08:39
    app\middleware\Check::handle=>before controller
    ##2021-03-20 23:08:39
    app\controller\Index::hello2
    ##2021-03-20 23:08:39
    app\middleware\Check::handle=>after controller
    ##2021-03-20 23:08:39
    app\middleware\Check::end
    
    
  • 配置中间件分组、别名与优先级

    有了中间件,我们还可以进一步对中间件进行别名与分组、优先级配置。

    这步骤在config目录下的middleware.php文件中进行配置。

    // 中间件配置
    return [
        // 别名或分组
        'alias'    => [
            "mid-check"=> app\middleware\Check::class,
        ],
        // 优先级设置,此数组中的中间件会按照数组中的顺序优先执行
        'priority' => [
            think\middleware\SessionInit::class,
            app\middleware\Check::class,        
        ],
    ];
    
  • 注册中间件

    4组中间件按执行顺序:全局中间件、应用中间件、路由中间件、控制器中间件

    • 全局中间件

      在app目录下面middleware.php文件中定义,使用下面的方式:

      return [
          \app\middleware\Check::class,        
      ];
      
      

      注册中间后,则所有控制器都会经过全局中间件Check的处理。

    • 应用中间件

      对应在应用目录下的middleware.php中定义。只有多应用模式下才有。

    • 路由中间件

      这里我们重点看下路由中间件。路由中间件是我们最常用的中间件。

      路由中间件是在路由文件直接注册的:

      //支持多个中间件
      Route::rule('hello/:name','hello')
      ->middleware([\app\middleware\Auth::class, "check"]);
      
      //支持对路由分组注册中间件
      Route::group('hello', function(){
          Route::rule('hello/:name','hello');
      })->middleware('auth');
      
      //还有多种方式,包括传参,具体可以参考官方文档
      

      路由中间件,可以满足我们对指定请求(具体某个或一个分组等请求)进行扩展处理。

    • 闭包中间件

      不定义中间件类,也可以使用中间件,我们可以在注册中间件时,传递一个闭包作为中间件。

      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);
      });
      
    • 控制器中间件

      虽然中间件都是针对控制器的前后置行为,但我们也可以直接在控制器上指定注册某个中间件。

      namespace app\controller;
      
      class Index
      {
          protected $middleware = ['check'];
      
          public function index()
          {
              return 'index';
          }
      
          public function hello()
          {
              return 'hello';
          }
      }
      
  • 框架内置中间件

    中间件类 描述
    think\middleware\AllowCrossDomain 跨域请求支持
    think\middleware\CheckRequestCache 请求缓存
    think\middleware\LoadLangPack 多语言加载
    think\middleware\SessionInit Session初始化
    think\middleware\FormTokenCheck 表单令牌

事件

事件,可以说是之前的行为及钩子的升级版,事件也比中间件在粒度上会更细,事件的目的也一样,解耦观察者和被观察者,主要是为了方便系统的易拓展、易维护。

事件与中间件目的是一致的,解耦系统,增强扩展性。但事件比起中间件粒度更小,也就是触发更精准,中间件最小的控制粒度是一个action(比如路由中间件),而事件可以精确到action中某个逻辑代码前后。

tp6事件使用了观察者模式。官方文档没有具体使用场景范例,未有事件定义、事件绑定、事件监听、事件订阅这些概念的通俗理解,特别是既然从观察者模式出发,可以结合观察者模式,讲解下对应的订阅者角色、事件、事件触发者、事件触发动作,这样能让更多对于观察者模式理解不够深刻的同学可以快速理解。

之前看过有人用订阅视频的方式讲解了观察者模式:订阅者(很多腾讯视频用户)、订阅什么事件(庆余年更新、王牌对王牌更新等多个事件-subscribe)、事件触发者(腾讯视频上新-publish)、订阅者做什么(视频用户收藏、马上看、喜欢、查看评论等等-update)

三个对象:订阅者、发布者、工具(监听发布者、通知发布者)

观察者模式也可以参考这个简单模式介绍:23-观察者模式 - 9ong

  • 定义事件

    首先我们要知道,tp的事件系统不依赖事件类,如果没有额外的需求,仅通过事件标识也可以使用,省去定义事件类的麻烦。

    也就是说只要想触发哪个类的哪个方法,都可以认为是触发了事件,并不一定要特意去定义一个事件类,才能触发一个事件类的方法操作。

    当然我们还要看正常的事件类是如何定义的:

    php think make:event UserLogin
    

    完善事件类UserLogin代码:

    declare (strict_types = 1);
    
    namespace app\event;
    
    class UserLogin
    {
        private $user = [];//为了测试方便,这里以数组代替User对象
        public function __construct(Array $user)
        {
            $this->user = $user;
            common_debug_log($user,true);
        }
    }
    
  • 事件绑定

    事件绑定,就是为事件绑定一个别名。

    绑定事件别名,不同于配置中间件分组、别名及优先顺序,事件别名一般在应用目录下的event.php文件中定义。而中间件的别名配置需要在对应的config目录下middleware.php配置。

    // 事件定义文件
    return [
        'bind'      => [
            "UserLogin"=> \app\event\UserLogin::class,//绑定事件别名
        ],
    
        'listen'    => [
            'AppInit'  => [],
            'HttpRun'  => [],
            'HttpEnd'  => [],
            'LogLevel' => [],
            'LogWrite' => [],
        ],
    
        'subscribe' => [
        ],
    ];
    
    

    当然我们还可以动态的去绑定别名:

    Event::bind(['UserLogin' => \app\event\UserLogin::class]);
    

    再次强调:tp的事件系统不依赖事件类,如果没有额外的需求,仅通过事件标识也可以使用,省去定义事件类的麻烦。

    官方也说了:如果你没有定义事件类的话,则无需绑定。对于大部分的场景,可能确实不需要定义事件类。

  • 事件监听

    前面的事件定义与事件绑定在实践中,是很少使用的,更多的还是通过事件监听与事件订阅来实现事件逻辑体系。

    创建一个用户登录事件监听器UserLogin,用来监听事件触发UserLogin(我们需要在event.php中定义事件别名UserLogin及对应事件监听器,后面会介绍),并通知订阅者(遍历subscribe订阅者,订阅者执行onUserLogin方法,后面会介绍)

    php think make:listener UserLogin
    

    事件监听器类:\app\listener\UserLogin

    declare (strict_types = 1);
    
    namespace app\listener;
    
    class UserLogin
    {
        /**
        * 事件监听处理
        *
        * @return mixed
        */
        public function handle($event)
        {
            common_debug_log(__METHOD__,true);
        }
    }
    
  • 事件订阅

    创建两个订阅者:User与TsingChan

    php think make:subscribe User
    php think make:subscribe TsingChan
    

    订阅者User订阅了UserLogin和UserLogout两个事件:

    namespace app\subscribe;
    
    class User
    {
        /**
        * 监听事件的方法命名规范是on+事件标识(驼峰命名)
        * 
        * @param type $user
        */
        public function onUserLogin($user)
        {
            // UserLogin事件响应处理
            common_debug_log(__METHOD__,true);
            common_debug_log($user,true);
        }
    
        public function onUserLogout($user)
        {
            // UserLogout事件响应处理
            common_debug_log(__METHOD__,true);
        }    
    }
    

    订阅者TsingChan订阅了UserLogin一个事件:

    namespace app\subscribe;
    
    class TsingChan
    {
        /**
        * 监听事件的方法命名规范是on+事件标识(驼峰命名)
        * 
        * @param type $user
        */
        public function onUserLogin($user)
        {
            // UserLogin事件响应处理
            common_debug_log(__METHOD__,true);
            common_debug_log($user,true);
        }
    }
    
  • 事件配置

    定义了事件监听器类和事件订阅者类后,我们可以在app/event.php配置事件监听器与订阅,配置的意思就是:启动了哪些事件监听器,哪些订阅者参与订阅了:

    // 事件定义文件
    return [
        'bind'      => [    
        ],
    
        'listen'    => [            
            "UserLogin"=>[app\listener\UserLogin::class],
        ],
    
        'subscribe' => [
            app\subscribe\User::class,
            \app\subscribe\TsingChan::class,
        ],
    ];
    
    
  • 事件触发

    namespace app\controller;
    
    use app\BaseController;
    
    class Index extends BaseController
    {
                  
        public function login()
        {
            //接受验证用户请求数据等操作
            common_debug_log(__METHOD__,true);
    
            //触发用户登录事件,也就是说发生了一个UserLogin事件,通知事件监听器app\listener\UserLogin发生了一个UserLogin事件,
            //框架根据事件event.php配置监听与订阅关系,遍历所有订阅者,执行订阅者对这个事件的响应方法,比如订阅者\app\Subscribe\User::onUserLogin()
            //观察者模式,做到了将事件触发者与订阅者解耦
            //事件监听器与事件订阅者是一个多对多的关系
            event("UserLogin", ['name'=>"9ong.com","sex"=>1]);
              
            return __METHOD__.PHP_EOL;
        }
    }
    
    

    测试输出:

    ##2021-03-21 14:48:53
    app\middleware\Check::handle=>before controller
    ##2021-03-21 14:48:53
    app\controller\Index::login
    ##2021-03-21 14:48:53
    app\listener\UserLogin::handle
    ##2021-03-21 14:48:53
    app\subscribe\User::onUserLogin
    ##2021-03-21 14:48:53
    Array
    (
        [name] => 9ong.com
        [sex] => 1
    )
    
    ##2021-03-21 14:48:53
    app\subscribe\TsingChan::onUserLogin
    ##2021-03-21 14:48:53
    Array
    (
        [name] => 9ong.com
        [sex] => 1
    )
    
    ##2021-03-21 14:48:53
    app\middleware\Check::handle=>after controller
    ##2021-03-21 14:48:53
    app\middleware\Check::end
    
    
  • 事件逻辑解释图

    https://www.huodongxing.com/file/ue/20150615/11D882E32C07F2FFC12332D0382A4AC43B/30734097652827212.jpg

路由

  • 路由的好处

    • 让URL更规范以及优雅;
    • 隐式传入额外请求参数;
    • 统一拦截并进行权限检查等操作;
    • 绑定请求数据;
    • 使用请求缓存;
    • 路由中间件支持;
  • 解析过程

    • 路由定义
    • 路由检测
    • 路由解析
    • 路由调度

    作为开发者只需要关注路由的定义与配置即可。

  • 路由不好的地方

    • 需要在开发前对路由进行主体规划和定义
    • 属于特殊规则,相对于默认的pathinfo规则来说比较复杂
  • 相关文件与配置

    • 路由配置文件:config/route.php
    • 路由定义文件:route/app.php
    • 可以在config/app.php中配置write_route关闭路由,如果是应用目录下的config/app.php将只关闭当下应用的路由功能
  • 路由定义

    路由是针对应用的,每个应用的路由是完全独立。也就是说设置路由后的访问URL地址还需要带上应用名,除非是单应用。

    route目录下的任何路由定义文件都是有效的,默认的路由定义文件是app.php,而且我们可以改名,还可以添加多个路由定义文件。比如我们需要在多控制器和操作且多人密集协作,为了避免冲突时,可以考虑多路由文件定义。

    • 路由注册

      路由的定义,本质上由三部分组成:路由表达式、路由地址、请求类型:

      Route::rule('路由表达式', '路由地址', '请求类型');
      
    • 路由定义快捷方法get/post/put/delete/patch

      Route::快捷方法名('路由表达式', '路由地址');
      Route::get('new/<id>','News/read'); // 定义GET请求路由规则
      Route::post('new/<id>','News/update'); // 定义POST请求路由规则
      Route::put('new/:id','News/update'); // 定义PUT请求路由规则
      Route::delete('new/:id','News/delete'); // 定义DELETE请求路由规则
      Route::any('new/:id','News/read'); // 所有请求都支持的路由规则
      
    • 按照定义顺序匹配路由规则

    • 静态地址与动态地址路由规则

      Route::rule('/', 'index'); // 首页访问路由
      Route::rule('my', 'Member/myinfo'); // 静态地址路由
      Route::rule('blog/:id', 'Blog/read'); // 静态地址和动态地址结合
      Route::rule('new/:year/:month/:day', 'News/read'); // 静态地址和动态地址结合
      Route::rule(':user/:blog_id', 'Blog/read'); // 全动态地址
      
    • 根据路由生成URL地址

      不建议使用路由标识,不要为了标识而标识,有点鸡肋。

      // 注册路由到News控制器的read操作
      Route::rule('new/:id','News/read')
          ->name('new_read');//不建议给标识别名
      
      url('new_read', ['id' => 10]);//不建议用,只有自己才知道是什么,不便于管理
      url('News/read', ['id' => 10]);//这不是挺好,看代码就知道是什么
      
    • 额外传参

      Route::get('blog/:id','blog/read')
      ->append(['status' => 1, 'app_id' =>5]);
      
  • 变量规则

    这里说的变量规则,是路由表达式中的变量规则。

    • 路由规则变量有限制

      用来约束动态路由变量:

      // 默认的路由变量规则 只会匹配字母、数字、中文和下划线字符,并不会匹配特殊符号以及其它字符
      'default_route_pattern' => '[\w\.]+',
      
      // 支持批量添加
      Route::pattern([
          'name' => '\w+',
          'id'   => '\d+',
      ]);
      
      // 定义GET请求路由规则 并设置name变量规则
      Route::get('new/:name', 'News/read')
          ->pattern(['name' => '[\w|\-]+']);
      
    • 动态路由

      可以把路由规则中的变量传入路由地址中,就可以实现一个动态路由,例如:

      // 定义动态路由
      Route::get('hello/:name', 'index/:name/hello');
      

      name变量的值作为路由地址传入,可以达到路由根据变量name的不同而路由到不同的地址上。

  • 路由地址

    • 路由支持多级控制器

      Route::get('blog/:id','group.Blog/read');
      //表示路由到下面的控制器类,index/controller/group/Blog
      
    • 路由支持到类解析

      \完整类名@方法名
      

      或者(静态方法)

      \完整类名::方法名
      
      Route::get('blog/:id','\app\index\service\Blog@read');
      
    • 路由支持302重定向

      Route::redirect('blog/:id', 'http://blog.thinkphp.cn/read/:id', 302);
      
    • 路由支持直接到view模板

      // 路由到模板文件
      Route::view('hello/:name', 'index/hello', ['city'=>'shanghai']);
      //表示该路由会渲染当前应用下面的view/index/hello.html模板文件输出。
      //在模板中可以输出name和city两个变量。{$name}--{$city}!
      
    • 路由支持直接输出响应(response)

    • 路由支持到闭包(当场响应请求)

      Route::get('hello', function () {
          return 'hello,world!';
      });
      
      //自定义输出
      Route::get('hello/:name', function () {
          response()->data('Hello,ThinkPHP')
          ->code(200)
          ->contentType('text/plain');
      });
      
      //传递参数
      Route::get('hello/:name', function ($name) {
          return 'Hello,' . $name;
      });
      
      //依赖注入
      Route::rule('hello/:name', function (Request $request, $name) {
          $method = $request->method();
          return '[' . $method . '] Hello,' . $name;
      });
      
  • 路由参数

    路由参数提供对url的检查及二次处理。

    Route::get('new/:id', 'News/read')
        ->ext('html')
        ->https();
    

    更多参数详细参考官方文档:路由参数 · ThinkPHP6.0完全开发手册 · 看云

  • 路由中间件

    在中间件部分,已经着重介绍过路由中间件。

    更多参考:路由中间件 · ThinkPHP6.0完全开发手册 · 看云

  • 路由分组

    路由分组功能允许把相同前缀的路由定义合并分组,这样可以简化路由定义,并且提高路由匹配的效率,不必每次都去遍历完整的路由规则(尤其是开启了路由延迟解析后性能更佳)

    Route::group('blog', function () {
        Route::rule(':id', 'blog/read');
        Route::rule(':name', 'blog/read');
    })->ext('html')->pattern(['id' => '\d+', 'name' => '\w+']);
    
    //如果仅仅是用于对一些路由规则设置一些公共的路由参数(也称之为虚拟分组),也可以使用:
    Route::group(function () {
        Route::rule('blog/:id', 'blog/read');
        Route::rule('blog/:name', 'blog/read');
    })->ext('html')->pattern(['id' => '\d+', 'name' => '\w+']);
    
    //路由prefix方法,prefix将自动补充到路由地址上blog/read blog/update
    Route::group('blog', function () {
        Route::get(':id', 'read');
        Route::post(':id', 'update');
        Route::delete(':id', 'delete');
    })->prefix('blog/')->ext('html')->pattern(['id' => '\d+']);
    

    关于延迟路由解析与路由规则合并解析,官方说在分组和域名路由情况下,可以提升性能,但为什么框架不自动处理了?而是默认关闭,让开发者自己开启?是有其他什么负面影响?文档里没有看到更多的描述,需要深入源码了解。

  • 资源路由

    支持设置RESTFul请求的资源路由,方式如下:

    Route::resource('blog', 'Blog');
    

    表示注册了一个名称为blog的资源路由到Blog控制器,系统会自动注册7个路由规则,如下:

    标识 请求类型 生成路由规则 对应操作方法(默认)
    index GET blog index
    create GET blog/create create
    save POST blog save
    read GET blog/:id read
    edit GET blog/:id/edit edit
    update PUT blog/:id update
    delete DELETE blog/:id delete

    比如:

    http://serverName/blog/,访问Blog类的index方法
    http://serverName/blog/128,访问Blog类的read方法
    http://serverName/blog/28/edit,访问Blog类的edit方法
    
    当然要注意:这三各都是GET类型,save、update、delete对方的类型不是GET
    
  • 注解路由

    说到注解,引用PHP手册里熟悉又陌生的知识点 - 9ong中的注解小节:

    php8刚引入注解,在php8之前symfony、laravel、hyperf等框架都通过反射实现了所谓的“注解”,满足AOP编程。

    在官方注解未出来之前,大部分框架的注解是通过反射实现,先把类或函数的注释取到,用语法解析或者正则之类的方式匹配注解符号,假装是注解,然后处理“注解”。

    官方注解的符号也一直在变:从@ 到 «» 到 @@ 再到 #[]

    很需要更多的应用场景来深入理解php的注解,注解目前更多用于配置去中心化。

    虽然配置去中心化看起来挺美的,但在方法的注释中使用@Route关键词定义路由规则,目前应该会存在一些意想不到的问题,不大建议用于生产环境。

  • 路由绑定

  • 域名路由

    支持完整域名、子域名和IP部署的路由和绑定功能,同时还可以起到简化URL的作用。

    • 解析路由规则

      可以单独给域名设置路由规则,例如给blog和admin子域名注册单独的路由规则:

      Route::domain(['blog', 'admin'], function () {
          // 动态注册域名的路由规则
          Route::rule('new/:id', 'news/read');
          Route::rule(':user', 'user/info');
      });
      
    • 支持域名绑定到路由

      支持绑定的控制器、命名空间、类,甚至response对象。

      域名路由 · ThinkPHP6.0完全开发手册 · 看云

  • MISS路由

    注意设置MISS路由意味着开启强制路由

    // 只有GET请求下MISS路由有效
    Route::miss('public/miss', 'get');
    
    //或者闭包
    Route::miss(function() {
        return '404 Not Found!';
    });
    
    //也支持分组里或域名路由,在没有匹配到路由规则时,采用miss路由
    Route::group('blog', function () {
        Route::rule(':id', 'blog/read');
        Route::rule(':name', 'blog/read');
        Route::miss('blog/miss');
    })
    
  • 支持跨域请求

    还有待进一步完善,期待更多实践,暴露这方面的问题,官方及时完善修复。

  • URl生成规则

    注意:如果开启了路由延迟解析,需要生成路由映射缓存才能支持全部的路由地址的反转解析。脑壳疼。

    • 建议使用路由地址
    • 注意URL后缀的配置url_html_suffix
    • 补充域名生成,支持自动当前域名,支持指定域名
    • 支持生成锚点
    • 当然tp6依然会有助手函数url()来生成url地址

    除了

    Route::buildUrl('index/blog/read', ['id'=>5])->domain('blog.thinkphp.cn');
    

    也支持助手函数url()

    url('index/blog/read', ['id'=>5])
        ->suffix('html')
        ->domain(true)
        ->root('/index.php');
    

    URL生成 · ThinkPHP6.0完全开发手册 · 看云

  • 吐槽

    我们觉得路由规则不需要搞的这么复杂,在有中间件,事件这些机制的情况下,路由可以简化,也不需要那么多别名标识之类的东西,这些本身不是路由的重点,路由功能多到,不知道哪些是重点,让开发者容易混淆,甚至有些过于灵活而不方便维护。什么都有个度,过了度也不好。比如勇敢是有一个度量的,胆子太小,我们说懦弱,胆子过大,我们说鲁莽。

控制器

  • 仍然支持多级控制器,但不建议使用(除非逼不得已),多级控制器在URL上存在一些不好处理的问题,需要通过路由协助访问多级控制器
  • 不建议使用php原生的die及exit,避免中断,不利于后续的单元测试、框架流程还有其他第三方组件比如swoole
  • 支持错误时默认控制器Error类
  • 支持资源控制器(RESTFul),配合资源路由
  • 支持中间件

请求Request对象

  • 使用request对象

    • 通常使用依赖注入,不需要单独实例化,可以是类构造器方法注入,也可以是每个操作方法注入
    • 通过Facade\Request静态调用
    • 通过助手函数request()调用
  • 请求信息

    • 支持获取当前请求信息,如http协议上的相关信息

      // 获取完整URL地址 不带域名
      Request::url();
      // 获取完整URL地址 包含域名
      Request::url(true);
      
      

      获取更多请求信息参考官方文档:请求信息 · ThinkPHP6.0完全开发手册 · 看云

    • 支持获取当前访问控制器及操作

      Request::controller();//获取controller驼峰形式
      Request::action(true);//获取小写的action
      
  • 输入变量

    • 支持检测变量是否设置

      Request::has('name','post');
      
    • 变量获取

      支持get、post、param、put、session、cookie、request、server、env、file、route等方法,用于获取不同渠道的输入变量。详细参考:输入变量 · ThinkPHP6.0完全开发手册 · 看云

      这些方法表达式:

      变量类型方法('变量名/变量修饰符','默认值','过滤方法')
      
      // 获取当前请求的name变量
      Request::param('name');
      // 获取当前请求的所有变量(经过过滤)
      Request::param();
      // 获取当前请求未经过滤的所有变量
      Request::param(false);
      Request::get('name','default'); // 返回值为default
      Request::only(["name","id"]);//只要name 和id两个字段的变量,这个省去了我们自己封装的arrayCut相关方法
      
    • 支持变量修饰符

      修饰符 作用
      s 强制转换为字符串类型
      d 强制转换为整型类型
      b 强制转换为布尔类型
      a 强制转换为数组类型
      f 强制转换为浮点类型
      Request::get('id/d');
      Request::post('name/s');
      Request::post('ids/a');
      
    • 支持中间件设置和获取变量

      前面中间件章节已经有介绍过。

    • 输入的助手函数input

      request对象有的,助手函数input也都有。

      //判断变量是否定义
      input('?get.id');
      input('?post.name');
      
      //获取PARAM参数
      input('param.name'); // 获取单个参数
      input('param.'); // 获取全部参数
      // 下面是等效的
      input('name'); 
      input('');
      
      //获取GET参数
      // 获取单个变量
      input('get.id');
      // 使用过滤方法获取 默认为空字符串
      input('get.name');
      // 获取全部变量
      input('get.');
      
      //使用过滤方法
      input('get.name','','htmlspecialchars'); // 获取get变量 并用htmlspecialchars函数过滤
      input('username','','strip_tags'); // 获取param变量 并用strip_tags函数过滤
      input('post.name','','org\Filter::safeHtml'); // 获取post变量 并用org\Filter类的safeHtml方法过滤
            
      //使用变量修饰符
      input('get.id/d');
      input('post.name/s');
      input('post.ids/a');
      
  • 支持请求类型获取

    常用的方法有:

    • isAjax
    • isPost
    • isGet
    • isCli 是否cli执行
    • isCgi 是否cgi模式
    • isMobile 是否手机访问

    请求类型 · ThinkPHP6.0完全开发手册 · 看云

  • http头信息

    关于php获取当前请求http头信息,php没有提供原生函数,除了只支持apache服务器的函数apache_request_headers()可以获取当前请求的所有请求头信息 。

    tp6提供了Request::header()方法获取当前请求的HTTP请求头信息.

    header()方法默认返回http头信息数组,还支持返回指定头信息:

    $agent = Request::header('user-agent');
    
  • 支持伪静态

    URL伪静态通常是为了满足更好的SEO效果,ThinkPHP支持伪静态URL设置,可以通过设置url_html_suffix参数随意在URL的最后增加你想要的静态后缀,而不会影响当前操作的正常执行。

  • 支持请求缓存,只对get类型请求有效

    // 定义GET请求路由规则 并设置3600秒的缓存
    Route::get('new/:id','News/read')->cache(3600);
    

    请求缓存支持全局设置,但注意如果存在部分get请求需要实时的数据时,要额外排除,比如验证码等页面

    // 定义GET请求路由规则 并关闭请求缓存(即使开启了全局请求缓存)
    Route::get('new/:id','News/read')->cache(false);
    

    请求缓存 · ThinkPHP6.0完全开发手册 · 看云

响应Response

Response类不能直接实例化,必须使用 Response::make() 静态方式创建,建议直接使用系统提供的助手函数完成。

大多数情况,我们不需要关注Response对象本身,只需要在控制器的操作方法中返回数据即可。

  • 响应输出

    为了规范和清晰起见,最佳的方式是在控制器最后明确输出类型(毕竟一个确定的请求是有明确的响应输出类型),默认支持的输出类型包括:

    输出类型 快捷方法 对应Response类
    HTML输出 response \think\Response
    渲染模板输出 view \think\response\View
    JSON输出 json \think\response\Json
    JSONP输出 jsonp \think\response\Jsonp
    XML输出 xml \think\response\Xml
    页面重定向 redirect \think\response\Redirect
    附件下载 download \think\response\File

    在以往我们经常也会封装好图片资源的输出预览,而不是下载,这方面好像没有

    在V6.0.3+版本,开始支持设置是否强制下载,例如需要打开图像文件而不是浏览器下载的话哈,可以使用:

    public function download()
    {
        return download('image.jpg', 'my.jpg')->force(false);
    }
    
    $data = ['name' => 'thinkphp', 'status' => '1'];
    return json($data);
    
  • 支持设置响应状态码

    json($data,201);
    view($data,401);
    
  • 支持设置响应头信息

    json($data)->code(201)->header([
        'Cache-control' => 'no-cache,must-revalidate'
    ]);
    
  • 支持写入cookie

    response()->cookie('name', 'value', 600);
    
  • 支持额外参数

  • 重定向

    可以使用redirect助手函数进行重定向。

    • 站外重定向,直接给url地址

    • 站内重定向,支持完整path地址,支持url函数生成的最终url地址

      看到重定向这部分文档已经有点无奈了,按文档的范例执行不了,经网友提醒需要加上->send()方法才有效重定向:

      redirect('/index/hello/domain/9ong.com')->send();
      redirect((string) url('domain',['name' => '9ong.com']))->send();
      
    • 记住请求地址

      这功能很适合用在重新登录的场景。访问url1,发现未登录,跳转到login地址,登录成功后,返回到记住的请求地址。

      比如:我们第一次访问index操作的时候会重定向到hello操作并记住当前请求地址,然后操作完成后到restore方法,restore方法则会自动重定向到之前记住的请求地址,完成一次重定向的回归,回到原点!(再次刷新页面又可以继续执行)

      详细例子参考:重定向 · ThinkPHP6.0完全开发手册 · 看云

      官方的例子没能走通,报错:

      时间:2019.11.28

      [0] ArgumentCountError in helper.php line 373
      Too few arguments to function redirect(), 0 passed in I:\src\tp6\tp\app\index\controller\Index.php on line 45 and at least 1 expected
      

      redirect方法第一个参数url没有默认值,我们尝试用空值代替,虽然没有报错了,但redirect("")->restore()没有达到官方说的效果

      没有跳转到remeber的地址,简单看了下代码,应该是session没有生效,暂时没有空调试。

      看到session模块,我们就知道为什么了,大部分人是因为没有开启sessionInit中间件,app/middleware.php,加入sessionInit中间件

      return [
          // 全局请求缓存
          // \think\middleware\CheckRequestCache::class,
          // 多语言加载
          // \think\middleware\LoadLangPack::class,
          // Session初始化
          \think\middleware\SessionInit::class,
          app\middleware\Check::class,
      ];
            
      
  • 文件下载

    文件下载的封装很符合我们的习惯,我们很喜欢,以下都能正确运行在6.0.7的版本上,

    //下载一个文件
    public function download(){
        return download("res/downloadtest.md","1.md");
    }
    
    //直接在浏览器预览图片,而不是浏览器下载,通过force(false)实现,仅在6.0.3以上版本支持
    public function viewimg(){
        return download("res/tsingchan.JPG","t.jpg")->force(false);
    }
    

    更多关于下载方法参数:文件下载 · ThinkPHP6.0完全开发手册 · 看云

数据库

@todo 由于最近时间关系,之后章节不再细补充细节,有时间再补充上。

数据库配置

支持全局配置,支持应用独立配置(覆盖全局),支持群组配置,可灵活切换数据库连接,与我们之前在5.0版本对数据库配置切换连接的改造思想契合(模型类的定义中考虑了不同model自动连接不同数据库),满足我们的多库灵活清晰使用数据库,赞一个

\think\facade\Db::connect('demo')
	->table('user')
    ->find();
  • 配置与配置参考

  • 连接

  • 模型定义

    官方始终强调的:TP的数据库连接是惰性的,所以并不是在实例化的时候就连接数据库,而是在有实际的数据操作的时候才会去连接数据库。

    如果某个模型类里面定义了connection属性的话,则该模型操作的时候会自动按照给定的数据库配置进行连接,而不是配置文件中设置的默认连接信息。这个也满足我们灵活设定义模型的数据库连接,满足一个项目使用多个数据库。

  • 长连接,我们会建议使用mysql的长连接,但长连接会带来内存问题

    详见:mysql实战阅读笔记-基础架构中关于长连接问题解决 - 9ong

    TP6中提供了断线重连的功能,如果你使用的是长连接或者命令行,在超出一定时间后,数据库连接会断开,这个时候你需要开启断线重连才能确保应用不中断。

    在数据库连接配置中设置:

    // 开启断线重连
    'break_reconnect' => true,
    

    但除了数据库断开外,我们在实践过程中会遇到需要我们主动定期断开重连长连接:

    • 定期断开长连接。使用一段时间,或者程序里面判断执行过一个占用内存的大查询后,断开连接,之后要查询再重连。
    • 如果你用的是MySQL 5.7或更新版本,可以在每次执行一个比较大的操作后,通过执行 mysql_reset_connection来重新初始化连接资源。这个过程不需要重连和重新做权限验证,但是会将连接恢复到刚刚创建完时的状态。

分布式与读写分离

分布式与读写分离配置更加清晰、成熟、简洁

  • 分布式与读写分离配置

    详见官方文档:分布式数据库 · ThinkPHP6.0完全开发手册 · 看云

  • 调用查询类或者模型的CURD操作的话,系统会自动判断当前执行的方法是读操作还是写操作并自动连接主从服务器。

  • 对于原生sql的执行,人工判断写操作sql使用exceute,读操作sql使用query,才能保证读写分离的正常,避免主从读写混乱

  • 主从有个弊端就是当写完数据,特别是新增数据后,需要马上再读取数据时,主库还未同步到从库时,出现读取旧数据问题,TP6提供了read_master的配置参数,允许我们在某个表的写操作后,当前请求的后续所有对该表的查询都会从主库读取

  • 可能还有人会担心主从读写分离的方式,在事务、读锁等操作时是否需要自己分清主从?

    其实TP6除了读操作和写操作的方法自动连接对应数据库外,在事务、锁(lock)、连接失败、手动master/readmaster设置都会自动连接主数据库

  • TP6提供的是框架级别的分布式与读写分离,市面上的云主机为了方便开发者可能只会提供一个连接地址,由云系统内部实现分布式负载均衡与读写分离调度

    比如阿里云只会提供一个ip地址连接数据库,阿里云内部实现读写分离调度。

查询构造

还是吐槽下facade的方式,让IDE的自动补全没了,很是难受,后面看看有没有插件帮忙补全,提供开发者的敲代码效率。(6.0.7的think-helper仍然没有自动补全)

2020年仍然还存在不少问题,还需要更多的实际项目的考验,发现tp6更多的问题。

添加数据 · ThinkPHP6.0完全开发手册 · 看云

  • chunk数据分批处理

    如果你需要处理成千上百条数据库记录,可以考虑使用chunk方法,该方法一次获取结果集的一小块,然后填充每一小块数据到要处理的闭包,该方法在编写处理大量数据库记录的时候非常有用。

  • cursor游标查询,利用迭代生成器

    采用了php的生成器(返回迭代器),可以节省大量内存,大大提高查询处理效率

    $cursor = Db::table('user')->where('status', 1)->cursor();
    foreach($cursor as $user){
        echo $user['name'];
    }
    

    cursor方法返回的是一个生成器对象,user变量是数据表的一条数据(数组)。

  • 吐槽TP数据库查询构造方法多,参数格式不固定

    TP系列一直让我们吐槽的是效果相同的操作方法多得让开发者有记忆和选择困难,有了update还支持save,update($data)也可以data($data),where()参数写法超过两种,让人记忆混淆,特别是从3.2一直追到6的开发者,这个过程中,出现好多种where参数写法(字符串,关联数组,索引数组,而数组的方式格式还不一样,经常有开发者拿3.2的写法用在5.1以上的版本),中间还废弃了一些。

  • 竟然支持无条件删除所有数据,既然不建议,就不用提供,而且删除所有数据,比如mysql下建议使用truncate而不是简单delete

  • where

    where是think orm中的精髓之一。

    where三种方式:

    where('字段名','查询表达式','查询条件');
    where(数组查询条件);
    where(字符串查询条件);
    

    从tp3到tp6,where方法支持多参数、字符串参数、数组参数。我们比较喜欢的是where带数组参数的查询,数组结构太容易配置参数和扩展了。

    • 多参数:

      Db::name('user')->where('id','in',[1,5,8])->select();
      Db::name('user')->where('id','exp',' IN (1,3,8) ')->select();
      
    • 数组

      支持索引数组与键值对,键值对一般用于等值查询,索引数组可支持的表达式更多:

      // 传入数组作为查询条件
      Db::table('think_user')->where([
          ['name','like','thinkphp'],
          ['status','=',1]
      ])->select(); 
      
    • 字符串

      字符串一般用于比较不好用以上两种方式实现的查询条件,更贴近原生:

      Db::table('think_user')->whereRaw('type=1 AND status=1')->select(); 
      
  • 链式操作

    where、table、alias、field、limit、page、order、group、having、join、union、lock、distinct等

    where、field、order这几个我们更喜欢用数组作为参数。

  • page方法比limit更加直观人性化

    使用page方法你不需要计算每个分页数据的起始位置,page方法内部会自动计算。

    // 查询第一页数据
    Db::table('article')->limit(0,10)->select(); 
    // 查询第二页数据
    Db::table('article')->limit(10,10)->select(); 
    // 查询第一页数据
    Db::table('article')->page(1,10)->select(); 
    // 查询第二页数据
    Db::table('article')->page(2,10)->select(); 
    

    注意:page从1开始,limit是从0的位置开始。

  • having

    having方法只有一个参数,并且只能使用字符串。

    Db::table('score')
    ->field('username,max(score)')
    ->group('user_id')
    ->having('count(test_time)>3')
    ->select(); 
    
  • join

    • INNER JOIN: 等同于 JOIN(默认的JOIN类型),如果表中有至少一个匹配,则返回行
    • LEFT JOIN: 即使右表中没有匹配,也从左表返回所有的行
    • RIGHT JOIN: 即使左表中没有匹配,也从右表返回所有的行
    • FULL JOIN: 只要其中一个表中存在匹配,就返回行

    如果还不明白4个的区别,可以参考:mysql的join明明白白 - 9ong

  • lock

    Db::name('user')->where('id',1)->lock(true)->find();
    

    就会自动在生成的SQL语句最后加上 FOR UPDATE或者FOR UPDATE NOWAIT(Oracle数据库)。

  • cache 本地文件缓存

    我们先说mysql旧版本也提供了查询缓存,mysql实战阅读笔记-基础架构-缓存 - 9ong,mysql的server层提供了查询缓存,但我们不建议使用,而且在mysql8.0版本会被废弃。server层的查询缓存其实并不高效,因为数据可能会经常变更,导致缓存失效频率太高,反而增加写缓存的时间,查询缓存适合历史静态数据。

    而tp6支持的cache是在本地的cache,在外部还可以通过\think\facade\Cache::get(cache标识)来获取查询缓存数据:

    $result = Db::table('user')->cache('key',60)->find();
    $data = \think\facade\Cache::get('key');
    

    从动态数据与静态数据原理来看,适当选择查询缓存机制。

  • comment 支持为sql添加注释内容

  • fetchSql 返回sql语句,适合用于调试,以前的版本还得组装输出

  • force 强制使用索引

    如果你觉得使用这个索引更高效的话,可以强制使用索引,毕竟mysql有时也会犯糊涂,选择错误的索引。

    参考:mysql实战阅读笔记 - mysql如何选择索引 - 9ong

  • partition 支持分区查询

    我们不建议在mysql上使用分区,这个涉及到分区访问方式和锁机制,详细见:mysql实战阅读笔记 - 分区与分表 - 9ong

  • tp6内置分页实现

    区别于limit与page方法,paginate方法支持定义分页的数量、当前页、url路径、url参数、锚点、分页变量名,用于html模板的渲染。

    分页查询 · ThinkPHP6.0完全开发手册 · 看云

  • 时间查询,框架内置了常用的时间查询,支持自动识别时间字段类型

    时间查询 · ThinkPHP6.0完全开发手册 · 看云

    很适合用于流水账、记录类、日志类的数据库查询

  • 高级查询

    提供了快捷查询方式、批量查询方式、闭包查询、混合查询、字符串查询;我们觉得快捷查询并不快捷,还额外让开发者有了记忆负担,建议熟练批量查询并偶尔字符串查询及闭包查询,结合whereXXX快捷方法基本可以完成所有查询条件。

    快捷查询里,常用的有whereOr,因为很多人都不知道where方法如何做到whereOr的效果。

    就是有点啰嗦,当然可能也是为了照顾各种不同需求,大而全,也很容易给开发者造成很多烦恼,说不定以后得出个mini-thinkphp。

    当然好处也大大的,就是你想要什么样的查询,tp6都有:

    高级查询 · ThinkPHP6.0完全开发手册 · 看云

  • 视图查询

    官方说并不是数据库的视图,而是join的上一层封装实现而已。

    但应该有很多开发者疑惑这个view方法到底是什么,代替了join不需要数据库视图的支持就可以实现,那具体完成的是什么sql语句,还是说通过其他方式获得数据,在框架层做了join算法处理,肯定不是。

    @todo有空时,配置下满足条件的join表,测试下效果与sql

    mysql实战阅读笔记 - 9ong - join是怎么工作的

  • json类型查询

    mysql也开始支持json类型字段了,有点类似于NoSQL的感觉,TP6提供了关于json字段(可以是字符串类型字段存储json格式,可以是json类型字段)

    • 支持json类型的写入
    • 支持json类型中部分数据的查询
    • 支持json类型数据更新
    
    $user['name'] = 'jm';
    $user['info'] = [
        'site'    => '9ong.com',
        'nickname' => 'tsingchan',
    ];
    //插入json类型数据
    Db::name('user')
        ->json(['info'])
        ->insert($user);
    
    //查询
    $user = Db::name('user')
        ->json(['info'])
        ->where('info->nickname','tsingchan')
        ->find();
      
    //更新
    
    $data['info'] = [
        'site'    => 'www.9ong.com',
        'nickname' => 'tsingchanreal',
    ];
    Db::name('user')
        ->json(['info'])
        ->where('id',1)
        ->update($data);
    
    

    JSON字段 · ThinkPHP6.0完全开发手册 · 看云

  • 子查询sql生成

    fetchSql buildSql

    对于复杂的查询,用到子查询语句时,我们可以使用buildSql方法生成子查询语句:

    $subQuery = Db::table('think_user')
        ->field('id,name')
        ->where('id', '>', 10)
        ->buildSql();
    Db::table($subQuery . ' a')
        ->where('a.name', 'like', '9ong')
        ->order('id', 'desc')
        ->select();
    
  • 原生查询

    原生sql通过query及execute实现,支持参数绑定,实现方式有:问号占位符?和命名占位符:paramter,与sql语句参数绑定保持一致。

    如果是原生sql语句,建议用参数绑定的方式,避免sql注入问题:

    Db::query("select * from think_user where id=? AND status=?", [8, 1]);
    // 命名绑定
    Db::execute("update think_user set name=:name where status=:status", ['name' => 'thinkphp', 'status' => 1]);
    
  • 查询事件

    数据库操作的回调也称为查询事件,是针对数据库的CURD操作而设计的回调方法。

    目前有5个事件:before_select、before_find、after_insert、after_update、after_delete

    查询事件 · ThinkPHP6.0完全开发手册 · 看云

  • 支持mysql的InnoDB的XA分布式事务

  • 数据集

    数据库的查询结果默认返回数据集对象think\Collection。提供了和数组无差别用法,并且另外封装了一些额外的方法。

    个人反感额外封装上层数据集对象,php的精髓数组很尴尬,好用,但php又要追面向对象编程的方式,数据库与模型的查询返回结果默认统一为数据集对象。如果一个项目model层可以更换的话,不同框架的orm返回的结果集结构保持一致是最好的,键值对的数组是一个很好的通用的统一的规范数据结构。

    在模型中进行数据集查询,全部返回数据集对象,但使用的是think\model\Collection类(继承think\Collection),但用法是一致的。

    数据集 · ThinkPHP6.0完全开发手册 · 看云

模型

模型的操作方法无需像数据库查询一样调用首先必须调用table或者name方法,因为模型会按照规则自动匹配对应的数据表,例如:

Db::name('user')->where('id','>',10)->select();

改成模型操作的话就变成

User::where('id','>',10)->select();

@todo存疑。虽然看起来是相同的查询条件,但一个最明显的区别是查询结果的类型不同。第一种方式的查询结果是一个 (二维)数组,而第二种方式的查询结果是包含了模型(集合)的数据集。不过,在大多数情况下,这二种返回类型的使用方式并无明显区别。

上面这段说返回结果的类型不同,一个二维数组?但在文档数据集 · ThinkPHP6.0完全开发手册 · 看云,却说数据库的查询结果默认返回数据集对象think\Collection。在模型中进行数据集查询,全部返回数据集对象,但使用的是think\model\Collection类(继承think\Collection),但用法是一致的。

官方介绍:

模型操作和数据库操作的另外一个显著区别是模型支持包括获取器、修改器、自动时间写入在内的一系列自动化操作和事件,简化了数据的存取操作,但随之而来的是性能有所下降(其实并没下降,而是自动帮你处理了一些原本需要手动处理的操作)。

  • 支持动态切换后缀,支持多语言或分表情况,解决相同结构表多个模型问题

    // suffix方法用于静态查询  blog_cn blog_en
    $blog = Blog::suffix('_en')->find();
    $blog = Blog::suffix('_cn')->find();
    
  • 模型如果没有提前明确定义字段,将会浪费一次查询开销,但定义字段偏麻烦,当然TP还是一直提供指令来手动缓存数据库表字段信息

    php think optimize:schema
    

    当然这也有个弊端,在分布式服务器上每个服务器都需要执行一次,需要一个控制脚本来批量处理。

  • 新增、更新

    • 模型对象实例方法

      save、saveAll

      save方法成功返回true,并只有当before_update事件返回false的时候返回false,有错误则会抛出异常。

    • 模型类静态方法

      create、update,返回模型对象实例

    • 最佳实践建议

      总之关于新增插入数据,使用静态方法create新增数据,使用实例方法saveAll批量新增数据。关于更新,如果是先查询数据后,再更新,建议采用实例方法save;如果是直接更新,可以使用静态方法update

    • 注意事项

      • 不要调用save方法进行多次数据写入。不要在一个模型实例里面做多次新增或更新,会导致部分重复数据不再更新,或重复插入数据。

      • 如非必要,尽量不要使用批量更新。(先查询后更新,对于性能影响大不大?)

  • 删除

    模型的删除和数据库的删除方法区别在于,模型的删除会包含模型的事件处理。

    • 模型删除

      $user = User::find(1);
      $user->delete();
      
    • 静态方法删除

      User::destroy(1);
      // 支持批量删除多个数据
      User::destroy([1,2,3]);
      User::where('id','>',10)->delete();//这个delete不是模型实例的delete方法,不会处理模型事件
      
    • 建议

      建议使用静态方法destory。(关键词有点奇怪,可能是delete被用了,没有找到更好的)

  • 查询

    模型查询除了使用自身的查询方法外,一样可以使用数据库的查询构造器,返回的都是模型对象实例。但如果直接调用查询对象的方法,IDE可能无法完成自动提示。如果很在意IDE是否能自动完成,特别是完美主义者很难接受。

    在文档中看到太多类似的提醒:“如果你是在模型内部获取数据,请不要使用$this->name的方式来获取数据,请使用$this->getAttr(‘name’) 替代。”

    而这些提醒很大部分让人感觉是自然而然的实现方式,却被告知不要这样不要那样,这很不合理!好产品应该让人感觉很自然就是这么用,结果你反其道而行。吐槽他人总是容易的哈,没忍住。

    最佳实践:

    模型查询的最佳实践原则是:在模型外部使用静态方法进行查询,内部使用动态方法查询,包括使用数据库的查询构造器。

    查询 · ThinkPHP6.0完全开发手册 · 看云

  • 查询范围

    官方支持一些查询的二次封装,看起来挺有用的,但使用方式像是在做字符游戏,有点牵强的使用方式,不自然,难以记忆及阅读,而且对于IDE来说不友好。

  • 获取器

    获取器的作用是对模型实例的(原始)数据做出自动处理。

    获取器,获取的是字段值对应自定义的值,比如status值为1,我们自定义为正常,0定义为异常;

    使用场景:集合/枚举/数字状态/组合字段的转换文本输出场景,以前需要开发者自己组织这方面的代码,TP6在模型中提供了获取器的设置与读取。

    另外除了获取器这样的实现样,为了编码效率,可以在模型文件中定义这些枚举、数字状态之类的字段值的一个常量。比如字段status值有0有1,在模型文件中就定义:

    const STATUS_NORMAL = 1;//正常
    const STATUS_ABNORMAL = 0;//异常
    

    获取器方法命名规范:

    //getFieldNameAttr
    public function getStatusAttr($value)
    {
        $status = [-1=>'删除',0=>'禁用',1=>'正常',2=>'待审核'];
        return $status[$value];
    }
    
    $user = User::find(1);
    echo $user->status; // 例如输出“正常”
    
  • 修改器

    和获取器相反,修改器的主要作用是对模型设置的数据对象值进行处理。

    当然修改器方法仅对模型的写入方法有效,调用数据库的写入方法写入无效,例如下面的方式修改器无效。

    //模型类
    //setFieldNameAttr
    public function setNameAttr($value)
    {
        return strtolower($value);
    }
      
    

    模型实例在赋值字段时,将会自动触发对应的修改器:

    $user = new User();
    $user->name = 'TsingChan';
    $user->save();
    echo $user->name; // tsigchan
    
  • 搜索器

    头疼,一个查询,何必把条件表达式分布在不同层代码里,加大阅读难度,加大调试难度。

    好吧,吐槽完,就会发现还是有用的:

    搜索器的场景包括:

    • 限制和规范表单的搜索条件;
    • 预定义查询条件简化查询;

    在一些项目中,这些限制与规范可以降低同组的粗心的开发者外部查询条件不准确或有意外的问题,使得代码更健壮。

    如果有个很大的表,我们往往很担心前端或业务逻辑层的开发者没有过滤好查询条件,比如时间跨度查询,如果时间输入不是合法的,或者是跨度很大,或是缺失参数,此时,模型层还能挽救下,避免非法异常查询,导致慢查询等问题:

    //模型类
    public function searchCreateTimeAttr($query, $value, $data)
    {
        //两个时间点的跨度之类判断等逻辑再处理@todo
        $query->whereBetweenTime('create_time', $value[0], $value[1]);
    } 
    
  • 数据集

    模型的select查询方法返回数据集对象 think\model\Collection,该对象继承自think\Collection,因此具有数据库的数据集类的所有方法,而且还提供了额外的模型操作方法。

    注意:数据集对象的空判断,不能使用empty结构,而要用数据集对象的isEmpty方法判断。

  • 自动时间戳

    // 开启自动写入时间戳字段
    'auto_timestamp' => true,
    

    或者模型类中定义变量:

    protected $autoWriteTimestamp = true;
    

    时间字段的自动写入仅针对模型的写入方法,如果使用数据库的更新或者写入方法则无效。

    我们很喜欢用,因为我们的表基本都会创建create_time和update_time,但注意auto_timestamp在TP6是默认开启的,如果不需要自动写入,需要再单独的模型中设置

    protected $autoWriteTimestamp = false;
    

    写入数据的时候,系统会自动写入create_time和update_time字段。

  • 只读字段

    支持保护敏感字段。

    只读字段用来保护某些特殊的字段值不被更改,这个字段的值一旦写入,就无法更改。 要使用只读字段的功能,我们只需要在模型中定义readonly属性:

    protected $readonly = ['name', 'email'];
    

    模型类的配置,全靠protected的属性支撑着哈。

    只读字段仅针对模型的更新方法,如果使用数据库的更新方法则无效,例如下面的方式无效。

    //数据库的更新方式
    $user = new User;
    // 要更改字段值
    $data['name'] = 'tsingchan';
    $data['email'] = 'jm@9ong.com';    
    // 保存更改后的用户数据
    $user->where('id', 5)->update($data);
    
  • 软删除

    软删除只有在模型下有效,数据库方式无效

    使用官方模型的软删除,可以正常使用模型的删除方法,还可以通过方法恢复软删除数据;当然如果我们觉得麻烦或者觉得还是数据库方式用的舒服,那也可以考虑自己设计软删除,也不复杂,就是要注意调用删除方法(自定义封装)

    软删除 · ThinkPHP6.0完全开发手册 · 看云

  • 模型事件

    模型事件只在调用模型的方法生效,使用查询构造器操作是无效的

    namespace app\model;
    
    use think\Model;
    use app\model\Profile;
    
    class User extends Model
    {
        public static function onBeforeUpdate($user)
        {
            if ('thinkphp' == $user->name) {
                return false;
            }
        }
          
        public static function onAfterDelete($user)
        {
            Profile::destroy($user->id);
        }
    }
    

    更多模型事件 · ThinkPHP6.0完全开发手册 · 看云

  • 模型关联

    模型关联,让开发者不用去关心底层sql具体如何实现,甚至怎么组装sql更优化,也避免致命的sql错误及严重的性能瓶颈

    但模型关联,使用方式还有点生硬,可能多用几次后,会更加上手,特别是根据关联条件查询has、hasWhere 的使用,一开始会摸不着头脑

模型的使用过程中,太多前提限制条件。虽然官方说6.0的原则是规范和统一,确实越来越规范了,但规则不统一,容易让人混乱;比如多对多关联,中间表要继承新的类,会自动关闭自动写入时间,需要手动重新设置;当然模型会越来越让开发门槛更低,不用再去了解数据库和sql,要掌握模型建议不要带着数据库sql的想法去看;存在不少特殊情况,这也是框架发展的一个趋势,特殊设置多,多到你记不住。

视图

  • 视图过滤

    允许通过闭包的方式对视图进行过滤替换

  • 模板引擎

    新版框架默认只能支持PHP原生模板,如果需要使用thinkTemplate模板引擎,需要安装think-view扩展(该扩展会自动安装think-template依赖库)。

    composer require topthink/think-view
    

    在配置目录下view.php:

    return [
        // 模板引擎类型
        'type'         => 'Think',//值php只支持php原生模板,值Think支持thinkTemplate模板引擎
        // 模板路径
        'view_path'    => './template/',
        // 模板后缀
        ...
    ];
    

    关于thinkTemplate引擎更多信息:介绍 · ThinkTemplate开发指南 · 看云

  • thinkTemplate模板引擎文档

    安装配置 · ThinkTemplate开发指南 · 看云

    安装配置、模板渲染、变量输出、使用函数、模板布局、模板继承、包含文件、输出替换、标签库、标签与扩展等

异常

  • 自定义异常页面

    config/app.php

    // 异常页面的模板文件
    'exception_tmpl'   => app()->getThinkPath() . 'tpl/think_exception.tpl',
    
    // 错误显示信息,非调试模式有效
    'error_message'    => '页面错误!请稍后再试~',
    // 显示错误信息
    'show_error_msg'   => true,
    
  • 自定义异常处理(接管异常处理)

    框架支持异常处理由开发者自定义类进行接管,需要在app目录下面的provider.php文件中绑定异常处理类,例如:

    // 绑定自定义异常处理handle类
    'think\exception\Handle' => ExceptionHandle::class,
    

    实际上TP6安装时已经内置了app\ExceptionHandle异常处理类,我们可以参考该类定义自己的异常处理类

  • 异常抛出及捕获

    很高兴,php的框架都在进步,很明显的一点就是在错误和异常处理上,越来越多的框架采用异常机制,虽然php异常内置系统异常较少,但越来越多框架补了这块短板,并引导开发者去使用,当然开发者不论是构件/中间件开发者还是产品开发者,都应该更注意异常的抛出,协助完善php异常生态,让系统更加灵活优雅。(虽然这些java在很多年前就干的很好了)

  • HTTP异常

    框架提供了一个abort助手函数快速抛出一个HTTP异常。这个方式和我们之前在封装的抛出异常的通用函数思路相似,我们很喜欢用异常,也喜欢用这个快速抛出异常的函数封装。

    // 抛出404异常
    abort(404, '页面异常');
    

日志

日志记录了所有的运行错误,日志配置app/log.php

官方这个版本的日志,可以满足我们各种需要的日志功能,不需要自己想破脑子的封装自己的日志组件

支持日志配置,多通道,多日志级别类型,支持延时和实时写入,提供日志级别的写入方法,允许上下文信息替换写入,支持某个级别的日志信息单独文件写入,默认日志按日期分目录按天生成新日志文件,但我们也可配置将某类型日志全部记录到一个文件,还支持日志写入回调,可以通过clear静态方法手动清除日志,也可以配合日志自动清理,支持json格式日志配合第三方日志分析工具

  • Log::record方法,日志写入内存

  • Log::write方法,日志实时写入通道

  • Log::info方法,info级别日志

  • Log::error方法,error级别日志

  • 助手函数trace(“错误日志”,“error”)

  • 也支持Log::diy(),自定义日志类型

  • log配置文件log.php

  • 上下文信息,写到心里来了,还可以进一步封装为特殊的日志信息格式,用于替换,比如新旧数据的修改日志

    Log::info('修改名称为{new}', ['new' => 'tsingchan']);
    
  • 独立日志

    日志默认是全部写到同一个地方,我们可以通过apart_level设置哪些级别的日志可以写到独立的日志(比如sql单独写到sql的日志文件里):

    return [
        'default'      => 'file',
        'channels'    =>    [
            'file'    =>    [
                'type'          => 'file', 
                // error和sql日志单独记录
                'apart_level'   =>  ['error','sql'],
            ],
        ],
    ];
    

    如果apart_level设置为true,则表示所有的日志类型都会独立记录。

  • 写入处理回调

    这里我们主要是为了加深事件机制。我们看下这里如何通过事件实现日志写入处理回调:

    Event::listen('think\event\LogWrite', function($event) {
        if('file' == $event->channel) {
            $event->log['info'][] = 'test info';
        }
    });
    

    这里监听了think\event\LogWrite事件,一旦监听到事件触发,将调用后面的匿名函数作为回调处理。

  • Log::clear 清理所有通道的日志(内存中),也可以传入通道清理指定通道日志

  • Log::close 关闭所有通道日志写入,也可以传入通道关闭指定通道日志写入

  • 文件写入路径,默认写入runtime/log/YYYYmm/dd.log,可以同配置path调整

  • 范例

    $msg = "今天是{date}";
    $context = ["date"=>date("Y-m-d H:i:s")];
    \think\facade\Log::record($msg, "info", $context);
    \think\facade\Log::write($msg, "debug", $context);
    
    [2020-01-22T20:03:19+08:00][info] 今天是2020-01-22 20:03:19
    [2020-01-22T20:03:19+08:00][debug] 今天是2020-01-22 20:03:19
    

详见官方文档日志处理 · ThinkPHP6.0完全开发手册 · 看云

调试

调试是开发的一个很重要的环节

  • 环境配置

    通过create-project默认安装的话, 会在根目录自带一个.example.env文件,你可以直接更名为.env文件。

    本地开发的时候可以在应用根目录下面定义.env文件。在开发阶段,可以修改环境变量APP_DEBUG开启调试模式。

  • 部署模式下显示具体错误信息

    在部署模式下,发生错误后不会提示具体的错误信息,我们经常希望看到具体的错误信息,可以在app.php文件中如下设置:

    // 显示错误信息
    'show_error_msg'        =>  true,    
    

    其实更好的方法应该是在部署模式下,error级别的错误,记录到错误日志文件即可,不需要展示,错误时展示异常页面更友好

  • Trace调试

    Trace调试功能就是ThinkPHP提供给开发人员的一个用于开发调试的辅助工具。可以实时显示当前页面或者请求的请求信息、运行情况、SQL执行、错误信息和调试信息等,并支持自定义显示,并且支持没有页面输出的操作调试。

    默认安装的时候会自动安装topthink/think-trace扩展,所以你可以在项目里面直接使用。

    很不错的扩展调试工具。

    页面Trace功能仅在调试模式下有效。

验证

看过php扩展Filter用于验证与过滤数据,我们会更喜欢TP的验证器、验证规则等

PHP函数参考30-Filter过滤器扩展函数 - 9ong

验证器 · ThinkPHP6.0完全开发手册 · 看云

  • 定义与验证

    支持在控制器或业务类中设置验证规则(rule)并执行验证(check)

    \think\facade\Validate::rule($rules)->check($data);
    

    也支持独立的验证类,可以让业务开发更关注业务,工具开发关注工具

    除了手写类外,我们还可以使用官方提供的命令:

    php think make:validate User
    

    验证器定义:定义类、定义规则、定义错误信息

    class User extends Validate
    {
        /**
        * 定义验证规则
        * 格式:'字段名'	=>	['规则1','规则2'...]
        *
        * @var array
        */	
        protected $rule =   [
            'name'  => 'require|max:25|min:6',
            'age'   => 'number|between:1,120',
            'email' => 'email',    
        ];
          
        /**
        * 定义错误信息
        * 格式:'字段名.规则名'	=>	'错误信息'
        *
        * @var array
        */	
        protected $message  =   [
            'name.require' => '名称必须',
            'name.max'     => '名称最多不能超过25个字符',
            'name.min'     => '名称最少要6个字符',
            'age.number'   => '年龄必须是数字',
            'age.between'  => '年龄只能在1-120之间',
            'email'        => '邮箱格式错误',    
        ];    
    }    
    

    验证调用

    try{
        $res = validate(\app\validate\User::class)->batch(true)->check([
            'name'  => '12345',
            'email' => 'thinkphpqq.com',                
        ]);
        if(true != $res){
            dump($res);
        }
              
    } catch (\think\exception\ValidateException $ex) {
        dump($ex->getMessage());
    }  
    

    如果是临时使用或少量规则,可以使用当下临时验证;常用提交页面或API可以使用验证器类,易于拓展且不涉及业务逻辑代码

  • 验证场景

    上面提到验证器类可以满足新增、修改相关数据对象,虽然新增和修改或其他操作验证的字段大部分一样,但有时也存在部分字段差异,官方提供了验证场景的机制,很好的补充这方面

    //验证器类
    protected $scene = [
        'edit'  =>  ['name','age'],
    ];
    

    然后验证的时候,指定验证场景:

     validate(app\validate\User::class)
        ->scene('edit')
        ->check($data);
    

    表示当前场景是edit,只验证name、age字段

  • 内置规则

    验证的主体

    内置规则 · ThinkPHP6.0完全开发手册 · 看云

  • 路由验证

    我们甚至可以直接在路由就直接验证;

    路由验证 · ThinkPHP6.0完全开发手册 · 看云

  • 表单令牌

    如果没有采用前后端分离的开发方式的话,提交建议采用表单令牌,避免低级的重复提交和重放攻击

    注意token的实现方式依赖于session中间件(不是php的原始session机制,是TP官方的session机制)

  • 注解验证

    等php的注解和各方框架的注解有了比较统一的注解方式时,我们会更愿意考虑。

缓存

TP6缓存cache更多的是集成了文件、memcache、redis等第三方缓存的一些基础实现,主要是通用的set、get、inc、dec、expire等。

如果我们确定使用redis内存数据库级别的缓存,可以通过返回当前缓存类型对象的句柄进行相应缓存操作方法:

$redis = Cache::store('redis')->handler();//可能是\Redis|\Phpredis类,根据使用的是php-redis扩展还是采用phpredis

内置支持的缓存类型包括file、memcache、wincache、sqlite、redis。

如果自己实现缓存类需要注意:

  • 缓存容易配置
  • 默认过期时间
  • 缓存路径(redis操作库)
  • 前缀定义

参考:缓存 · ThinkPHP6.0完全开发手册 · 看云

session与cookie

  • session

    新版本不支持操作原生$_SESSION数组和所有session_开头的函数,只能通过Session类(或者助手函数)来操作。会话数据统一在当前请求结束的时候统一写入 所以不要在session写入操作之后执行exit等中断操作,否则会导致Session数据写入失败。

    • session是中间件
    • 默认不开启
    • 官方提示:尽量避免把对象保存到Session会话
    • session 提供了一个闪存flah方法,下次请求前有效
    • 在分布式场景,可以将session存储方式更换为cache

    Session · ThinkPHP6.0完全开发手册 · 看云

  • cookie

    ThinkPHP采用think\facade\Cookie类提供Cookie支持。

多语言

  • 开启多语言中间件

    // 多语言加载
     \think\middleware\LoadLangPack::class,
    
  • 配置lang.php

    • 设置默认语言

    • 设置语言列表

    • 设置语言变量名

      • url参数:比如lang
      • cookie:比如think_lang
      • header:比如think_lang,V6.0.3之后支持
    • 检查语言顺序

      开启自动侦测后会会首先检查请求的URL或者Cookie中是否包含语言变量,然后根据HTTP_ACCEPT_LANGUAGE自动识别当前语言(并载入对应的语言包)。

  • 语言文件定义

    • 默认文件位置

      // 单应用模式
      app\lang\当前语言.php
      // 多应用模式
      app\应用\lang\当前语言.php
      
    • 语言文件格式

      语言包文件支持多种格式,包括php数组、yaml格式及json格式(V6.0.4+)

      zh_cn.php

      return [
          'helo'  => '你好!',
          'something wrong' => '有什么地方出错了。',
      ];
      
    • 其他语言包

      'extend_list'    =>    [
          'zh-cn'    => [
              app()->getBasePath() . 'lang\zh-cn\app.php',
              app()->getBasePath() . 'lang\zh-cn\core.php',
          ],
      ]
      
  • 多语言输出

    • 控制器输出

      return \think\facade\Lang::get("helo")."\t".lang("something wrong")."\t".lang("app helo").PHP_EOL;
      //你好! 有什么地方出错了。 app helo
      
    • 模型类验证输出提示信息

      通常多语言的使用是在控制器里面,但是模型类的自动验证功能里面会用到提示信息,这个部分也可以使用多语言的特性。

      如果使用了多语言功能的话(假设,我们在当前语言包里面定义了’ lang_var'=>'标题必须!'),就可以使用下面的字符串来替代原来的错误提示。

      {%lang_var}
      
    • 模板输出

      如果要在模板中输出语言变量不需要在控制器中赋值,可以直接使用模板引擎特殊标签来直接输出语言定义的值:

      {$Think.lang.lang_var}
      
  • 变量传入输出

    采用变量绑定替换的方式:

    'file_format'    =>    '文件格式: {:format},文件大小:{:size}',
    

    模板中输出:

    {:lang('file_format',['format' => 'jpeg,png,gif,jpg','size' => '2MB'])}
    
  • 语言分组

    • 开启分组配置

      // 开启多语言分组
      'allow_group'    =>    true
      
    • 语言分组定义

      return [
          'user'    =>    [
              'welcome'  => '欢迎回来',
              'login' => '用户登录',
              'logout' => '用户登出',
          ]
      ];
      
    • 使用语言分组

      Lang::get('user.login');
      lang('user.login');
      
  • php官方多语言方案getText

    其实php官方也提供了多语言国际化方案getText,在大多php7.1的版本都默认有安装: PHP函数参考10-GetText国际化解决方案 - 9ong

    我们觉得getText的方案有两个让开发者不舒适的地方:

    • 使用前的环境配置。

      比如setlocale、bindTextDomain等,由于getText的实现不像TP、laravel框架多语言实现的层级高,所以像寻找语言文件这些都没有提取配置封装,就是配置和使用耦合在一起,需要我们自己手动分离。

    • po文件的制作

      getText的语言文件是建立在po文件的基础上,对于po文件的创建与编辑还是有点门槛的。

看完TP的多语言方案,我们还是很赞的,在更新的版本中还支持json格式定义语言文件,这个以后会有助于,开发语言之间的语言包的共享。

上传

从官方的评论来看,上传是一个经常使用的功能,随着上云的产品越来越多,上传到第三方云端会是更多开发者的选择

阿里云、腾讯云、又拍云、七牛牛等

命令行

  • 查看所有命令与介绍

    PS I:\src\tp6\tp6> php think
    version 6.0.7
    
    Usage:
    command [options] [arguments]
    
    Options:
    -h, --help            Display this help message
    -V, --version         Display this console version
    -q, --quiet           Do not output any message
        --ansi            Force ANSI output
        --no-ansi         Disable ANSI output
    -n, --no-interaction  Do not ask any interactive question
    -v|vv|vvv, --verbose  Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
    
    Available commands:
    clear             Clear runtime file
    help              Displays help for a command
    list              Lists commands
    run               PHP Built-in Server for ThinkPHP
    version           show thinkphp framework version
    make
    make:command      Create a new command class
    make:controller   Create a new resource controller class
    make:event        Create a new event class
    make:listener     Create a new listener class
    make:middleware   Create a new middleware class
    make:model        Create a new model class
    make:service      Create a new Service class
    make:subscribe    Create a new subscribe class
    make:validate     Create a validate class
    optimize
    optimize:route    Build app route cache.
    optimize:schema   Build database schema cache.
    route
    route:list        show route list.
    service
    service:discover  Discover Services for ThinkPHP
    vendor
    vendor:publish    Publish any publishable assets from vendor packages
    
  • 启动内置服务器

    php think run
    php think run -H tp.com -p 80
    
  • 自动生成应用目录

    对我们来说还是很有用处的命令工具:

    • 快速生成应用

      php think build demo
      

      会自动生成demo应用,自动生成的应用目录包含了controller、model和view目录以及common.php、middleware.php、event.php和provider.php等文件。

    • 应用目录结构自定义

      如果你希望自定义生成应用的结构,可以在app目录下增加一个build.php文件,内容如下:

      return [
          // 需要自动创建的文件
          '__file__'   => [],
          // 需要自动创建的目录
          '__dir__'    => ['controller', 'model', 'view'],
          // 需要自动创建的控制器
          'controller' => ['Index'],
          // 需要自动创建的模型
          'model'      => ['User'],
          // 需要自动创建的模板
          'view'       => ['index/index'],
      ];
      

      可以给定义需要自动生成的文件和目录,以及MVC类。

      • dir 表示生成目录(支持多级目录)
      • file 表示生成文件(默认会生成common.php、middleware.php、event.php和provider.php文件,无需定义)
      • controller表示生成控制器类
      • model表示生成模型类
      • view表示生成模板文件(支持子目录)

    自动生成应用目录 · ThinkPHP6.0完全开发手册 · 看云

  • 快速生成类库文件

    • 控制器类
    • 模型类
    • 中间件类
    • 验证器类
    • 事件类
    php think make:controller Blog  //单应用
    php think make:model index@Blog //多应用下的index应用
    php think make:middleware Auth
    php think make:validate index@User
    

    创建类库文件 · ThinkPHP6.0完全开发手册 · 看云

  • 清除缓存文件

    比如有些开发者可能需要在更新服务端代码后,需要删除runtime下的文件,但又担心不小心误操作/误删服务器上的文件,我们就可以考虑使用TP提供的命令行工具:

    不带任何参数调用clear命令的话,会清除runtime目录(包括模板缓存、日志文件及其子目录)下面的所有的文件,但会保留目录。

    //clear 还有更多参数,详细见官方文档
    php think clear
    

    清除缓存文件 · ThinkPHP6.0完全开发手册 · 看云

  • 生成数据库表字段缓存

    比如通过生成数据库表字段信息缓存,可以提升数据库查询性能,避免不必要的表字段查询。

    //更多参数详见官方文档
    php think optimize:schema
    

    执行后会自动在runtime/schema目录下面按照数据表生成字段缓存文件。

    没有继承think\Model类的(抽象)模型类不会生成。分布式服务器的runtime文件不一样,需要所有服务器都执行一次该命令,重新生成相应缓存

    生成数据表字段缓存 · ThinkPHP6.0完全开发手册 · 看云

  • 路由相关

    生成路由映射缓存 · ThinkPHP6.0完全开发手册 · 看云

    输出路由定义 · ThinkPHP6.0完全开发手册 · 看云

  • 自定义命令

    • 通过命令command创建自定义命令类文件
    • 配置config/console.php
    • 终端执行命令
    • 控制器中调用命令

    详细参见:自定义指令 · ThinkPHP6.0完全开发手册 · 看云

数据库迁移工具

安装:

composer require topthink/think-migration

数据库迁移工具 · ThinkPHP6.0完全开发手册 · 看云

可用于程序安装上的数据库表创建,也可用服务数据库表升级,降级还是谨慎操作,毕竟一般只增不减。

think-migration基于Phinx Documentation - 0.12实现,但目前维护上好像比较吃力,tp产品线版本多。

验证码

如果对验证码使用要求严谨,建议使用第三方验证方式(风控/人机验证),比如极验证、网易易盾等

当然官方提供的验证码还是能满足一般的需求的。但现在验证标配都要达到极验、网易易盾、阿里云验证等级别了。

验证码 · ThinkPHP6.0完全开发手册 · 看云

Workerman

Workerman是一款纯PHP开发的开源高性能的PHP socket 服务器框架。被广泛的用于手机app、手游服务端、网络游戏服务器、聊天室服务器、硬件通讯服务器、智能家居、车联网、物联网等领域的开发。 支持TCP长连接,支持Websocket、HTTP等协议,支持自定义协议。基于workerman开发者可以更专注于业务逻辑开发,不必再为PHP Socket底层开发而烦恼。

composer require topthink/think-worker

workerman引擎相对比较成熟,本身也是php基础上实现,在结合到TP框架中也比较稳定。

Workerman · ThinkPHP6.0完全开发手册 · 看云

Workerman结合其他mvc框架 - 9ong

Swoole

Swoole 是一个使用 C++ 语言编写的基于异步事件驱动和协程的并行网络通信引擎,为 PHP 提供协程、高性能网络编程支持。提供了多种通信协议的网络服务器和客户端模块,可以方便快速的实现 TCP/UDP服务、高性能Web、WebSocket服务、物联网、实时通讯、游戏、微服务等,使 PHP 不再局限于传统的 Web 领域。

think-swoole扩展的使用。目前仅支持Linux环境或者MacOs下运行,要求swoole版本为4.3.1+。

由于 think-swoole是基于swoole的,要了解这个扩展如何使用,首先需要对swoole有一定的了解,这也是本文阅读的前提,具体可以参考 Swoole官方文档内容:Swoole4 文档 - /

composer require topthink/think-swoole

Swoole · ThinkPHP6.0完全开发手册 · 看云

目前看TP在结合Swoole上还有很多问题需要完善。

think助手工具库

composer require topthink/think-helper

目前还只是字符串和数组的简单处理,一般成熟的项目内部也都有字符串、数组、对象、加解密等相关工具库

助手函数

助手函数 · ThinkPHP6.0完全开发手册 · 看云

更新升级

官方不建议老的项目升级到新版,除非你有重构计划,否则就算升级了也只是表面上升级了。也就是这点,让人很头疼,从3到5,5.0到5.x的各个版本,5到6都不能无缝升级或不保证所有功能正常升级。

如果有5.1版本需要升级到6.0的版本,可以参考官方指导: 升级指导 · ThinkPHP6.0完全开发手册 · 看云

其他旧版本要么就这样了,要么重构了。

自动补全

目前即使是一直对TP自动补全做的比较好的phpstorm也暂时无法自动补全,需要继续等待官方或第三方的补充,更不用说那些部分试用的NetBeans、vscode等

如果你使用PHPStorm的话,建议安装PHP Annotations插件:https://plugins.jetbrains.com/plugin/7320-php-annotations ,可以支持注解的自动完成。

单元测试

官方没有提供TP6这方面的单元测试相关文档。

以下一些关于TP5和单元测试的文档:

phpunit基础用法 - 9ong

php单元测试 - 9ong

thinkphp单元测试手册

ThinkPHP5与单元测试PHPUnit使用

PHPUnit简介及使用(thinkphp5的单元测试安装及使用

更多

自动加载原理 · ThinkPHP 6.0 核心分析 · 看云


@continue tsingchan