ThinkPHP6.x 使用指南

发布时间 2023-12-16 02:51:15作者: SRIGT

PHP 版本:PHP 8.1.0

框架版本:ThinkPHP 6

编辑工具:PHPStorm 2021.3.3

系统环境:Windows 10

0x01 概述

(1)简介

  • ThinkPHP 框架简称 TP 框架
  • TP 框架是免费开源的、轻量级的、简单快速且敏捷的 PHP 框架
  • 可以免费使用 TP 框架,可以将项目商用

(2)创建项目

可以在官网获取 TP 的详细安装方法,以下是关键步骤说明

以下创建项目方法都基于 Composer

a. 直接创建项目

  1. 使用命令 composer create-project topthink/think tp 即可创建名称为 tp 的项目

  2. 使用命令 php think run 即可启动项目

    如果提示端口被占用,可以使用命令 php think run -p 80,即在 80 端口启动

b. 在 PHPStorm 创建项目

  1. 选择 “创建 Composer 项目”
  2. 指定 composer.phar 与 PHP 解释器
  3. 软件包选择 topthink/think,版本选择 6.0.x-dev
  4. 完成项目创建
  5. 在 PHPStorm 中有相应的功能用于启动项目

(3)开发规范

  1. TP6 遵循的是 PSR-2 的命名规范和 PSR-4 的自动加载

  2. 目录和文件的规范如下

    1. 目录名(小写+下划线)
    2. 类库和函数文件统一以 .php 为后缀
    3. 类的文件名均以命名空间定义,并且命名空间的路径和类库文件所在路径一致
    4. 类(包含接口和 Trait)文件采用驼峰式命名(首字母大写),其它采用小写 + 下划线命名
    5. 类名(包括接口和 Trait)和文件名保持一致,统一采用驼峰式命名(首字母大写)
  3. 函数和类、属性命名规范如下

    1. 类的命名采用驼峰法(首字母大写),如: UserUserType
    2. 函数的命名使用小写字母和下划线(小写字母开头) 的方式,如:get_client_ip
    3. 方法的命名使用驼峰法(首字母小写),如:getUserName
    4. 属性的命名使用驼峰法(首字母小写),如:tableName
    5. 特例: 以双下划线打头的函数或方法作为魔术方法,如:__call__autoload
  4. 常量与配置的规范如下

    1. 常量以大写字母和下划线命名,如:APP_PATH
    2. 配置参数以小写字母和下划线命名,如:url_convert
    3. 环境变量定义使用大写字母和下划线命名,如:APP_DEBUG
  5. 数据表和字段的规范如下

    1. 数据表和字段采用小写加下划线方式命名
    2. 注意字段名不要以下划线开头,如:think user 表和 user_name
    3. 字段不建议使用驼峰和中文作为数据表及字段命名

(4)目录结构

  • TP6 支持多应用模式部署,app 是应用目录

  • 默认情况下,TP6 是采用的单应用模式

    flowchart TB A(app 应用目录)-->C(controller 控制器目录) & M(model 模型目录) & ... & C2(common.php 公共函数目录) & E(event.php 事件定义文件)
  • 多应用模式,其中的单应用可以有多个

    flowchart TB A(app 多应用目录)-->A2(app_name 单应用目录) & C(common.php 公共函数文件) & E(event.php 事件定义文件) A2-->C1(common.php 函数文件) & C2(controller 控制器目录) & M(model 模型目录) & V(view 视图目录) & ...
  • 在目录结构上,只确保对外可访问的仅 public 目录

    flowchart TB P(public WEB目录/对外访问目录)-->I(index.php 入口文件) & R(router.php 快速测试文件) & .(.htaccess 用于 apache 的重写)
  • 在 app 目录中,还提供了一些其他文件

    文件 说明
    BaseController.php 默认基础控制器类
    ExceptionHandle.php 应用异常定义为空文件
    common.php 全局公共函数文件
    middleware.php 全局中间件定义文件
    provider.php 全局服务提供定义文件
    Request.php 应用请求对象
    event.php 全局事件定义文件

(5)开启调试

  • 在开发阶段,我们建议开启框架的调试模式。调试模式开启后,会牺牲一些执行效率,但大大提高了开发排错的能力。当项目部署到生产环境时,再关闭调试模式即可
  • 安装好的 TP6 默认并没有开启调试。通过命令行安装的 TP6.0,会自动在根目录生成一个 .example.env 文件,这个 .env 文件是环境配置文件,只要删除前面的 .example 即可生效
    • 查看 .env 文件,打开调试的环境变量为 APP_DEBUG = true
    • 右下角会出现 Trace 调试小图标,说明调试开启了
  • 开启调试模式的显著优势
    • 记录系统运行流程的执行过程
    • 展示错误和调试信息,并开启日志记录
    • 模版修改可以及时生效(不会被缓存干扰)
    • 启动右下角的 Trace 调试功能,更加强大
    • 发生异常时,也会显示异常信息
  • 关闭调试也可以显示简要的错误信息
    1. 关闭调试模式:APP_DEBUG = false
    2. 在根目录下 config/app.php 中开启错误信息展示:'show_error_msg' => true

(6)配置文件

  • 配置文件有两种形式

    1. .env 文件:适合本地
      • .env 中的环境变量用于本地开发测试,部署后会被忽略
    2. 根目录下的 config 目录:适合部署
  • 获取配置文件的值

    以下内容在根目录下的 app/controller/Index.php 中的 config 函数中进行编辑,通过访问 /index.php/index/config 进行查看

    1. 对于 .env 文件

      use think\facade\Env;
      class Index extends BaseController
      {
          // ...
          public function config() {
      		return Env::get( 'database .hostname');
          }
      }
      
    2. 对于 config 文件

      use think\facade\Config;
      class Index extends BaseController
      {
          // ...
          public function config() {
      		return Config::get('database.connections.mysql.hostname');
          }
      }
      
  • 也可以判断这两种文件的配置是否存在

    echo Env::has('database .hostname');
    echo Config::has('database.connections.mysql.hostname');
    
  • .env 的优先级高于 config

    • 当 config 无法从 .env 中获取环境变量时会使用自己的环境变量
    • 当进入部署环境后,.env 会被忽略,此时以 config 的环境变量为主

(7)URL 解析

TP 框架非常多的操作都是通过 URL 来实现的

  • URL 结构

    • 单应用:http://hostname:port/(public/)index.php/(控制器/)(操作/)(参数/)(值/)...
    • 多应用:http://hostname:port/(public/)index.php/(应用名/)(控制器/)(操作/)(参数/)(值/)...
  • index.php:根目录下的 public/index.php

  • 应用名:多应用目录 app 下的单应用名

  • 控制器:app 目录下的控制器目录 controller 中的控制器类

    • 如:控制器 Index.php

      <?php
      namespace app\controller;
      
      use app\BaseController;
      
      class Index extends BaseController
      {
          public function index()
          {
              return 'xxx';
          }
      
          public function hello($name = 'ThinkPHP6')
          {
              return 'hello,' . $name;
          }
      }
      
  • 操作:控制器中的方法

    • 如:index 默认免写、hello 必写
  • 为省略 URL 中的 index.php,修改 public/.htaccess

    <IfModule mod_rewrite.c>
     Options +FollowSymlinks -Multiviews
      RewriteEngine On
      RewriteCond $1 !^(index.php|images|robots.txt)
      RewriteCond %{REQUEST_FILENAME} !-d
      RewriteCond %{REQUEST_FILENAME} !-f
      RewriteRule ^(.*)$ index.php?/$1 [QSA,PT,L]
    </IfModule>
    
  • 举例:在 app/controller 下新建 Test.php 并写入以下代码后访问 /test/hello/value/world

    <?php
    
    namespace app\controller;
    
    use app\BaseController;
    
    class Test extends BaseController
    {
        public function index()
        {
            return 'xxx';
        }
    
        public function hello($value = '')
        {
            return 'hello, ' . $value;
        }
    }
    

(8)控制器

a. 概述

  • 控制器(controller)的文件一般存放在 controller 目录下,存放的目录可以通过 config/route.php 中的 'controller_layer' => '[目录名]'进行更改

  • 类名和文件名大小写保持一致,并采用驼峰式 (首字母大写)

    // filename: Test.php
    namespace app\controller;
    class Test{...}
    
    // filename: UserController.php
    namespace app\controller;
    class UserController{...}
    
  • 通过 URL 访问控制器类中的方法,假设类中均含有 index()hello() 方法

    • Test.php

      /test//test/hello/

    • UserController.php

      /usercontroller//usercontroller/hello/
      /user_controller//user_controller/hello/

  • 为避免引入同类名时的冲突,可以在 config/route.php 中的 'controller_suffix' => true 设置控制器后缀,此时 Test.php 以及相关类名须要重命名为 TestController.php

b. 渲染输出

  • TP 采用方法内 return 返回的方式直接输出

  • 如果需要 JSON 格式的输出可以采用 json 函数

    public function index()
    {
        $datas = array('a'=>1, 'b'=>2, 'c'=>3);
        return json($datas);
    }
    
  • 不推荐使用 dieexit 等 PHP 方法中断代码执行,推荐使用助手函数 halt

    halt("中断调试");
    

c. 基础控制器

  • 创建控制器后可以通过继承基础控制器(BaseController)获取其中的方法

    // 返回实际路径
    return $this->app->getBasePath();
    
    // 返回当前方法名
    return $this->request->action();
    
  • 基础控制器仅仅提供了控制器验证功能,并注入了 think\Appthink\Request

d. 空控制器

在单应用模式下,可以定义一个 Error 控制器类来提示空控制器

<?php

namespace app\controller;

class Error
{
    public function index() {
        return "错误:当前为空控制器";
    }
}

e. 多级控制器

  • 在控制器 controller 目录下再建立目录并创建控制器称为多级控制器

  • 举例:在 controller 目录下新建 group 目录,在其中创建控制器 User.php,写入以下代码后访问 /group.user/

    <?php
    
    namespace app\controller\group;
    
    class User
    {
        public function index()
        {
            return "Group User Index";
        }
    
        public function hello()
        {
            return "Group User Hello";
        }
    }
    

0x02 数据库

(1)连接数据库

  • TP 采用内置抽象层将不同的数据库操作进行封装处理,数据抽象层基于 PDO 模式,无须针对不同的数据库编写相应的代码

  • .env 和 config/database.php 可以设置数据库连接信息

    • .env

      [DATABASE]
      TYPE = mysql
      HOSTNAME = 127.0.0.1
      DATABASE = thinkphp
      USERNAME = root
      PASSWORD = root
      HOSTPORT = 3306
      CHARSET = utf8
      DEBUG = true
      
    • database.php

      // 数据库类型
      'type'            => env('database.type', 'mysql'),
      // 服务器地址
      'hostname'        => env('database.hostname', '127.0.0.1'),
      // 数据库名
      'database'        => env('database.database', 'thinkphp'),
      // 用户名
      'username'        => env('database.username', 'root'),
      // 密码
      'password'        => env('database.password', 'root'),
      // 端口
      'hostport'        => env('database.hostport', '3306'),
      // 数据库连接参数
      'params'          => [],
      // 数据库编码默认采用utf8
      'charset'         => env('database.charset', 'utf8'),
      // 数据库表前缀
      'prefix'          => env('database.prefix', 'tp_'),
      
  • 测试数据库连接

    • 创建数据库 thinkphp 以及表 tp_user 并填充一些数据

    • 创建新的控制器 DatabaseTest.php,写入以下代码并访问 /databasetest/

      <?php
      
      namespace app\controller;
      
      use think\facade\Db;
      
      class DatabaseTest
      {
          public function index()
          {
              $user = Db::table('tp_user')->select();
              return json($user);
          }
      }
      
    • 如果配置了多个数据库,则使用下述代码

      $user = Db::connect('mysql')->table('tp_user')->select();
      return json($user);
      

(2)模型初探

  • 在 app 目录下创建目录 model,在其中创建 User.php 模型类并写入以下代码

    <?php
    
    namespace app\model;
    
    use think\Model;
    
    class User extends Model
    {
        protected $connection = 'mysql';
    }
    
  • User 类继承模型基类即可实现数据调用

  • 控制器端调用方式

    <?php
    
    // ...
    use app\model\User;
    
    class Test extends BaseController
    {
        // ...
        public function getUser()
        {
            $user = User::select();
            return json($user);
        }
    }
    

(3)数据查询

a. 单数据查询

  • 使用 Db::table() 方法必须指定包括前缀的完整数据表
  • 如果希望只查询一条数据,可以使用 find() 方法,需指定 where 条件
    • 如:Db::table('tp_user')->where('id', 2)->find()
  • 使用 Db::getLastSql() 方法可以得到最近一条 SQL 查询的原生语句
    • 返回内容示例:SELECT * FROM tp_user' LIMIT 1
    • 如果没有查询到任何值,则返回 null
  • 使用 findOrFail() 方法同样可以查询一条数据
    • 如:Db::table('tp_user')->where('id', 1)->findOrFail()
    • 如果没有数据,则抛出一个异常
  • 使用 findOrEmpty()方法也可以查询一条数据
    • 如:Db::table('tp_user')->where('id', 1)->findOrEmpty()
    • 如果没有数据,则返回一个空数组

b. 数据集查询

  • 想要获取多列数据,可以使用 select() 方法

    • 如:Db::table('tp_user')->select()
      SELECT * FROM tp_user
  • 多列数据在查询不到任何数据时返回空数组,使用 selectOrFail() 抛出异常

    • 如:Db::table('tp_user')->where('id', 1)->selectorFail()
  • select() 方法后再使用 toArray() 方法,可以将数据集对象转化为数组

    $user = Db::table('tp_user')->select()->toArray();
    dump($user);
    
  • 当在数据库配置文件中设置了前缀,那么我们可以使用 name() 方法忽略前缀

    • 如:Db::name('user')->select()

c. 其他查询

  • 通过 value() 方法,可以查询指定字段的值(单个),没有数据返回 null

    • 如:Db: :name('user')->where('id', 27)->value('username')
  • 通过 colunm() 方法,可以查询指定列的值(多个),没有数据返回空数组

    • 如:Db::name('user')->column('username')
  • 可以指定 id 作为列值的索引

    • Db::name('user')->column( 'username','id')
  • 为了避免内存处理太多数据出错,可以使用 chunk() 方法分批处理数据

    • 如:每次只处理 100 条,处理完毕后,再读取 100 条继续处理

      Db::table('tp_user')->chunk(3, function($users) {
          foreach ($users as $user) {
              dump($user);
          }
          echo 1;
      });
      
  • 可以利用游标查询功能,可以大幅度减少海量数据的内存开销,它利用了 PHP 生成器特性。每次查询只读一行,然后再读取时,自动定位到下一行继续读取

    $cursor = Db::table('tp_user')->cursor();
    foreach ($cursor as $user) {
        dump($user);
    }
    

(4)链式查询

a. 概述

  • 通过指向符号 -> 多次连续调用方法称为链式查询

  • 当使用方法 Db::name('user') 时就返回查询对象(Query),即可使用连缀数据库对应的方法。而每次执行一个数据库查询方法时(如:where())还将返回查询对象(Query)。只要还是数据库对象,那么就可以一直使用指向符号 -> 进行链式查询。最后利用方法 find()select() 等返回数组(Array)或数据集对象(Colletion)

  • find()select() 是结果查询方法,并不是链式查询方法

  • 可以把对象实例保存下来,再进行反复调用,从而避免反复静态创建示例造成的浪费

    $userQuery = Db::name('user');
    $dataFind = $userQuery->where('id', 2);
    $dataSelect = $dataFind->select();
    return json($dataSelect);
    
  • 当同一个对象实例第二次查询后,会保留第一次查询的值

    $userQuery = Db::name('user');
    $data1 = $userQuery->order('age')->select();
    $data2 = $userQuery->select();
    return Db::getLastSql();
    // 返回的结果:SELECT * FROM `tp_user` ORDER BY `age`
    
  • 使用 removeOption() 方法,可以清理掉上一次查询保留的值

    $userQuery = Db::name('user');
    $data1 = $userQuery->order('age')->select();
    $data2 = $userQuery->select();
    $userQuery->removeOption('order')->select();
    return Db::getLastSql();
    // 返回的结果:SELECT * FROM `tp_user`
    

b. where() 方法

  • 表达式查询就是 where() 方法的基础查询方式

  • 关联数组查询:通过键值对来数组键值对匹配的查询方式称为

    Db::name('user')->where([
        'name' => '张三',
        'age' => 17
    ])->select();
    
  • 索引数组查询:通过数组里的数组拼装方式来查询

    Db::name('user')->where([
        ['name', '=', '张三'],
        ['age', '=', 17]
    ])->select();
    
  • 将复杂的数组组装后,通过变量传递,将增加可读性

    $map[] = ['name', '=', '张三'];
    $map[] = ['age', '=', 17];
    Db::name('user')->where($map)->select();
    
  • 字符串形式传递,简单粗暴的查询方式,whereRaw() 支持复杂字符串格式

    Db::name('user')->whereRaw( 'sex="男" AND age IN (16, 17)')->select();
    
  • 如果 SQL 查询采用了预处理模式,比如 id=:id,也能够支持

    Db::name('user')->whereRaw('id=:id', ['id'=>19])->select();
    

c. field() 方法

  • 使用 field() 方法,可以指定要查询的字段

    Db::name('user')->field('id, name, age')->select();
    Db::name('user')->field(['id', 'name', 'age'])->select();
    
  • 使用 field() 方法,给指定的字段设置别名

    Db::name('user')->field('id, name as username')->select();
    
    Db::name('user')->field(['id', 'name'=>'username'])->select();
    
  • fieldRaw() 方法里,可以直接给字段设置 MySQL 函数

    Db::name('user')->fieldRaw('id, SUM(age)')->select();
    
  • 使用 field(true) 方法,可以显式的查询获取所有字段,而不是 *

    Db::name('user')->field(true)->select();
    
  • 使用 withoutField() 方法,可以屏蔽掉想要不显示的字段

    Db::name('user')->withoutField('id')->select();
    
  • 使用 field() 方法在新增时,可以验证字段的合法性;

    Db::name('user')->field('id, name, age')->insert($data);
    

d. alias() 方法

  • 使用 alias() 方法可以给数据库起别名

    Db::name('user')->alias('username')->select();
    

e. limit() 方法

  • 使用 limit() 方法,可以限制获取输出数据的个数

    Db::name('user')->limit(5)->select();
    
  • 分页模式

    • 如:从第 3 条开始显示 5 条数据

      Db::name('user')->limit(2, 5)->select();
      
  • 手动分页

    // 第一页
    Db::name('user')->limit(0, 5)->select();
    
    // 第二页
    Db::name('user')->limit(5, 5)->select();
    

f. page() 方法

  • page() 分页方法,优化了 limit() 方法,无需计算分页条数

    // 第一页
    Db::name('user')->page(1, 5)->select();
    
    // 第二页
    Db::name('user')->page(2, 5)->select();
    

g. order() 方法

  • 使用 order() 方法,可以指定排序方式,第二参数默认为 asc

    Db::name('user')->order('id')->select();
    
    Db::name('user')->order('age', 'desc')->select();
    
  • 支持数组的方式,对多个字段进行排序

    Db::name('user')->order([
        'id' => 'desc',
        'age' => 'asc'
    ])->select();
    
  • 使用 orderRaw() 方法,支持排序时指定 MySQL 函数

    Db::name('user')->orderRaw('FIELD(name, "张三") DESC')->select();
    

h. group() 方法

  • 使用 group() 方法,可以进行指定字段的总和统计

    Db::name('user')->fieldRaw('sex', 'SUM(age)')->group('sex')->select();
    
  • 也可以进行多字段分组统计

    Db::name('user')->fieldRaw('sex', 'SUM(age)')->group('age', 'sex')->select();
    

i. having() 方法

  • 使用 group() 分组之后,再使用 having() 进行筛选

    Db::name('user')->fieldRaw('sex', 'SUM(age)')
    				->group('sex')
    				->having('SUM(age)>100')
    				->select();
    

(5)数据新增

a. 单数据

  • 使用 insert() 方法可以向数据表添加一条数据,更多的字段采用默认

    • 如果新增成功,insert() 方法会返回一个 1
    $data = [
        'name' => "王五",
        'age' => 17
    ];
    return Db::name('user')->insert($data);
    
  • 如果你添加一个不存在的字段数据,会抛出一个异常 Exception。如果想强行新增抛弃不存在的字段数据,则使用 strick(false)方法,忽略异常

    return Db::name('user')->strict(false)->insert($data);
    
  • 如果我们采用的数据库是 mysql,可以支持 replace 写入。insert 和 replace 写入的区别,前者表示表中存在主键相同则报错,后者则修改

    Db::name('user')->replace()->insert($data);
    return Db::getLastSql();
    
  • 使用 insertGetId() 方法,可以在新增成功后返回当前数据 ID

    return Db::name('user')->insertGetId($data);
    

b. 多数据

  • 使用 insertAll() 方法,可以批量新增数据,但要保持数组结构一致

    $datas = [
        [
            'name' => "赵六",
            'age' => 22,
        ],
        [
            'name' => "田七",
            'age' => 20
        ]
    ];
    return Db::name('user')->insertAll($datas);
    
  • 批量新增也支持 replace() 方法,添加后改变成 replace into

    Db::name('user')->replace()->insertAll($datas);
    

c. 使用 save() 方法新增

  • save() 方法是一个通用方法,可以自行判断是新增还是修改(更新)数据。save() 方法判断是否为新增或修改的依据为,是否存在主键,不存在即新增

    Db::name("user")->save($data);
    

(6)数据修改

  • 使用 update() 方法来修改数据,修改成功返回影响行数,没有修改返回 0

    $data = [
        'age' => 21
    ];
    return Db::name('user')->where('name', '赵六')->update
    
  • 如果修改数据包含了主键信息,比如 id,那么可以省略掉 where 条件

    $data = [
        'id' => 4,
        'age' => 20,
    ];
    return Db::name('user')->update($data);
    
  • 如果想让一些字段修改时执行 SQL 函数操作,可以使用 exp() 方法实现

    Db::name('user')->where('id', 4)
        			->exp('name', 'UPPER(name)')
        			->update();
    
  • 如果要自增/自减某个字段,可以使用 inc/dec 方法,并支持自定义步长

    Db::name('user')->where('id', 4)
        			->inc('age')
        			->update();
    
    Db::name('user')->where('id', 4)
        			->dec('age', 2)
        			->update();
    
  • 使用 raw() 方法也可以实现上述内容

    Db::name('user')->where('id', 4)
        			->update([
                        'name' => Db::raw('UPPER(name)'),
                        'age' => Db::raw('age + 1')
                    ]);
    
  • 使用 save() 方法进行修改数据,这里必须指定主键才能实现修改功能

    Db::name('user')->where('id', 4)->save(['age' => '19']);
    

(7)数据删除

  • 极简删除可以根据主键直接删除,删除成功返回影响行数,否则返回 0

    Db::name('user')->delete(5);
    
  • 根据主键,还可以删除多条记录

    Db::name('user')->delete([3, 4]);
    
  • 正常情况下,通过 where() 方法来删除

    Db::name('user')->where('id', 2)->delete();
    
  • 通过 true 参数删除数据表所有数据

    Db::name('user')->delete(true);
    

(8)查询表达式

a. 比较查询

  • 查询表达式支持大部分常用的 SQL 语句,语法格式如下 where('字段名', '查询表达式', '查询条件');

    • 举例:查询 id=6 的数据

      Db::name('user')->where('id', 6)->find();
      
      Db::name('user')->where('id', '=', 6)->find();
      
  • 使用 <>><>=<= 可以筛选出各种符合比较值的数据列表

    • 举例:筛选出 id<7 的数据

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

b. 模糊查询

  • 使用 like 表达式进行模糊查询,还可以支持数组传递进行模糊查询

    Db::name('user')->where('age', 'like', '1%')->select();
    
    Db::name('user')->where('age', 'like', ['1%', '_8'], 'and')->select();
    
  • like 表达式具有两个快捷方式 whereLike()whereNotLike()

    Db::name('user')->whereLike('age', '1%')->select();
    
    Db::name('user')->whereNotLike('age', '_8')->select();
    

c. 区间查询

  • 使用 between 表达式进行区间查询

    Db::name('user')->where('age', 'between', '16, 17')->select();
    
    Db::name('user')->where('age', 'between', [16, 17])->select();
    
  • between 表达式具有两个快捷方式 whereBetween()whereNotBetween()

    Db::name('user')->whereBetween('age', '16, 17')->select();
    
    Db::name('user')->whereNotBetween('age', '16, 17')->select();
    

d. 集合查询

  • 使用 in 表达式进行集合查询

    Db::name('user')->where('age', 'in', '16, 17')->select();
    
    Db::name('user')->where('age', 'in', [16, 17])->select();
    
  • in 表达式具有两个快捷方式 whereIn()whereNotIn()

    Db::name('user')->whereIn('age', '16, 17')->select();
    
    Db::name('user')->whereNotIn('age', '16, 17')->select();
    

e. Null 查询

  • 查询值为 null 的数据

    Db::name('user')->where('name', 'null')->select();
    
    Db::name('user')->where('name', 'not null')->select();
    
  • 两个快捷方式 whereNull()whereNotNull()

    Db::name('user')->whereNull('name')->select();
    
    Db::name('user')->whereNotNull('name')->select();
    

f. EXP 查询

  • 使用 exp 可以自定义字段后的 SQL 语句

    Db::name('user')->where('age', 'exp', 'IN (16, 17)')->select();
    
  • exp 的快捷方式 whereExp()

    Db::name('user')->whereExp('age', 'IN (16, 17)')->select();
    

(9)时间查询

  • 传统方式:使用 ><>=<= 或 between 关键字来查询

    Db::name('user')->where('create_time', '>', '2023-1-1')->select();
    
    Db::name('user')->where('create_time', 'between', ['2023-1-1', '2023-2-28'])->select();
    
    Db::name('user')->where('create_time', 'not between', ['2023-1-1', '2023-2-28'])->select();
    
  • 快捷方式:使用 whereTime()whereBetweenTime()whereNotBetweenTime()

    whereTime() 默认使用 >,可以省略

    Db::name('user')->whereTime('create_time', '<', '2023-1-1')->select();
    
    Db::name('user')->whereBetweenTime('create_time', '2023-1-1', '2023-2-28')->select();
    
    Db::name('user')->whereNotBetweenTime('create_time', '2023-1-1', '2023-2-28')->select();
    
  • 固定查询:

    • 使用 whereYear() 方法查询今年、去年、指定年的数据

      Db::name('user')->whereYear('create_time')->select();
      
      Db::name('user')->whereYear('create_time', 'last year')->select();
      
      Db::name('user')->whereYear('create_time', '2021')->select();
      
    • 使用 whereMonth() 方法查询当月、上个月、指定月的数据

      Db::name('user')->whereMonth('create_time')->select();
      
      Db::name('user')->whereMonth('create_time', 'last month')->select();
      
      Db::name('user')->whereMonth('create_time', '2021-1')->select();
      
    • 使用 whereDay() 方法查询今天、昨天、指定天的数据

      Db::name('user')->whereDay('create_time')->select();
      
      Db::name('user')->whereDay('create_time', 'last day')->select();
      
      Db::name('user')->whereDay('create_time', '2021-1-1')->select();
      
  • 其他查询:

    • 查询指定时间的数据

      • 举例:两小时内的数据

        Db::name('user')->whereTime('create_time', '-2 hours')->select();
        
    • 查询两个时间字段时间有效期的数据

      • 举例:开始到结束的期间

        Db::name('user')->whereBetweenTimeField('start_time', 'end_time')->select();
        

(10)聚合查询

  • 使用 count() 方法,求出所查询数据的数量。count() 可设置指定 id,对于有空值(Null)的情况,不会被计算数量

    Db::name('user')->count();
    
    Db::name('user')->count('id');
    
  • 使用 max() 方法,求出所查询数据字段的最大值

    Db::name('user')->max('age');
    
    • 如果求出的值不是数值,则通过第二参数强制转换

      Db::name('user')->max('age', false);
      
  • 使用 min() 方法,求出所查询数据字段的最小值,也可以强制转换

    Db::name('user')->min('age');
    
  • 使用 avg() 方法,求出所查询数据字段的平均值

    Db::name('user')->avg('age');
    
  • 使用 sum() 方法,求出所查询数据字段的总和

    Db::name('user')->sum('age');
    

(11)子查询

  • 使用 fetchSql() 方法,可以设置不执行 SQL,而返回 SQL 语句,默认 true

    Db::name('user')->fetchSql(true)->select();
    
  • 使用 buildSql() 方法,也是返回 SQL 语句,不需要再执行 select(),且有括号

    Db::name('user')->buildSql(true);
    
  • 使用闭包的方式执行子查询

    Db::name('user')->where('uid', 'in', function ($query) {
        $query->name('user_course')->where('cid', 1)->field('uid');
    })->select();
    

(12)原生查询

  • 使用 query() 方法,进行原生 SQL 查询,适用于读取操作,SQL 错误返回 false

    Db::query('select * from tp_user');
    
  • 使用 execute() 方法,进行原生 SQL 更新写入等,SQL 错误返回 false

    Db::execute('update tp_user set age=19 where id=6');
    

(13)高级查询

  • 使用 |(OR)或 &(AND)来实现 where 条件的高级查询,where 支持多个连缀

    Db::name('user')->where('name|nickname', 'like', '张%')
        			->where('age&id', '>', 0)
        			->select();
    
  • 条件字符串复杂组装,比如使用 exp 了,就使用 raw() 方法

    Db::name('user')->where('age', 'exp', Db::raw('>18'))->select();
    
  • 加上一个中括号可以提高处理优先级

    Db::name('user')->where(['age', '>', 18])
        			->where('name', 'like', '张%')
        			->select();
    
  • 闭包查询可以连缀,会自动加上括号,如果是或逻辑,可以使用 whereOr() 方法

    Db::name('user')->where(function ($query) {
        $query->where('age', '>', 18);
    })->whereOr(function ($query) {
        $query->where('name', 'like', '张%');
    })->select();
    
  • 对于比较复杂或你不知道如何拼装 SQL 条件,那么就直接使用 whereRaw() 即可,whereRaw() 方式也支持参数绑定操作

    Db::name('user')->whereRaw('(age > 18)', [
        'name' => '张%',
        'sex' => '男'
    ])->select();
    

(14)快捷查询

  • 系统封装了很多 where 方法的快捷方式

    名称 说明
    whereOr() 或查询
    whereXor() 异或查询
    whereNull() 空值查询
    whereNotNull() 非空查询
    whereIn() 集合查询
    whereNotIn() 非集合查询
    whereBetween() 区间查询
    whereNotBetween() 非区间查询
    whereLike() 模糊查询
    whereNotLike() 非模糊查询
    whereExists() 存在查询
    whereNotExists() 不存在查询
    whereExp() 表达式查询
    whereColumn() 比较两个字段
  • 系统还针对字段查询提供了几个方便查询的快捷方式

    • whereFieldName() 方法用于查询某个字段的值,其中 FileName 是字段名

      Db::name('user')->whereUserName('张三')->find();
      
    • getByFieldName() 方法,查询某个字段的值,该方法只能查询一条结果

      Db::name('user')->getUserName('张三')->find();
      
    • getFieldByFieldName() 方法,通过查询得到某个指定字段的单一值

      Db::name('user')->getFieldUserName('张三', 'age');
      
  • when() 可以通过条件判断,执行闭包里的分支查询

    Db::name('user')->when(false, function ($query) {
    	$query->where('age', '>', 18);
    }, function ($query) {
    	$query->where('name','like', '张%');
    })->select()
    

(15)事务处理

  • 数据库的表引擎需要是 InnoDB 才可以使用

  • 事务处理需要执行多个 SQL 查询,数据是关联恒定的。如果成功一条查询,改变了数据,而后一条失败,则前面的数据回滚

  • 系统提供了两种事务处理的方式:

    • 自动处理,出错自动回滚

      Db::transaction(function () {
          Db::name('user')->where('name', '张三')->save(['age' => Db::raw('age + 1')]);
          Db::name('user')->where('name', '李四')->save(['age' => Db::raw('age - 1')]);
      });
      
    • 手动处理,基本和原生处理类似,可以自行输出错误信息

      Db::startTrans();
      try {
          Db::name('user')->where('name', '张三')->save(['age' => Db::raw('age + 1')]);
          Db::name('user')->where('name', '李四')->save(['age' => Db::raw('age - 1')]);
          Db::commit();
      } catch (\Exception $exception) {
          echo "执行失败,开始回滚";
          Db::rollback();
      }
      

(16)获取器

  • 获取器:将数据的字段进行转换处理再进行操作

  • 举例:在获取数据列表的时候,将获取到的内容全部大写

    $user = Db::name('user')->withAttr('name', function ($value, $data) {
    	return strtoupper($value);
    })->select();
    return json($user);
    

(17)数据集

  • 数据集是当查询后的结果集,它是 think\Collection 类型,和数组一样

  • 额外提供了一些方法

    名称 说明
    isEmpty() 判断是否为空
    toArray() 转换为数组
    all() 返回所有数据
    merge() 合并其他数据
    diff() 比较数组并返回差集
    flip() 交换数据中的键和值
    intersect() 比较数组并返回交集
    keys() 返回所有键
    pop() 删除最后一个元素
    shift() 删除第一个元素
    unshift() 在开头插入一个元素
    push() 在结尾插入一个元素
    reduce() 通过使用用户自定义函数以字符串返回数组
    reverse() 倒序
    chunk() 数据分隔为多个数据块
    each() 给数据的每个元素执行回调
    filter() 用回调函数过滤数据中的元素
    column() 返回数据指定列
    sort() 排序
    order() 按指定字段排序
    shuffle() 打乱
    slice() 截取
    map() 用回调函数处理数组中的元素
    where() 根据字段条件过滤数组中的元素

0x03 模型

21~39 pass

(1)定义模型

  • 定义一个和数据库表相匹配的模型

    <?php
    
    namespace app\model;
    
    use think\Model;
    
    class User extends Model {...}
    
  • 模型会自动对应数据表并存在自己的命名规则

  • 模型类需要去除表前缀 tp_ 并采用驼峰命名(首字母大写)

    数据表名 模型类名
    tp_user User
    tp_user_type UserType
  • 创建空模型后可以在控制器中调用,可以直接使用模型的名称 User::* 调用查询方法 select()

  • 可以开启应用类后缀来避免模型类名与 PHP 关键字冲突

  • 设置 $name 属性指定数据表

    protected $name = 'user';
    

(2)设置模型

  • 默认主键为 id,可以设置其他主键

    protected $pk = 'uid';
    
  • 从控制器端调用模型操作,如果和控制器类重名,则可以设置别名

    use app\model\User as UserModel;
    
  • 在模型定义中可以设置其他数据表

    protected $table = 'tp_other';
    
  • 模型和控制器一样的有初始化,模型初始化必须设置 static 静态方法

    protected static function init()
    {
        parent::init();
        echo 'Init User Model...';
    }
    

(3)新增

  • 使用实例化的方式添加一条数据,共两种实例化方法

    $user = new UserModel();
    
    $user = new \app\model\User();
    
  • 设置需要新增的数据并使用 save() 方法写入数据库

    $user->name = 'Alex';
    $user->age = 18;
    $user->save();
    
    • 可以使用数组传递

      $user->save([
          'name' => 'Alex',
          'age' => 18
      ]);
      
  • 使用 allowField() 方法设置允许要写入的字段,此时其他字段无法被写入

    $user->allowField(['name', 'age'])->save(...);
    
  • 使用 replace() 方法实现 REPLACE INTO 的新增方式

    $user->replace()->save();
    
  • 新增成功后使用 $user->id 可以获得自增 id(前提主键是 id

    echo $user->id;
    
  • 使用 saveAll() 方法可以批量新增数据

    $datas = [
        [
            'name' => 'Bob',
            'age' => 17
        ],
        [
            'name' => 'Charles',
            'age' => 19
        ]
    ];
    dump($user->saveAll($datas));
    
  • 使用 create() 静态方法可以创建要新增的数据

    $user = UserModel::create([
        'name' => 'David',
        'age' => 20
    ], ['name'], false);
    

    参数说明:

    1. 参数一是新增数据数组,必填
    2. 参数二是允许写入的字段
    3. 参数三是是否选择 replace 写入,否则使用 insert 写入

(4)删除

  • 使用 find() 方法通过主键查询到想要删除数据,再通过 delete() 方法将数据删除

    $user = UserModel::find(1);
    $user->delete();
    
  • 使用 destroy() 静态方法通过主键删除数据

    UserModel::destroy(1);
    
    • 可以批量删除

      UserModel::destroy([1, 2, 3]);
      
  • 可以通过数据库类的查询条件删除

    UserModel::where('id', '=', 1)->delete();
    
  • 使用闭包的方式进行删除

    UserModel::destroy(function ($query) {
        $query->where('id', '=', 1);
    });
    

(5)更新

  • 使用 find() 方法通过主键查询到想要修改数据,再通过 save() 方法保存修改

    $user = UserModel::find(1);
    $user->name = 'Bob';
    $user->age = 18;
    $user->save();
    
    • 也可以通过 where() 方法结合 find() 方法的查询条件获取数据并修改

      $user = UserModel::where('name', 'Bob')->find();
      $user->name = 'Charles';
      $user->age = 18;
      $user->save();
      
    • save() 方法只会更新变化的数据,如果提交的数据没有变化则不更新

  • 如果需要强制更新数据(即使数据一样)可以使用 force() 方法

    $user->force()->save();
    
  • Db::raw() 执行 SQL 函数的方式依旧有效

    $user->age = Db::raw('age+1');
    
  • 使用 allowField() 方法库设置允许更新的字段

    $user->allowField(['name', 'age'])->save(...);
    
  • 使用 saveAll() 方法可以批量修改数据

    $datas = [
        [
            'id' => 1,
            'name' => 'Bob',
            'age' => 17
        ],
        [
            'id' => 2,
            'name' => 'Charles',
            'age' => 19
        ]
    ];
    $user->saveAll($datas);
    
    • 只能通过主键进行更新
  • 使用 update() 静态方法进行更新

    UserModel::update([
        'id' => 1,
        'name' => 'Alex'
    ]);
    
    • 在第二参数单独指定主键

      UserModel::update([
          'name' => 'Alex'
      ], ['id' => 1]);
      
    • 在第三参数指定允许更新的字段

      UserModel::update([
          'name' => 'Alex'
      ], ['id' => 1], ['name']);
      
  • 模型的新增和修改都是 save() 进行执行的,采用了自动识别体系来完成,实例化模型后调用 save() 方法表示新增,查询数据后调用 save() 表示修改,如果在 save() 传入更新修改条件后也表示修改

(6)查询

  • 使用 find() 方法通过主键查询到想要的数据,如果数据不存在则返回 null

    $user = UserModel::find(1);
    return json($user);
    
  • 可以使用 where() 方法进行条件筛选查询数据

    $user = UserModel::where('name', 'Alex')->find();
    return json($user);
    
  • 使用 findOrEmpty() 方法对于数据不存在返回空模型,使用 isEmpty()方法来判断是否为空模型

    $user = UserModel::findOrEmpty(1000);
    if ($user->isEmpty()) {
    	echo 'Empty Model';
    }
    
  • 使用 select([]) 方法可以查询多条指定主键 id 的字段,不指定就是所有字段

    $user = UserModel::select([19, 20, 21]);
    foreach ($user as $key=>$obj) {
        echo $obj->name;
    }
    
  • 模型方法也可以使用 where() 等连缀查询方法,和数据库查询方式一样

    $user = UserModel::where('age', 18)
        ->limit(5)
        ->order('id', 'desc')
        ->select();
    
  • 获取某个字段 value() 或某个列 column() 的值

    UserModel::where('id', 1)->value('name');
    
    UserModel::whereIn('id', [1, 2])->column('name');
    
  • 模型支持动态查询

    UserModel::getByName('Alex');
    UserModel::getByAge(18);
    
  • 模型支持聚合查询

    UserModel::max('age');
    
  • 使用 chunk() 方法可以分批处理数据

    UserModel::chunk(5, function ($users) {
        foreach($users as $user) {
            echo $user->name;
        }
        echo '<br />----------<br />'
    })
    
  • 可以利用游标查询功能,可以大幅度诚少海量数据的内存开销,它利用了 PHP 生成器特性。每次查询只读一行,然后再读取时,自动定位到下一行继续读取

    foreach (UserModel::where('age',18)->cursor() as $user {
        echo $user->name;
    	echo '<br />------<br/ >';
    }
    

(7)字段设置

  • 模型的数据字段和表字段是对应关系,默认会自动获取,包括字段的类型,但是自动获取会导致增加一次查询,如果在模型中配置字段信息,会减少内存开销,可以在模型设置 $schema 字段,明确定义字段信息,字段需要对应表写完整

    protected $schema = [
        'id' => 'int',
        'name' => 'string',
        'age' => 'int'
    ];
    
  • 使用命令 php think optimize:schema 可以自动生成一个字段信息缓存,生成后的字段缓存文件在 runtime/schema 目录下

  • 如果需要模型和数据库 Db 类同时有效,则直接运用字段缓存文件即可,默认情况下字段缓存文件是关闭状态,需要在 config/database.php 开启

    'fields_cache' => true
    
  • 当数据获取到后可以用 -> 和数组方式单独获取数据

    $user = UserModel::find(1);
    echo $user->name;
    echo $user['age'];
    
  • 在模型端对数据进行整理并交由控制器直接调用

    • 模型

      public function getName($id)
      {
          $obj = $this->find($id);
          return $obj->getAttr('name');
      }
      
    • 控制器

      $user = new UserModel();
      return $user->getName(1);
      
  • 字段的赋值操作可以是 -> 和数组方式,作用就是提交给模型处理

    $user = new UserModel();
    $user->name = 'Alex';
    $user['age'] = 18;
    
  • 默认情况下,字段是严格区分大小写的,需要和数据表字段保持一致

  • 可以在模型属性 $strict 设置为 false,即可实现非严格字段

    并非肆无忌惮的不严格,只能首字母大写

(8)获取器

  • 获取器的作用是对模型实例的数据做出自动处理

  • 一个获取器对应模型的一个特殊方法,该方法类型为 public,方法名的命名规范为 getFieldAttr()

  • 举例:数据库表示状态 status 字段采用的是数值,页面上,需要输出 status 字段希望是中文,就可以使用获取器:

    • 在模型端创建一个公共方法

      public function getstatusAttr($value)
      {
          $status = [-1=>'删除', 0=>'禁用', 1=>'正常', 2=>'待审核'];
      	return $status[$value];
      }
      
    • 在控制器端直接输出数据库字段的值即可得到获取器转换的对应值

      $user = UserModel::find(1);
      return $user->status;
      
    • 除了 getFieldAttr() 中的 Field 可以是字段值,也可以是自定义的虚拟字段

      public function getNothingAttr($value, $data)
      {
          $dic = [-1=>'删除', 0=>'禁用', 1=>'正常', 2=>'待审核'];
      	return $dic[$data['status']];
      }
      
      $user = new UserModel();
      return $user->nothing;
      
    • 如果定义了获取器,则可以使用 getData() 方法获取原始值

      return $user->getData('status');
      
    • 使用 WithAttr() 方法可以在控制器端实现动态获取器

      $user = UserModel::WithAttr('name', function ($value) {
          return strtoupper($value);
      })->select();
      return json($user);
      
      • 如果同时定义了模型获取器和动态获取器,那么动态获取器优先级更高

(9)修改器

  • 修改器的作用是对模型设置对象的值进行处理,如对数据进行格式化、过滤、转换等处理

  • 修改器的命名规则为 setFieldAttr()

  • 举例:设置新增数据时,规定英文姓名都必须大写,修改器如下:

    public function setNameAttr($value)
    {
        return strtoupper($value);
    }
    
    • 除了新增会调用修改器,修改更新也会触发修改器
  • 修改器只对模型方法有效,对调用数据库的方法是无效的

(10)查询范围

  • 在模型端创建一个封装的查询或写入方法,方便控制器端等调用

  • 方法名规范:前缀 scope,后缀随意。调用时直接把后缀作为参数使用

  • 举例:封装一个筛选所有年龄为 18 的查询,并且只显示部分字段 5条

    public function scopeEighteen($query)
    {
        $query->where('age', 18)
            ->field('id,name,age')
            ->limit(5);
    }
    
  • 在控制器端直接调用并输出结果即可

    public function scope()
    {
        $result = UserModel::scope('eighteen')->select();
        return json($result);
    }
    
    • 使用助手函数:$result = UserModel::eighteen()->select();
  • 查询封装可以传递参数

    public function scopeName($query, $value)
    {
        $query->where('name', 'like', '%'.$value.'%');
    }
    
    $result = UserModel::scope('name', 'e')->select();
    return json($result);
    
    • 使用助手函数:$result = UserModel::name('Alex')->select();
  • 可以实现多个查询封装方法连缀调用

    public function scopeAge($query, $value)
    {
        $query->where('age', '>', $value);
    }
    
    $result = UserModel::scope('name', 'e')
        ->scope('age', 18)
        ->select();
    return json($result);
    
  • 查询范围只能使用 find()select() 两种方法

  • 全局范围查询是在此模型下无论如何查询都会加上全局条件

    //定义全局的查询范围
    protected $globalScope = ['age'];
    //全局范围
    public function scopeAge($query)
    {
        $query->where('age', '>', 18);
    }
    
  • 在定义了全局查询后,如果需要取消这个查询的所有全局查询,可以使用方法 UserModel::withoutGlobalScope()

    • 如果需要取消这个查询的部分全局查询,可以添加参数指定

      UserModel::withoutGlobalScope(['age']);
      

(11)搜索器

  • 搜索器是用于封装字段或搜索标识的查询表达式,类似查询范围

  • 一个搜索器对应模型的一个特殊方法,该方法类型为 public,方法名的命名规范为 searchFieldAttr()

  • 举例:封装一个姓名字符模糊查询,然后封装一个年龄限定查询

    • 在模型端,创建两个方法

      public function searchNameAttr($query, $value, $data)
      {
          $query->where('name', 'like', '%'.$value.'%');
      }
      
      public function searchCreateTimeAttr($query, $value, $data)
      {
          $query->whereBetween('age', $value[0], $value[1]);
      }
      
    • 在控制器端,通过 withSearch() 方法实现模型搜索器的调用

      $result = UserModel::withSearch(['name', 'age'], [
          'name' => 'e',
          'age' => [18, 35]
      ])->select();
      
      • 参数一用于现代搜索器的字段
      • 参数二是表达式值
    • 如需增加查询条件,则直接使用链式查询

      UserModel::withSearch(...)->where(...)->select();
      
    • 如需添加排序功能,则在模型端添加内容

      public function searchNameAttr($query, $value, $data)
      {
          $query->where('name', 'like', '%'.$value.'%');
          if (isset($data['sort'])) {
              $query->order($data['sort']);
          }
      }
      
      • 搜索器的参数三 $data,可以得到 withSearch() 方法的参数二的值
    • 字段也可以设置别名:'username' => 'un'

(12)数据集

  • 数据集是直接继承 collection 类,所以和数据库方式一样,数据集对象和数组操作方法一样,循环遍历、删除元素等

  • 判断数据集是否为空,我们需要采用 isEmpty() 方法

    $resut = UserModel::where('id', 1000)->select();
    if ($resut->isEmpty()) {
    	return 'Empty'
    }
    
  • 使用模型方法 hidden() 可以隐藏某个字段,使用 visible() 可以只显示某个字段,使用 append() 可以添加某个获取器字段,使用 withAttr() 对字段进行函数处理

    $result = UserModel::select();
    $result->hidden(['password'])->append(['nothing'])->withAttr('name'
    function ($value) {
        return strtoupper($value);
    });
    return json($result);
    

(13)时间戳

  • 在 database.php 中可以开启全局模型自动时间戳

    'auto_timestamp' => true,
    
  • 可以在模型中设置当前模型开启自动时间戳

    protected $autoWriteTimestamp = true;
    
  • 自动时间戳开启后会自动写入 create_timeupdate_time 两个字段,此时它们的默认类型都是 int,如果是时间类型可以进行两种方式的更改

    'auto_timestamp' => 'datetime',
    
    protected $autoWriteTimestamp = 'datetime';
    
  • 配置完毕后,当新增一条数据时,系统会自动写入时间,同理,当修改一条数据时,系统也会自动更新时间

  • 自动时间戳只能在模型下有效,数据库方法不可以使用

  • 如果创建和修改时间戳不是默认定义的,也可以自定义

    protected $createTime = 'create_at';
    protected $updateTime = 'update_at';
    
  • 如果业务中只需要 create_time 而不需要 update_time,可以关闭它

    protected $updateTime = false;
    
    • 也可以动态实现不修改 update_time

      $user->isAutoWriteTimestamp(false)->save();
      

(14)只读字段

  • 模型中可以设置只读字段,即无法被修改的字段设置

  • 举例:设置 nameage 不允许被修改

    protected $readonly = ['username', 'email'];
    
    • 除了在模型端设置,也可以动态设置只读字段

      $user->readonly(['username', 'email'])->save();
      
  • 只读字段只支持模型方式不支持数据库方式

(15)类型转换

  • 系统可以通过模型端设置写入或读取时对字段类型进行转换

  • 可以通过读取的方式来演示部分效果:在模型端设置需要类型转换的字段属性,属性值为数组

    protected $type = [
        'age' => 'integer',
        'status' => 'boolean',
        'create_time' => 'datetime:Y-m-d'
    ];
    
  • 数据库查询读取的字段很多都是字符串类型,我们可以转换其他类型

    类型 说明
    integer 整型
    float 浮点型
    boolean 布尔型
    array 数组
    object 对象
    serialize 序列化
    json JSON
    timestamp 时间戳
    datetime 日期

    类型转换还是会调用属性里的获取器等操作,编码时要注意这方面的问题

  • 当某个字段在开发项目版本升级中不再使用,可以设置为废弃字段

    protected $disuse = ['name', 'age'];
    
    • 设置废弃字段后,这个字段就不在查询数据列表里了

(16)JSON

a. 数据库 JSON

  • 数据库写入 JSON 字段直接通过数组的方式即可完成

    $data = [
        'name' => 'Alex',
        'age' => 18,
        'message' => ['name' => 'Alex', 'age' => 18]
    ];
    Db::name('user')->json(['message'])->insert($data);
    
  • 查询数据时需要设置 json() 方法从而正确转换 JSON 数据格式

    Db::name('user')->json(['message'])->find(1);
    
  • 可以将 JSON 字段里的数据作为查询条件

    $user = Db::name('user')->json(['message'])->where('list->name', 'Alex')->find();
    
  • 可以完全修改 JSON 数据

    $data['message'] = ['name'=>'Bob','age'=>19];
    Db::name('user')->json(['message'])->where('id', 1)->update($data);
    
  • 可以只修改 JSON 数据里的某一个项

    $data['message->name'] = 'Charles';
    Db::name('user')->json(['message'])->where('id', 1)->update($data);
    

b. 模型 JSON

  • 在模型端设置需要写入 JSON 字段

    protected $json = ['message'];
    
  • 使用模型方式去新增包含 JSON 数据的字段

    $user = new UserModel();
    $user->name = 'Alex';
    $user->age = 18;
    $user->message = ['name' => 'Alex', 'age' => 18];
    $user->save();
    
    • 也可以使用对象的方式

      $message = new \StdClass();
      $message->name = 'Alex';
      $message->age = 18;
      $user->message = $message;
      
  • 可以通过对象调用方式,直接获取 JSON 里面的数据

    $user = UserModel::find(1);
    return $user->message->name;
    
  • 通过 JSON 的数据查询可以获取一条数据

    $user = UserModel::where('message->name', 'Alex')->find();
    return $user->message->age;
    
  • 可以直接通过对象的方式更新修改 JSON 数据

    $user = UserModel::find(1);
    $user->message->name = 'Alex';
    $user->save();
    

(17)软删除

介于数据库软删除没有太多的可操作的方法,官方手册推荐使用模型软操作

  • 在模型端设置软删除的功能需要引入 SoftDelete,它是 trait

    use SoftDelete;
    protected $deleteTime = 'delete_time';
    
  • delete_time 默认设置的是 null,可以更改这个默认值

    //protected $defaultSoftDelete = 0;
    
  • 软删除和真实删除的方法如下,包括 destroy()delete()

    UserModel::destroy(1);
    UserModel::find(1)->delete();
    
  • 默认情况下,开启了软删除功能的查询,模型会自动屏蔽被软删除的数据

    $user = UserModel::select();
    return json($user);
    
  • 在开启软删除功能的前提下,使用 withTrashed() 方法可以取消屏蔽软删除的数据

    $user = UserModel::withTrashed()->select();
    return json($user);
    
  • 如果只需查询被软删除的数据,可以使用 onlyTrashed() 方法

    $user = UserModel::onlyTrashed()->select();
    return json($user);
    
  • 如果需要让某一条被软删除的数据恢复到正常数据,可以使用 restore() 方法

    $user = UserModel::onlyTrashed()->find();
    $user->restore();
    
  • 如果需要让一条软删除的数据真正删除,可以在恢复正常后使用 delete() 方法

    $user = UserModel::onlyTrashed()->find(1);
    $user->restore(1);
    $user->force()->delete();	// 或 UserModel::destory(1, true);
    

(18)事件

a. 数据库事件

  • 执行增删改查的时候可以触发一些事件来执行额外的操作,这些额外的操作事件,可以部署在构造方法里等待激活执行

  • 数据库事件方法为 Db::event('事件名', '执行函数')

    事件 说明
    before_select select 查询前回调
    before_find find 查询前回调
    after_insert insert 操作成功后回调
    after_update update 操作成功后回调
    after_delete delete 操作成功后回调
  • 在控制器端,事件一般可以写在初始化方法里

    public function initialize()
    {
        Db::event('before_select', function ($query) {
            echo 'before select';
        });
        
        Db::event('after_update', function ($query) {
            echo 'after update';
        });
    }
    

b. 模型事件

  • 模型支持的事件比数据库支持的要丰富的多

    事件 说明 方法名
    after_read 查询后 onAfterRead()
    before_insert 新增前 onBeforeInsert()
    after_insert 新增后 onAfterInsert()
    before_update 更新前 onBeforeUpdate()
    after_update 更新后 onAfterUpdate()
    before_write 写入前 onBeforeWrite()
    after_write 写入后 onAfterWrite()
    before_delete 删除前 onBeforeDelete()
    after_delete 删除后 onAfterDelete()
    before_restore 恢复前 onBeforeRestore()
    after_restore 恢复后 onAfterRestore()
  • 在模型端使用静态方法调用即可完成事件触发

    protected static function onAfterRead($query)
    {
        echo 'After reading';
    }
    

(19)关联模型

  • 关联模型是将表与表之间进行关联和对象化从而更高效的操作数据

  • 目前已有 tp_user 表,其主键为 id,需要新建一个附属表 tp_profile 来进行关联

    • 在 tp_profile 建立两个字段 user_idhobby,外键是 user_id
  • 创建 User 模型和 Profile 模型,User 模型需要关联 Profile 模型

    class User extends Model
    {
        public function profile()
        {
            return $this->hasOne(Profile::class, 'user_id');
        }
    }
    
    class Profile extends Model {}
    
  • 创建一个控制器 Grade.php 用于测试输出

    <?php
    
    namespace app\controller;
    
    use app\model\User as UserModel;
    
    class Grade
    {
        public function index()
        {
            $user = UserModel::find(1);
            return json($user->profile);
            // 或 return $user->profile->hobby;
        }
    }
    
  • 关联方式共 9 种

    关联方式 说明
    hasOne() 一对一
    belongsTo() 一对一
    hasMany() 一对多
    belongsToMany() 多对多
    hasOneThrough() 远程一对一
    hasManyThrough() 远程一对多
    morphTo() 多态
    morphOne() 多态一对一
    morphMany() 多态一对多
    • 上述案例中 User 模型采用了一对一关联
  • 对与 User 模型中的一对一关联,存在相对的反向关联,修改 Profile 模型类、User 模型类、Grade 控制器类

    <?php
    
    namespace app\model;
    
    use think\Model;
    
    class Profile extends Model
    {
        public function user()
        {
            return $this->belongsTo(User::class);
        }
    }
    
    <?php
    
    namespace app\model;
    
    use think\Model;
    
    class User extends Model {}
    
    <?php
    
    namespace app\controller;
    
    use app\model\Profile as ProfileModel;
    
    class Grade
    {
        public function index()
        {
            $profile = ProfileModel::find(1);
            return $profile->user->name;
        }
    }
    
  • 正反向关联对应表

    关联方式 关联关系 反向关联关系
    一对一 hasOne() belongsTo()
    一对多 hasMany() belongsTo()
    多对多 belongsToMany() belongsToMany()
    远程一对多 hasManyThrough() 不支持
    多态一对一 morphOne() morphTo()
    多态一对多 morphMany() morphTo()

(20)关联预载入

  • 在普通的关联查询下,循环数据列表会执行 \(n+1\) 次 SQL 查询

    $list = UserModel::select([1, 2, 3]);
    foreach ($list as $user) {
        dump($user->profile);
    }
    
  • 如果采用关联预载入的方式,则将会从四次减少到两次,即起步一次,循环一次

    $list = UserModel::with(['profile'])->select([1, 2, 3]);
    foreach ($list as $user) {
        dump($user->profile);
    }
    
  • 关联预载入减少了查询次数提高了性能,但是不支持多次调用,如果有主表关联了多个附表,并且都需要进行预载入,可以传入多个模型方法

  • 如果需要在关联模型实现链式操作,则可以使用闭包

    $user = UserModel::field('id,name')->with(['profile'=>function ($query) {
        $query->field('user_id, hobby');
    }])->select([1, 2, 3]);
    
  • 关联预载入还提供了一个延迟预载入,即先执行 select() 查找再执行 load() 载入

    $list = UserModel::select([1, 2, 3]);
    $list->load(['profile']);
    foreach ($list as $user) {
    	dump($user->profile);
    }
    

(21)关联统计

  • 使用 withCount() 方法可以统计主表关联附表的个数,输出用 profile_count

    $list = UserModel::withCount(['profile'])->select([1, 2, 3]);
    foreach ($list as $user) {
    	echo $user->profile_count;
    }
    
  • 关联统计的输出采用 关联方法名_count 结构输出

  • 还支持 withMax()withMin()withSum()withAvg() 等方法,但是均需要指定统计字段

    $list = UserModel::withSum(['profile'], 'status')->select([1, 2, 3);
    foreach ($list as $user) {
        echo $user->profile_sum.'<br />';
    }
    
  • 对于输出的属性可以自定义

    $list = UserModel::withSum([ 'profile'=>'pf'], 'status')->select([1, 2, 3]);
    foreach ($list as $user) {
        echo $user->pf.'<br />';
    }
    

(22)关联输出

  • 使用 hidden() 方法可以隐藏主表字段或附属表的字段

    $list = UserModel::with('profile')->select();
    return json($list->hidden(['profile.status']));
    

    return json($list->hidden(['name', 'age', 'profile'=>['hobby','user_id']]));
    
  • 使用 visible() 方法可以只显示相关的字段

    $list->visible(['profile.status']);
    
  • 使用 append() 方法可以添加一个额外字段

    $list->append(['book']);
    

(23)关联查询

a. 一对一

I. hasOne 模式

  • hasOne 模式适合主表关联附表

    hasOne('关联模型',['外键','主键']);
    

    例如:

    return $this->hasOne(Profile::class,'user_id','id');
    

    以下内容摘自官方文档:

    • 关联模型(必写):关联的模型名或者类名
    • 外键:默认的外键规则是当前模型名(不含命名空间,下同) + _id,例如 user_id
    • 主键:当前模型主键,默认会自动获取也可以指定传入
  • 使用 save() 方法可以设置关联修改,通过主表修改附表字段的值

    • ->profile 属性方式可以修改数据

      $user->profile->save(['hobby' => 'ysqd']);
      
    • ->profile() 方法方式可以新增数据

      $user->profile()->save(['hobby' => 'ysqd'])
      

II. belongsTo 模式

  • belongsTo 模式适合附表关联主表

    belongsTo('关联模型',['外键','关联主键']);
    

    例如:

    return $this->belongsTo(Profile::class,'user_id','id');
    

    以下内容摘自官方文档:

    • 关联模型(必写):模型名或者模型类名
    • 外键:当前模型外键,默认的外键名规则是关联模型名 + _id
    • 关联主键:关联模型主键,一般会自动获取也可以指定传入
  • 使用 hasOne() 可以模拟 belongsTo() 来进行查询

    //参数一表示的是 User 模型类的 profile 方法,而非 Profile 模型类
    $user = UserModel::hasWhere('profile',['id'=>2])->find();
    return json($user);
    
    //采用闭包,这里是两张表操作,会导致 id 识别模糊,需要指明表
    $user = UserModel::hasWhere('profile', function ($query) {
    	$query->where('profile.id',2);
    })->select();
    return json($user);
    

b. 一对多

  • hasMany 模式适合主表关联附表,实现一对多查询

    hasMany('关联模型',['外键','主键']);
    
    return $this->hasMany(Profile::class,'user_id','id');
    

    以下内容摘自官方文档:

    • 关联模型(必写):模型名或者模型类名
      外键:关联模型外键,默认的外键名规则是当前模型名 + _id
      主键:当前模型主键,一般会自动获取也可以指定传入
  • 使用 ->profile() 方法模式可以进一步进行数据的筛选

    $user->profile()->where('age','>',18)->select();
    
  • 使用 has() 方法可以查询关联附表的主表内容

    UserModel::has('profile', '>=', 2)->select();
    
  • 使用 save()saveAll() 进行关联新增和批量关联新增

    $user = UserModel::find(1);
    $user->profile()->save(['hobby'=>'ysqd', 'status'=>1]);
    $user->profile()->saveAll([
        ['hobby'=>'baqd', 'status'=>1],
        ['hobby'=>'btqd', 'status'=>1]
    ]);
    
  • 使用 together() 方法可以在删除主表内容时,将附表关联的内容全部删除

    $user = UserModel::with('profile')->find(1);
    $user->together(['profile'])->delete();
    

c. 多对多

  • 多对多分解来看就是一个用户对应多个角色,同时一个角色对应多个用户。那么这种对应关系,就是多对多关系,最经典的应用就是权限控制

  • 创建三张数据表并添加键

    • 用户表(tp_user):id、name、age
    • 角色表(tp_role):id、type
    • 中间表(tp_access):id、user_id、role_id
  • belongsToMany 为多对多关联

    belongsToMany('关联模型','中间表',['外键','关联键']);
    
  • 创建 User 模型、Role 模型以及 Access 模型,在 User 模型类中设置多对多关联,Role 模型类继承 Model、Access 模型类继承 Pivot

    public function roles()
    {
        return $this->belongsToMany(Role::class, Access::class);
    }
    
  • 在控制器端中创建 many() 方法用于测试

    public function many()
    {
        $user = UserModel::find(1);
        $roles = $user->roles;
        return json($roles)
    }
    
  • 当给一个用户创建一个角色时,需要用到多对多关联新增,而关联新增后,不但会给 tp_role 新增一条数据,也会给 tp_access 新增一条

    $user->roles()->save(['type'=>'Role2']);
    

    或批量新增

    $user->roles()->saveAll([[...],[...]]);
    
  • 一般来说,上述新增方式适用于初始化角色,即各种权限的角色应在初始制定好。此时真正需要的就是通过用户表新增到中间表关联

    $user->roles()->save(1);
    

    $user->roles()->save(Role::find(1))
    $user->roles()->saveAll([1,2,3]);	// 批量新增
    

    $user->roles()->attach(1);
    $user->roles()->attach(2, ['details'=>'details_content']);
    
  • 使用 detach() 方法可以直接删除中间表数据

    $user->roles()->detach(1);
    

0x04 路由

(1)概述

  • 路由的作用就是让 URL 地址更加的规范和优雅,或者说更加简洁,设置路由对 URL 的检测、验证等一系列操作提供了极大的便利性

  • 路由是默认开启的,如果想要关闭路由,在 config/app.php 配置

    路由的配置文件在 config/route.php 中,定义文件在 route/app.php

    'with_route' => false
    
  • route 目录下的定义文件的文件名随机,都有效,或多个均有效果

    • 举例:创建一个 Address 控制器类,创建两个方法,具体如下

      <?php
      
      namespace app\controller;
      
      class Address
      {
          public function index()
          {
              return 'index';
          }
      
          public function details($id)
          {
              return $id;
          }
      }
      
    • 访问 /address/details/id/5 测试

  • 将这个 URL 定义路由规则,在 route/app.php 中配置

    Route::rule('details/:id', 'Address/details');
    
    • 访问 /details/5 测试
  • rule() 方法默认请求是 any,可以通过第三参数设置

    Route::rule('details/:id', 'Address/details', 'GET');
    
    • 请求的快捷方式

      Route::get('details/:id', 'Address/details');
      
      Route::post('details/:id', 'Address/details');
      
      • delete()put()patch()
  • 当我们设置了强制路由的时候,访问首页就会报错,必须强制设置首页路由,需要在 route.php 里面进行配置,然后在 route/app.php 配置首页路由

    'url_route_must' => true
    
    Route::rule('/', 'Index/index');
    
  • 在路由的规则表达式中,有多种地址的配置规则

    • 静态路由

      Route::rule('address', 'Address/index');
      
    • 静态动态结合的地址

      Route::rule('details/:id', 'Address/details');
      
    • 多参数静态动态结合的地址

      Route::rule('details/:id/:uid', 'Address/details');
      
    • 全动态地址(不限制是否 search 固定)

      Route::rule(':details/:id/:uid', 'Address/details');
      
    • 包含可选参数的地址

      Route::rule('details/:id/[:uid]', 'Address/details');
      
    • 规则完全匹配的地址

      Route::rule('details/:id/:uid$', 'Address/details');
      
  • 路由定义好之后,在控制器要创建这个路由地址,可以通过 url() 方法实现

    • 不定义标识的做法

      return url('Address/details', ['id' => 10]);
      
    • 定义标识的做法

      // filename: route/app.php
      Route::rule('details/:id', 'Address/details')->name('det');
      
      // filename: controller/Index.php
      return url('det', ['id' => 10]);
      

(2)变量规则与闭包

  • 系统默认的路由变量规则为 \w+,即字母、数字、中文和下划线。如果你想更改默认的匹配规则,可以修改 config/route.php 配置

    'default_route_pattern' => '[\w\.]+'
    
  • 如果我们需要对于具体的变量进行单独的规则设置,则需要通过 pattern()方法。将 details 方法里的 id 传值,严格限制必须只能是数字 \d+

    Route::rule('details/:id', 'Address/details')->pattern(['id' => '\d+']);
    
    • 对于多个参数的规则可以通过数组实现

      Route::rule('details/:id/:uid', 'Address/details')->pattern([
          'id' => '\d+',
          'uid' => '\d+'
      ]);
      
  • 可以直接在 app.php 设置全局变量规则

    Route::pattern([
        'id' => '\d+',
        'uid' => '\d+'
    ]);
    
  • 支持使用组合变量规则方式,实现路由规则

    Route::rule('details-<id>', 'address/details')->pattern('id', '\d+');
    
  • 动态组合的拼装,地址和参数如果都是模糊动态的,可以使用如下方法

    Route::rule('details-:name-:id', 'Hello:name/index')->pattern('id', ' d+');
    

(3)地址

  • 地址一般为 [控制器]/[操作方法]

  • 一种完整路径的方式去执行操作方法 [完整类名]@[操作方法]

    Route::rule('details/:id', '\app\controller\Address@details');
    
    • 对于静态方法可以使用另外的完整路径 [完整类名]::[静态方法]
  • 路由可以通过 ::redirect() 方法实现重定向调整,第三参数为状态码

    Route::redirect('details/:id', 'http://localhost/', 302);
    

(4)参数

  • 设置路由的时候,可以设置相关方法进行,从而实施匹配检测和行为执行

  • ext() 方法作用是检测 URL 后缀

    • 举例:我们强制所有 URL 后缀为 .html

      Route::rule('details/:id', 'address/details')->ext('html');
      
      Route::rule('details/:id', 'address/details')->ext('html|shtml');
      
  • https() 方法作用是检测是否为 https 请求

    • 举例:结合 ext() 方法强制 html

      Route::get('details/:id', 'address/details')->ext('html')->https();
      
  • 如果想让全局统一配置 URL 后缀的话,可以在 config/route.php 中设置。具体值可以是单个或多个后缀,也可以是空字符串(任意后缀),false 表示禁止后缀

    'url_html_suffix' => 'html'
    
  • denyExt() 方法作用是禁止某些后缀的使用,使用后直接报错

    Route::rule('details/:id', 'address/details')->denyExt('gif|jpg|png');
    
  • domain() 方法作用是检测当前的域名是否匹配,完整域名和子域名均可

    Route::rule('details/:id','Address/details')->domain('localhost');
    Route::rule('details/:id','Address/details')->domain('news.abc.com');
    Route::rule('details/:id','Address/details')->domain('news');
    
  • ajax()pjax()json() 方法作用是检测当前的页面是否是以上请求方式

    // filename: route/app.php
    Route::rule('details/:id','Address/details')->ajax();
    
    // filename: app/controller/Index.php
    public function index()
    {
        return view();
    }
    
    // filename: app/view/index/index.html
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
    <input type="button" id="button" value="Click"/>
    <script src="/static/jquery-3.5.1.min.js"></script>
    <script>
        $("#button").click(function () {
            let id = 5;
            $.ajax({
                type: "GET",
                url: "http://test.com/details/" + id,
                success: function (res) {
                    console.log(res);
                }
            });
        });
    </script>
    </body>
    </html>
    
  • filter() 方法作用是对额外参数进行检测,额外参数可表单提交

    Route::rule('details/:id', 'address/details')->filter(['id'=>5, 'type'=>1]);
    
  • append() 方法作用是追加额外参数,这个额外参数并不需要通过 URL 传递

    Route::rule('details/:id', 'address/details')->append(['status'=>1]);
    
  • option() 方法作用是使用数组统一配置多个参数,从而方便管理

    Route::rule('details/:id','address/details')->option([
        'ext' => 'html',
        'https' => true
    ]);
    

(5)域名

  • 打开 C:\windows\System32\drivers\etc 找到 hosts 文件,在末尾添加一句: 127.0.0.1 test.com 映射二级域名,此时,我们访问 www.test.com 就直接映射到 localhost 里了

  • 如果想限定在 www.test.com 这个域名下才有效,通过域名路由闭包的形式

    Route::domain('www', function () {
    	Route::rule('details/:id', 'Address/details');
    });
    
    • 除了二级(子)域名的开头部分,也可以设置完整域名

      Route::domain('www.test.com', function () {
      	Route::rule('details/:id', 'Address/details');
      });
      
  • 支持多个二级(子)域名开头部分,使用相同的路由规则

    Route::domain(['aaa','bbb','ccc'], function () {
    	Route::rule('details/:id', 'Address/details');
    });
    
  • 可以作为方法,进行二级(子)域名开头部分的检测,或完整域名检测

    Route::rule('details/:id', 'Address/details')->domain('www');
    
    Route::rule('details/:id', 'Address/details')->domain('www.test.com');
    
  • 路由域名也支持 ext、pattern、append 等路由参数方法的操作

(6)跨域请求

  • 当不同域名进行跨域请求的时候,由于浏览器的安全限制,会被拦截

  • 为了解除这个限制,我们通过路由方法 allowCrossDomain() 来实现

    Route::rule('details/:id', 'Address/details')->ext('html')->allowCrossDomain();
    
  • 如果需要限制跨域请求的域名,则可以增加一条参数

    Route::rule('details/:id', 'Address/details')->ext('html')->allowCrossDomain([
        'Access-Control-Allow-Origin' => 'http://www.test.com:8000'
    ]);
    

(7)分组

  • 将相同前缀的路由合并分组,这样可以简化路由定义,提高匹配效率

  • 使用 group() 方法,来进行分组路由的注册

    Route::group('address', function () {
        Route::rule(':id', 'address/details');
        Route::rule(':name', 'address/search');
    })->ext('html')->pattern([
        'id' => '\d+',
        'name' => '\w+'
    ]);
    
  • 可以省去第一参数,让分组路由更灵活一些

    Route::group(function () {
        Route::rule('details/:id', 'address/details');
        Route::rule('search/:name', 'address/search');
    })->ext('html')->pattern([
        'id' => '\d+',
        'name' => '\w+'
    ]);
    
  • 使用 prefix() 方法,可以省略掉分组地址里的控制器

    Route::group('address', function () {
        Route::rule(':id', 'details');
        Route::rule(':name', 'search');
    })->ext('html')->prefix('address')->pattern([
        'id' => '\d+',
        'name' => '\w+'
    ]);
    
  • 使用 append() 方法,可以额外传入参数

    Route::group()...->append(['status'=>1]);
    
  • 路由规则(主要是分组和域名路由)定义的文件,加载时会解析消耗较多的资源,尤其是规则特别庞大的时候,延迟解析开启让你只有在匹配的时候才会注册解析,在 route.php 中开启延迟解析,多复制几组规则,然后来查看内存占用

    'url_lazy_route' => true
    

(10)MISS

  • 全局 MISS 类似开启强制路由功能,匹配不到相应规则时自动跳转到 MISS

  • 分组 MISS 可以在分组中使用 miss() 方法,当不满足匹配规则时跳转到这里

    Route::miss('miss');
    

(11)资源路由

a. 概述

  • 资源路由,采用固定的常用方法来实现简化 URL 的功能;

    Route::resource('ads', 'Address');
    
  • 系统提供了一个命令,方便开发者快速生成一个资源控制器

    php think make:controller Blog
    
  • 从生成的多个方法,包含了显示、增删改查等多个操作方法

b. 基础资源路由

  • 在路由定义文件下创建一个资源路由,资源名称可自定义

    • 举例:创建一个博客资源路由

      Route::resource('blog', 'Blog');
      
    • 这里的 blog 表示资源规则名,Blog 表示路由的访问路径

  • 资源路由注册成功后,会自动提供以下方法,无须手动注册

    • GET 访问模式下:index(blog)create(blog/create)read(blog/:id)edit(blog/:id/edit)

    • POST 访问模式下:save(blog);

    • PUT 方式模式下:update(blog/:id);

    • DELETE 方式模式下:delete(blog/:id);

    • POST 是新增,一般是表单的 POST 提交,而 PUT 和 DELETE 用 AJAX 访问

    • 将跨域提交那个例子修改成 .ajax,其中 type 设置为 DELETE 即可访问到

      $.ajax({
          type: "DELETE",
          url: "http://localhost:8000/details/5",
          success: function (res) {
              console.log(res);
          }
      });
      
  • 默认的参数采用 id 名称,如果需要使用其他名称,则:

    ->vars(['blog'=>'blog_id'])
    
  • 可以通过 only() 方法限定系统提供的资源方法

    ->only(['index', 'save', 'create'])
    
  • 可以通过 except() 方法排除系统提供的资源方法

    ->except(['read', 'delete', 'update'])
    
  • 使用 rest() 方法,更改系统给予的默认方法,参数分别为请求方式、地址、操作

    Route::rest('create', ['GET', '/:id/add', 'add']);
    

    以下是批量操作

    Route::rest([
        'save' => ['POST', '', 'store'],
        'update' => ['PUT', '/:id', 'save'],
        'delete' => ['DELETE', '/:id', 'destory']
    ]);
    

c. 嵌套资源路由

  • 使用嵌套资源路由,可以让上级资源对下级资源进行操作,创建 Comment 资源

    <?php
    
    namespace app\controller;
    
    class Comment
    {
        public function read($id, $blog_id)
        {
            return 'Comment id: ' . $id . ', Blog id: ' . $blog_id;
        }
        
        public function edit($id, $blog_id)
        {
            return 'Comment id: ' . $id . ', Blog id: ' . $blog_id;
        }
    }
    
    Route::resource('blog.comment', 'Comment');
    
  • 嵌套资源生成的上级资源默认 idblog_id,可以通过 vars() 更改;

    Route::resource('blog.comment', 'Comment')->vars(['blog'=>'blogid']);
    

(12)注解路由

  • 路由的注解方式,并非系统默认支持,而是可选方案,需要执行命令 composer require topthink/think-annotation 额外安装扩展

  • 安装好后,使用 use 引入相关类库

    use think\annotation\Route;
    
  • 在控制器设置注解代码,可以使用 PHPDOC 生成一段,然后添加路由规则

    /**
     * @return string
     */
    public function index()
    {
        return 'index';
    }
    /**
     * @param $id
     * @return mixed
     */
    public function details($id)
    {
        return $id;
    }
    
  • 第二或以上参数,可以设置请求类型

    • 举例:GET 模式访问

      /**
       * @route("details/:id", method="GET")
       */
      
  • 更多参数可实现更多功能(不需要考虑顺序),比如 ext、https 等;

    /**
     * @route("details/:id", method="GET", ext="html", https=1)
     */
    
  • 注解模式支持资源路由,先要 use 相关类库,然后声明

    use think\annotation\Route\Resource;
    /**
     * @Resource("blog")
     */
    class Blog { ... }
    
  • 注解模式支持分组,先要 use 相关类库,然后声明

    use think\annotation\Route;
    use think\annotation\route\Group;
    /**
     * @Group( "ads" )
     */
    

(13)URL 生成

  • 创建一个新的控制器 Url.php,创建一个路由方法和 URL 生成的方法

    <?php
    
    namespace app\controller;
    
    class Url
    {
        public function index()
        {
            return 'url';
        }
        
        public function details($id)
        {
            return $id;
        }
    }
    
    Route::rule('details', 'Url/index');
    Route::rule('details/:id', 'Url/details');
    
  • 在控制器中使用 Route::buildUrl("地址’,[参数]...) 方式来获取路由的 URL 地址

    return Route::buildUrl('Url/details', ['id'=>5]);
    
    • 这里的地址和路由的定义是相辅相成的,如果没有定义,地址将会变化
  • 可以给路由定义取一个别名,然后在生成 URL 的时候,直接使用这个别名调用

    Route::rule('details/:id', 'Url/details')->name('uds');
    return Route::buildUrl('uds', ['id'=>5]);
    
  • 可以直接使用路由地址生成 URL,但这个方式并不需要和路由定义相匹配

    return Route::buildUrl('details/5');
    
  • 由于默认在配置设置了后缀为 .html,所以生成的 URL 会自动加上

    return Route::buildUrl('ds/5')->suffix('shtml');
    
  • 如果需要添加完整域名路径,可以再添加 domain() 方法

    return Route::buildUrl('details/5')->domain(true);
    return Route::buildUrl('details/5')->domain('www');
    return Route::buildUrl('details/5')->domain('www.test.com');
    return Route::buildUrl('details/5@www.test.com');
    
  • 可以直接使用助手函数 url() 来代替 Route::buildUrl()

    return url('details/5');
    

(14)依赖注入

  • 依赖注入本质上是指对类的依赖通过构造器完成自动注入

    • 例如:在控制器架构方法和操作方法中一旦对参数进行对象类型约束则会自动触发依赖注入,由于访问控制器的参数都来自于 URL 请求,普通变量就是通过参数绑定自动获取,对象变量则是通过依赖注入生成
  • 举例

    • 创建一个模型

      <?php
      
      namespace app\model;
      
      use think\Model;
      
      class One extends Model
      {
          public $username = "Alex";
      }
      
    • 创建一个控制器

      <?php
      
      namespace app\controller;
      
      use app\model\One;
      
      class Inject
      {
          protected $one;
          
          public function __construct(One $one)
          {
              $this->one = $one;
          }
      
          public function index()
          {
              return $this->one->username;
          }
      }
      
  • 允许通过类的方法传递对象的能力,并且限制了对象的类型(约束),而传递的对象背后的那个类被自动绑定并且实例化了,这就是依赖注入

(15)容器

  • 依赖注入的类统一由容器管理的,大多数情况下是自动绑定和自动实例化的,如果想手动来完成绑定和实例化,可以使用 bind()app() 助手函数来实现

    <?php
    
    namespace app\controller;
    
    class Inject
    {
        public function bind()
        {
            bind('one', 'app\model\One');
            return app('one')->username;
        }
    }
    
    • bind('one', '...') 绑定类库标识,这个标识具有唯一性,以便快速调用
    • app('one') 快速调用,并自动实例化对象,标识严格保持一致包括大小写
  • 自动实例化对象的方式,是采用单例模式实现

    $one = app('one', [], true);
    return $one->name;
    
  • app('one', []) 中第二参数在方法实例化对象的时候传递参数

    bind('one', 'app\model\One');
    $one = app('one', [['file']], true);
    return $one->username;
    
  • 可以直接通过 app() 绑定一个类到容器中并自动实例化

    return app('app\model\One')->username;
    
  • 使用 bind([]) 可以实现批量绑定

    bind([
        'one' => 'app\model\One',
        'two' => 'app\model\Two'
    ]);
    return app('one')->username;
    
    bind([
        'one' => app\model\One::class,
        'two' => app\model\Two::class
    ]);
    return app('two')->username;
    
    • ::class 模式不需要单引号
  • 系统提供了 provider.php 文件,用于批量绑定类到容器中

    return [
        'one' => app\model\One::class,
        'two' => app\model\Two::class
    ];
    

(16)门面 Facade

  • 门面设计模式(Facade)为容器的类提供了一种静态的调用方式

  • 举例:

    • 在 app 目录下创建 common 公共类库为文件夹并创建 Test.php

      <?php
      
      namespace app\common;
      
      class Test
      {
          public function hello($name)
          {
              return 'Hello, ' . $name;
          }
      }
      
    • 在 app 目录下创建 facade 文件夹并创建 Test.php,用于生成静态调用

      <?php
      
      namespace app\facade;
      
      use think\Facade;
      
      class Test extends Facade
      {
          protected static function getFacadeClass()
          {
              return 'app\common\Test';
          }
      }
      
    • 在控制器端可以和之前系统提供的静态调用一样调用

      <?php
      
      namespace app\controller;
      
      use app\BaseController;
      use app\facade\Test;
      
      class Index extends BaseController
      {
          // ...
          public function test()
          {
              return Test::hello('world');
          }
      }
      
  • 系统提供了常用 Facade 核心类库

(17)数据请求

a. 请求对象

  • 使用构造方法注入请求

    <?php
    
    namespace app\controller;
    
    use think\Request;
    
    class Rely
    {
        protected $request;
    
        public function __construct(Request $request)
        {
            $this->request = $request;
        }
    
        public function index()
        {
            return $this->request->param('username');
        }
    }
    
  • Request 请求对象拥有一个 param() 方法,传入参数 username,可以的得到相应的值,可以在普通方法下直接使用

    use think\Request;
    
    class Rely
    {
        // ...
        public function index(Request $request)
        {
            return $request->param('username');
        }
    }
    
  • 使用 Facade 方式应用于没有进行依赖注入时使用 Request 对象的场合

    <?php
    
    namespace app\controller;
    
    use think\facade\Request;
    
    class Rely
    {
    
        public function index()
        {
            return Request::param('username');
        }
    }
    
  • 使用助手函数 request() 方法可以应用在没有依赖注入的场合

    public function index()
    {
        return request()->param('username');
    }
    

b. 请求信息

  • Request 对象一些请求的固定信息

    方法 含义
    host() 当前访问域名或者 IP
    scheme() 当前访问协议
    port() 当前访问的端口
    remotePort() 当前请求的 REMOTE_ROOT
    protocol() 当前请求的 SERVER_PROTOCOL
    contentType() 当前请求的 CONTENT_TYPE
    domain() 当前包含协议的域名
    subDomain() 当前访问的子域名
    panDomain() 当前访问的泛域名
    rootDomain() 当前访问的根域名
    url() 当前完整的 URL
    baseUrl() 当前 URL(不含 QUERY_STRING)
    query() 当前请求的 QUERY_STRING 参数
    baseFile() 当前执行的文件
    root() URL 访问根地址
    rootUrl() URL 访问根目录
    pathinfo() 当前请求 URL 的 pathinfo 信息(含 URL 后缀)
    ext() 当前 URL 的访问后缀
    time() 获取当前请求的时间
    type() 当前请求的资源类型
    method() 当前请求类型
    rule() 当前请求的路由对象实例
  • 上述方法中个别方法需要传入 true

    Request::url();		// 获取完整 URL 地址
    Request::url(true);	// 获取完整 URL 地址,包含域名
    
    Request::baseFile();		// 获取当前 URL(不含 QUERY_STRING),不含域名
    Request::baseFile(true);	// 获取当前 URL(不含 QUERY_STRING),包含域名
    
    Request::root();		// 获取 URL 访问根地址,不含域名
    Request::root(true);	// 获取 URL 访问根地址,包含域名
    
  • 可以获取当前控制器和操作方法的名称 ::controller()::action()

    return Request::controller() . '|' . Request::action();
    

c. 请求变量

  • Request 对象支持全局变量的检测、获取和安全过滤

  • 使用 has() 方法可以检测全局变量是否已经设置

    Request::has('id', 'get');
    
  • Request 支持很多变量类型方法

    方法 描述
    param() 获取当前请求的变量
    get() 获取 $_GET 变量
    post() 获取 $_POST 变量
    put() 获取 PUT 变量
    delete() 获取 DELETE 变量
    session() 获取 SESSION 变量
    cookie() 获取 $_COOKIE 变量
    request() 获取 $_REQUEST 变量
    server() 获取 $_SERVER 变量
    env() 获取 $_ENV 变量
    route() 获取路由(包括 PATHINFO)变量
    middleware() 获取中间件赋值/传递的变量
    file() 获取 $_FILES 变量
  • param() 变量方法可以自动识别当前请求

    //获取请求为 name 的值,过滤
    Request::param('name');
    
    //获取所有请求的变量,以数组形式,过滤
    Request::param();
    
    //获取所有请求的变量(原始变量),不包含上传变量,不过滤
    Request::param(false);
    
    //获取部分变量
    Request::param(['name', 'age']);
    
  • 默认情况下,并没有配置字符过滤器,可在 app\Request.php 配置

    protected $filter = ['htmlspecialchars'];
    
  • 如果没有设置字符过滤器,或者局部用别的字符过滤器,可以通过第三参数

    Request::param('name', '', 'htmlspecialchars');
    Request::param('name', '', 'strip_tags,strtolower');
    
  • 如果设置了全局字符过滤器,且不在某个局部使用,可以只用 null 参数

    Request::param('name', '', null);
    
  • 也支持请求的变量设置一个默认值

    Request::param('name', '默认值');
    
  • 如果采用的是路由 URL,也可以获取到变量,但 param::get() 不支持路由变量

    Request::param('id');
    Request::route('id');
    Request::get('id');
    
  • 使用 only() 方法,可以获取指定的变量,也可以设置默认值

    Request::only(['id', 'name' ]);
    
    Request::only(['id'=>1, 'name'=>'默认值']);
    
    • 默认是 param 变量,可以在第二参数设置 GET、POST 等

      Request::only(['id', 'name'], 'post');
      
  • 相反的 except() 方法,就是排除指定的变量

    Request::except('id, name');
    
    Request::except(['id', 'name']);
    
    Request::except(['id'=>1, 'name'=>'默认值']);
    
    Request::except(['id', 'name'], 'post');
    
  • 使用变量修饰符,可以将参数强制转换成指定的类型

    Request::param('id/d');
    
    修饰符 说明
    /s 字符串
    /d 整型
    /b 布尔
    /a 数组
    /f 浮点
  • 为了简化操作,Request 对象提供了助手函数

    • 判断 get 下的 id 是否存在

      input('?get.id');
      
    • 判断 post 下的 name 是否存在

      input('?post.name');
      
    • 获取 param 下的 name 值

      input('param.name');
      
    • 默认值

      input('param.name', '默认值');
      
    • 过滤器

      input('param.name', '', 'htmlspecialchars');
      
    • 设置强制转换

      input('param.id/d');
      

d. 请求类型

  • 可以使用 method() 方法来判断当前的请求类型

    方法 说明
    method() 获取当前请求类型
    isGet() 判断是否是 GET 请求
    isPost() 判断是否是 POST 请求
    isPut() 判断是否是 PUT 请求
    isDelete() 判断是否是 DELETE 请求
    isAjax() 判断是否是 AJAX 请求
    isPjax() 判断是否是 PJAX 请求
    isJson() 判断是否是 JSON 请求
    isMobile() 判断是否是手机访问
    isHead() 判断是否是 HEAD 请求
    isPatch() 判断是否是 PATCH 请求
    isOptions() 判断是否是 OPTIONS 请求
    isCli() 判断是否是 CLI 执行
    isCgi() 判断是否是 CGI 模式
  • 使用普通表单提交,通过 method() 方法获取类型

    <form action="http://test.com/get" method="post">
        <input type="text" name="name" value="Alex" />
        <input type="submit" value="Submit" />
    </form>
    
    use think\facade\Request;
    // ...
    public function get()
    {
        return Request::method();
    }
    
  • 在表单提交时,可以设置请求类型伪装,设置隐藏字段 _method。而在判断请求,使用 method(true) 可以获取原始请求,否则获取伪装请求

    <form action="http://test.com/get" method="post">
        <input type="hidden" name="_method" value="PUT" />
        <input type="submit" value="Submit" />
    </form>
    
    Request::method();		// PUT
    Request::method(true);	// POST
    
  • 如果需要更改请求伪装变量类型的名称,可以在 app/Request.php 中更改:

    protected $varMethod = '_m';
    
  • AJAX/PJAX 的伪装可以使用?_ajax=1?_pjax=1,并使用 isAjax()isPjax()

    .../rely?_ajax=1
    Request::isAjax();
    
    • 这里需要用 isAjax()isPjax() 来判断,用 method() 无法判断是否为 AJAX/PJAX
  • 在 app.php 也可以更改 AJAX 和 PJAX 的名称

    protected $varAjax = '_a';
    protected $varpjax = '_p';
    

e. HTTP 头信息

  • 使用 header() 方法可以输出 HTTP 头信息,返回是数组类型

    Request::header();
    
  • 可以单信息获取

    Request::header('host');
    

f. 伪静态

  • 可以通过 route.php 修改伪静态的后缀,比如修改成 shtml、xml 等

    'url_html_suffix' => 'html'
    
  • 如果地址栏用后缀访问成功后,可以使用 Request::ext() 方法得到当前伪静态

    return Request::ext();
    
  • 配置文件伪静态后缀,可以支持多个,用竖线 | 隔开

    'url_html_suffix' => 'shtml|xml|pdf'
    
  • 如果直接将伪静态配置文件设置为 false,则关闭伪静态功能:

    'url_html_suffix' => false
    

g. 参数绑定

  • 参数绑定:URL 地址栏的数据传参:/details/id/5

  • 设置默认值防止 URL 参数错误

    public function details($id = 0)
    {
        return $id;
    }
    
  • 多参数

    public function details($id, $name)
    {
        return $id . $name;
    }
    
    • 访问 /details/id/5/name/Alex/details/name/Alex/id/5

h. 请求缓存

  • 请求缓存仅对 GET 请求有效,可以在全局和局部设置缓存

  • 如果要设置全局请求缓存,在中间件文件 middleware.php 中设置

    'think\middleware\CheckRequestCache',
    
  • 之后在 route.php 中设置缓存的声明周期即可

    'request_cache_expire' => 3600
    
  • 当第二次访问时,会自动获取请求缓存的数据响应输出,并发送 304 状态码

  • 如果要对路由设置一条缓存,则可以直接使用 cache(3600) 方法

    Route::get('get/:id', 'Rely/get')->cache(3600);
    

(18)响应输出

  • 响应输出包括 returnjson()view()

  • 响应输出可以使用 response() 方法达到相同达到效果

    return response($data);
    
  • 使用 response() 方法可以设置第二参数或使用 code() 来设置状态码

    return response($data, 201);
    
    return response($data)->code(201);
    
    • 使用 json()view() 方法和 response() 返回的数据类型不同,效果一样

      return json($data, 201);
      
      return json($data)->code(201);
      
  • 使用 response() 方法可以使用 header() 方法设置 HTTP 头信息

    return json($data)->code(201)->header(['Cache-control' => 'no-cache, must-revaliadate']);
    

(19)重定向

  • 使用 redirect() 方法可以实现页面重定向,需要 return 执行

    return redirect('http://www.baidu.com');
    
  • 站内重定向,直接输入路由地址或相对地址

    return redirect('details/5');
    
    • 可以通过第二参数设置状态码

      return redirect('/details/5', 201);
      
  • 使用 url() 自动生成跳转地址,包括普通地址或路由地址

    return redirect(url('/'));
    
  • 可以附加 session 信息,并跳转重定向

    return redirect(url('/'))->with('name', 'Alex');
    

0x05 验证

(1)验证器

  • 验证器的使用前,必须先定义,系统提供了一条命令直接生成想要的类:php think make:validate User

  • 这条命令会自动在应用目录下生成一个 validate 文件夹,并生成 User.php 类:

    <?php
    declare (strict_types = 1);
    
    namespace app\validate;
    
    use think\Validate;
    
    class User extends Validate
    {
        /**
         * 定义验证规则
         * 格式:'字段名' =>  ['规则1','规则2'...]
         *
         * @var array
         */
        protected $rule = [];
    
        /**
         * 定义错误信息
         * 格式:'字段名.规则名' =>  '错误信息'
         *
         * @var array
         */
        protected $message = [];
    }
    
    • 自动生成了两个属性:$rule 表示定义规则,$message 表示错误提示信息

      protected $rule = [
          'name' => 'require|max:20',         // 内容不为空 | 最大值为 20
          'age' => 'number|between:0, 100',   // 纯数字格式 | 0 到 100 之间
          'email' => 'email'                  // 邮箱格式
      ];
      
      protected $message = [
          'name.require' => '姓名不能为空',
          'name.max' => '内容过长',
          'age.number' => '内容必须为数字',
          'age.between' => '超出 0~100 范围',
          'email.email' => '邮箱格式错误'
      ];
      
    • 如果不设置 $message 的话,将提示默认的错误信息

  • 验证器调用测试

    • 创建 Verify.php 控制器

      <?php
      
      namespace app\controller;
      
      use app\validate\User;
      use think\exception\ValidateException;
      
      class Verify
      {
          public function index()
          {
              try {
                  validate(User::class)->check([
                      'name' => '张三',
                      'age' => 18,
                      'email' => 'example@mail.com'
                  ]);
              } catch (ValidateException $exception) {
                  dump($exception->getError());
              }
          }
      }
      
    • 访问 /verify

  • 默认情况下,出现一个错误就会停止后面字段的验证,我们也可以设置批量验证

    validate(User::class)->batch(true)->check(...);
    
  • 系统提供了常用的规则让开发者直接使用

    protected $rule = [
        'name' => 'require|max:20',         // 内容不为空 | 最大值为 20
        // ...
    ];
    
    • 验证规则使用方法支持字符串模式和数组模式

      • 字符串模式

        'name' => 'require|max:20',
        
      • 数组模式

        'name' => [
            'require',
            'max' => 20
        ],
        
  • 可以自行定义独有的特殊规则

    protected function checkName($value, $rule)
    {
        return $rule != $value ? true : '内容错误';
    }
    
    protected $rule = [
        'name' => 'checkName:Alex',         // 内容必须为 Alex
        // ...
    ];
    
  • 自定义规则中一共可以有五个参数

    参数 说明
    $value 当前字段值
    $rule 规则值
    $data 所有数据信息
    $field 当前字段名
    $title 字段描述,默认为字段名
  • 设置字段描述

    'name|姓名' => 'require|max:20'
    

(2)独立验证

独立验证就是手动调用验证类

a. 验证规则

  • 独立验证定义

    <?php
    
    namespace app\controller;
    
    use think\facade\Validate;
    
    class Verify
    {
        public function index()
        {
            $validate = Validate::rule([
                'name' => 'require|max:20'
            ]);
            $result = $validate->check([
                'name' => '张三'
            ]);
            if (!$result) {
                dump($validate->getError());
            }
        }
    }
    
  • 独立验证默认也是返回一条错误信息,如果要批量返回所有错误使用 ->batch(true)->

  • 独立验证支持对象化的定义方式,但不支持在属性方式的定义

    $validate = Validate::rule([
    	'name' => ValidateRule::isRequire()->max(20),
    	'age' => ValidateRule:;isNumber()->between([1,100]),
    	'email' => ValidateRule::isEmail()
    )];
    
  • 独立验证支持闭包的自定义方式,但这种方式会不支持字段的多规则

    $validate = Validate::rule([
    	'name' => function ($value) {
    		return $value != '' ? true : '姓名不得为空';
        },
    	'age' => function ($value) {
    		return $value > 0 ? true : '年龄不得小于零';
        }
    ]);
    

b. 错误信息

  • 独立验证的自定义错误提示,可以在方法的第二参数设置

    ValidateRule::isEmail(null, '邮箱格式不正确!');
    ValidateRule::isNumber()->between([1,100], '年龄范围 1-100 之间');
    
  • 可以独立使用 message() 方法,来设置相关错误信息

    $validate->message([
    	'name.require' => ['code' => 9999, 'msg' => '姓名不得为空'],
    	'name.max' => '姓名不可以超过20个字'
    ]);
    

(3)验证场景

  • 验证场景是在特定的场景下设置是否进行验证,独立验证不存在场景验证

    • 举例,新增数据需要验证邮箱,而修改更新时不验证邮箱

      可以在验证类 User.php 中,设置一个 $scene 属性,用来限定场景验证

      protected $scene = [
      	'insert' => ['name', 'age', 'email'],
      	'edit' => ['name','age']
      ];
      
    • 上述代码中,insert 新增需要验证三个字段,而 edit 更新则只要验证两个字段

  • 在控制器端验证时,根据不同的验证手段,绑定相关场景进行验证即可

    $validate->scene('edit')->check($data);
    
  • 在验证类端,可以设置一个公共方法对场景的细节进行定义

    public function sceneEdit()
    {
        $edit = $this->only(['name', 'age'])	// 仅对两个字段验证
            		->remove('name', 'max')		// 移出最大字段的限制
            		->append('age', 'require');	// 增加一个不能为空的限制
        return $edit;
    }
    
  • 不能对一个字段进行两个或以上的移出和添加

    • 正确写法

      remove('name', 'xxx|yyy|zzz');remove('name', ['xxx', 'yyy', 'zzz']);

    • 错误写法

      remove('name', 'xxx')->remove('name', 'yyy')->remove('name', 'zzz');

(4)路由验证

  • 路由验证是在路由的参数来调用验证类进行验证

    protected $rule = [
        'id' => 'number|between:1,10'
    ];
    
    protected $message = [
        'id.number' => 'id 只能为数字',
        'id.between' => 'id 范围是 1~10'
    ];
    
    Route::rule('verify/:id', 'Verify/route')->validate(\app\validate\User::class, 'route');
    
  • 使用独立的验证方式,使用对象化

    Route::rule('verify/:id', 'Verify/route')->validate([
        'id' => 'number|between:1,10',
        'email' => \think\validate\ValidateRule::isEmail()
    ], null, [
        'id.number' => 'id 只能为数字',
        'id.between' => 'id 范围是 1~10',
        'email' => '邮箱格式错误'
    ], true);
    

(5)验证内置规则

  • 内置的规则内容较多,并且严格区分大小写
  • 静态方法支持两种形式,比如 ::number() 或者 isNumber() 均可
  • require 是 PHP 的保留字,那么就必须用 isRequire()must()
  • 格式验证类,如:requirenumber
  • 长度和区间验证类,如:in:1,2,3notBetween:1,100
  • 字段比较类,如:confirm:stringeq:100
  • 其他验证类,如:regex:\d{6}fileMime:text/html

(6)单个验证

  • 静态调用是使用 facade 模式进行调用验证,适合单个数据的验证。引入 facade 中的 Validate 和其它 Validate 会冲突

    //验证邮箱是否合法
    dump(Validate::isEmail('example@mail.com'));
    //验证是否为空
    dump(Validate::isRequre(''));
    //验证是否为数值
    dump(Validate::isNumber(10));
    
  • 静态调用返回的结果是布尔值

  • 静态调用支持多规则验证的,使用 checkRule() 方法实现

    //验证数值合法性
    dump(Validate::checkRule(1, 'number|between:1,10'));
    
    • checkRule() 不支持错误信息,需要自己实现,但支持对象化规则定义

      dump(Validate::checkRule(10, ValidateRule::isNumber()->between('1,10')));
      

(7)注解验证

  • 结合注解路由的传参使用验证方式

    use think\annotation\Route;
    use think\annotation\route\Vaildate as V;
    
    class Verify
    {
        // ...
        
        /**
         * @param $id
         * @return mixed
         * @route("verify/:id")
         * @V(User::class)
         */
        public function route($id)
        {
            return $id;
        }
    }
    

0x06 模板

(1)引擎驱动

  • TP6 默认不自带 TT 模版引擎,其作为一个可选的扩展给开发人员安装,即不一定非要使用模板引擎的语法规则来开发视图部分

  • 如果不用模版引擎,可以在控制器通过 require() 方法引入 PHP 文件混编即可

  • 如果使用模版引擎,需要创建一个用于测试模板引擎的控制器 Show.php,写入模版引擎的调用语法,来判断,是否已经安装了模板引擎扩展

    return View::fetch('index');
    
  • 如果出现缺少驱动的错误,则使用命令 composer require topthink/think-view 安装驱动后,再刷新页面即可

(2)赋值变量

  • 在控制器区域,通过 assign() 设置一个向模版提供变量的赋值操作

    View::assign('name', 'Alex');
    
  • 模版区域只需通过 {$name} 的语法即可获取到控制器设置的值

  • assign() 方法支持通过数组的方式传递模版变量

    View::assign([
        'name' => 'Alex',
        'age' => 18
    ]);
    
  • 可以直接通过 fetch() 方法的第二参数直接用数组传递模板变量

    return View::fetch('index', [
        'name' => 'Alex',
        'age' => 18
    ]);
    
  • 可以通过助手函数 view() 实现与 View::fetch() 相同的效果

    return view('index', [
        'name' => 'Alex',
        'age' => 18
    ]);
    
  • 可以使用 filter() 方法,对所有模版的变量进行过滤操作

    return View::filter(function ($content) {
        return strtoupper($content);
    })->fetch('index');
    
    return view('index')->filter(function ($content) {
        return strtoupper($content);
    });
    

(3)模板配置

  • 默认情况下,config/view.php 就是默认模版引擎的配置文件
  • 内部的配置注释写的非常清楚了,一般情况下,不需要任何改动

(4)模板渲染

  • 除了在配置文件修改外,还可以在控制器端动态修改模版配置;

    View::config(['view_dir_name' => 'view2']);
    
  • 默认情况下,调用的是本控制器的模版文件,也可以调用其它控制器的模版文件

    return View::fetch('Address/index');
    
  • 如果你是多模块(多应用)模式下,也可以实现跨模块调用模版文件

    return View::fetch('admin@User/index');
    
  • 如果需要使用在 view 根目录下的模版文件,用一个斜杠来设定即可调用

    return View::fetch('/index');
    
  • 如果需要调用 public 公共目录的模版文件,用 ../public 后面跟着 URL 即可

    return View::fetch('../public/test/test');
    
  • 这种做法的调用方式,和模版引擎调用一样,只不过通信的数据获取方式有差异

    return View::engine('php')->fetch('index');
    
    • 把所有的要传递的变量,通过 fetch() 的第二个参数传递

      return View::engine('php')->fetch('index', [
          'name' => 'Alex',
          'age' => 18
      ]);
      

(5)变量输出

  • 当程序运行的时候,会在 runtime/temp 目录下生成一个编译文件,默认情况下,输出的模版变量会自动进行过滤,过滤函数默认如下

    <?php echo htmlentities($name); ?>
    
  • 如果传递的值是数组,在模版区域可以使用 $data.name 输出方式

    $arr = ['name'=>'Alex', 'age'=>100];
    return View::fetch('output',[
        'arr' => $arr
    ]);
    
    <p>{$arr.name}--{$arr.age}</p>
    
    <?php echo htmlentities($arr['name']); ?>
    
  • 如果传递的值是对象,那么编译文件也会自动相应的对应输出方式

    public $name ='Alex';
    public $age = 'age';
    const PI = 3.14;
    
    return View::fetch('output', [
        'obj' => $this
    ]);
    
    <p>{$obj->name}--{$obj->age}--{$obj->fn()}--{$obj::PI}</p>
    
    <?php echo htmlentities($obj->name); ?>
    
  • 如果输出的变量没有值,可以直接设置默认值代替

    <p>{$data.name|default='No Name'}</p>
    
  • 系统变量有 $_SERVER$_ENV$_GET$_POST$_REQUEST$_SESSION$_COOKIE

  • 对于注入 Request 对象,可以直接在模版输出

    <p>{$Request.get.id}</p>
    <p>{$Request.param.name}</p>
    <p>{$Request.host}</p>
    
  • 常量、配置信息等都可以通过$Think 输出

    <p>{$Think.PHP_VERSION}</p>
    <p>{$Think.const.PHP_VERSION}</p>
    <p>{$Think.config.app.app_host}</p>
    <p>{$Think.config.session.name}</p>
    

(6)函数

  • 举例:控制器端先赋值一个密码的变量,之后在模版区设置 md5 加密操作

    <p>{$password|md5}</p>
    
  • 如果在某个字符不需要进行 HTML 实体转义的话,可以单独使用 raw 处理

    <p>{$user['email']|raw}</p>
    
  • 系统提供的一些固定的过滤方法

    函数 说明
    date 格式化时间,如 `{$time
    format 格式化字符串,如 `{$number
    upper 转换为大写
    lower 转换为小写
    first 输出数组的第一个元素
    last 输出数组最后一个元素
    default 默认值
    raw 不使用转义
  • 如果函数中需要多个参数调用,直接使用逗号隔开即可

    {$name|substr=0,3}
    
  • 在模版中也支持多个函数进行操作,用 | 号隔开即可,函数从左到右依次执行

    {$password|md5|upper|substr=0,3}
    
  • 可以在模版中直接使用 PHP 的语法模式,该方法不会使用过滤转义:

    {:substr(strtoupper(md5($password)), 0, 3)}
    

(7)运算符

  • 在模版中的运算符有+-*%++--

    {$number + $number}
    
  • 如果模版中有运算符,部分函数不支持

    {$number + $number|default='没有值'}
    
  • 模版也可以实现三元运算,包括其它写法

    {$name ? '正确': '错误'}		  // $name 为 true 返回正确,否则返回错误
    {$name ?= '真'}					// $name 为 true 返回真
    {$Request.get.name ?? '不存在'}   // ??用于系统变量,没有值时输出
    {$name ?: '不存在'}			   // ?:用于普通变量,没有值时输出
    
  • 三元运算符也支持运算后返回布尔值判断

    {$a == $b ? '真': '假'}
    

(8)标签

a. 定义标签

  • 使用 {assign} 标签可以在模板文件中定义一个变量

    {assign name='var' value='Alex'}
    {$var}
    
  • 使用 {define} 标签定义或直接调用常量变量

    {define name='PI' value='3.1415926'}
    
    {$Think.const.PI}
    
  • 使用 {php} 标签进行原生编码

    {php}
    echo 'Hello, world'
    {/php}
    
    • 原生编码就是 PHP 源码,其中无法使用模板引擎的特殊编码方式
  • 标签之间支持嵌套功能

b. 范围标签

  • {in}{notin} 标签

    {in name='id' value='1, 2, 3'}
    集合中
    {/in}
    
    {notin name='id' value='1, 2, 3'}
    集合外
    {else/}
    集合中
    {/notin}
    
    • name 属性值可以是系统变量,如 name=$Think.const.PI
  • {between}{notbetween} 标签

    {between name='id' value='5, 10'}
    区间内
    {else/}
    区间外
    {/between}
    
    • value 属性值只能是两个值,输入的第三个值无效
    • 区间可以是数字区间或字母区间

c. 比较标签

  • {eq} ... {/eq} 标签是比较两个值是否相同,如果相同则输出标签中包含的内容

    {eq name="name" value="Alex"}
    相同
    {/eq}
    
    • 属性 name 是一个变量,可以省略 $ 符合
    • 属性 value 是一个字符串,如果作为变量则需要添加 $
  • 此标签支持 else 操作

    {eq name="name" value="Alex"}
    相同
    {else/}
    不同
    {/eq}
    
  • 此标签存在别名:{equal}

  • 所有比较标签汇总

    标签 说明
    eq / equal 等于
    neq / notequal 不等于
    gt 大于
    egt 大于等于
    lt 小于
    elt 小于等于
    heq 恒等于
    nheq 不恒等于
  • 所有标签都可以同一为 {compare} 标签使用,需要一个 type() 方法指定

    {compare name='name' value='Alex' type='eq'}
    相同
    {/compate}
    

d. 条件标签

I. if

  • {if} 标签可以实现简单条件判断

    {if $id>10}
    	gt 10
    {/if}
    
  • {if} 标签的条件判断中支持 ANDOR 等语法

    {if ($id>10) AND ($id<20)}
    	between 10 and 20
    {/if}
    
  • {if} 标签支持 {else/} 语法

    {if $id>10}
    	gt 10
    {else/}
    	lt 10
    {/if}
    
  • {if} 标签也支持多重条件判断

    {if $id>10}
    	gt 10
    {elseif $id>5}
    	between 5 and 10
    {else/}
    	lt 5
    {/if}
    
  • {if} 标签的条件判断支持 PHP 语法

    {if strtoupper($user->name)=='Alex'}
    Alex Confirmed
    {/if}
    

II. switch

  • {switch} ... {/switch} 标签实现了多个条件的判断

    {switch}
        {case 1}a{/case}
        {case 2}b{/case}
        {case 3}c{/case}
        {default/}None
    {/switch}
    
  • {case} 标签支持多个条件,各个条件需要使用 | 隔开

    {case 1|2|3}1\2\3{/case}
    
  • {case} 标签中的条件也可以是变量

    {case $id}$id{/case}
    

e. 判断标签

  • 判存标签:{present}{notpresent}

    {present name='id'}
    存在
    {else/}
    不存在
    {/present}
    
  • 判空标签:{empty}{notempty}

    {empty name='id'}
    不为空
    {else/}
    为空
    {/empty}
    
  • 判常标签:{defined}{notdefined}

    {defined name='PI'}
    已定义
    {else/}
    未定义
    {/defined}
    

f. 循环标签

I. foreach 循环

  • 控制前端先通过模型把相应的数据列表给筛选出来

    $list = User::class();
    return View::fetch('loop', [
        'list' => $list
    ]);
    
  • 在模板端使用对称的标签 {foreach}...{/foreach} 实现循环

    {foreach $list as $key=>$obj}
    <tr>
        <td>{$key} / {$obj->id}</td>
        <td>{$obj->name}</td>
        <td>{$obj->age}</td>
        <td>{$obj->email}</td>
    </tr>
    {/foreach}
    

    也可以使用 . 替代 ->,如 <td>{$obj.name}</td>

II. volist 循环

  • volist 也是将查询得到的数据集通过循环的方式输出

    {volist name="list" id="obj"}
    <tr>
        <td>{$key} / {$obj->id}</td>
        <td>{$obj->name}</td>
        <td>{$obj->age}</td>
        <td>{$obj->email}</td>
    </tr>
    {/volist}
    
    • name 属性表示数据总集
    • id 属性表示当前循环的数据单条集
  • 使用 offset 属性和 length 属性可以指定显示范围(下标从 0 开始)

    • 举例:从第 4 条开始显示 5 条

      {volist name="list" id="obj" offset='3' length='5'}
      
  • 可以使用 eq 标签来实现奇数或偶数的筛选数据

    {volist name="list" id="obj" mod="2"}
    {eq name='mod' value='0'}
    
    • mod=2 表示索引除以 2 得到的余数
  • 使用 empty 属性可以当没有任何数据的时候,实现指定输出的提示

    {volist name="list" id="obj" empty="没有数据"}
    
  • 使用 key='k' 让索引从 1 开始计算,不指定就用 {$i},指定后失效

    {volist name='list' id='obj' key='k'}
    {$k}
    {/volist}
    

III. for 循环

  • 通过起始和终止值,结合步长实现的循环

    {for start='1' end='100' comparison='lt' step='2' name='i'}
    {$i}
    {/for}
    

(9)加载包含输出

a. 包含文件

  • 使用 {include} 标签来加载公用重复的文件

  • 举例:加载头部、尾部和导航部分

    • 在模版 view 目录创建一个 public 公共目录,分别创建 header、footer 和 nav;

    • 创建控制器 Block,引入控制器模版 index,这个模版包含三个公用文件

      {include file='public/header,public/nav'/}
      
      • 也可以包含一个文件的完整路径,包括后缀

        {include file="../view/public/nav.html"/}
        
    • 模版的标题和关键字,可以通过固定的语法进行传递

      • 对于标题,在控制器先设置一下标题变量,然后通过 {include} 设置属性

        {include file='public/header' title='$title' keywords='关键字'/}
        
    • 切换到 public/header.html 模版页面,使用 [xxx] 的方式调用数据

      <title>[title]</title>
      <meta name="keywords" content="[keywords]" />
      

b. 输出替换

  • 将静态文件的调用路径整理打包

  • 在 view.php 中进行配置

    // 模板替换输出
    'tpl_replace_string' => [
        '__JS__' => '../static/js',
        '__CSS__' => '../static/css'
    ]
    
  • 在调用端可以通过魔术方法调用

    <link rel="stylesheet" type="text/css" href="__CSS__/css.css" />
    <script type='text/javascript' src="__JS__/js.js"></script>
    
  • 在测试的时候,由于是更改的配置文件刷新,每次都要删除编译文件才能生效

c. 文件加载

  • 传统方式调用 CSS 或 JS 文件时,采用 <link /><script></script> 标签实现,系统提供了更加智能的加载方式,方便加载 CSS 和JS 等文件

  • 使用 {load} 标签和 href 属性来链接,不需要设置任何其它参数

    {load href='__CSS__/css.css' /}
    {load href='__JS__/js.js' /}
    
  • 支持 href 多属性值的写法

    {load href='__CSS__/css.css,__JS__/js.js' /}
    
  • {load} 还提供了两个别名 {css}{js} 来更好的实现可读性

    {css href='__CSS__/css.css' /}
    {js href='__JS__/js.js' /}
    
    • {css}{js} 只是别名而已,识别 .css 还是 .js 是根据后缀的

(10)模板布局

  • 默认情况下不支持模版布局功能,在配置文件 view.php 中可以配置开始模版布局功能

    'layout_on' => true
    
  • 默认的布局文件是可以更改的,位置和名字均可自定义

    'layout_name' => 'public/layout'
    
  • layout.html 负责所有通用模块的代码部分,而局部内容通过变量引入

  • 使用 {__CONTENT__} 类似魔术方法的标签来引入 index.html 非通用内容

    {include file='public/header,public/nav' title='$title' keywords='关键字!' /}
    {__CONTENT__}
    
  • 可以更改 {__CONTENT__},需要在配置文件中配置

    'layout_item' => '{__REPLACE__}'
    
    • 再次测试的时候,如果更改了配置文件,务必删除 temp 下编译文件后刷新
  • 上述配置方法外,存在第二种方式启用布局,在 block.html 最上面加入以下代码

    {layout name="public/layout" replace='[__REPLACE__]'}
    

(11)模板继承

  • 模版继承是另一种布局方式,这种布局的思路更加的灵活

  • 首先,需要创建一个 public/base.html 的基模版文件,文件名随意

    <!DOCTYPE html>
    <html>
        <head>
            <meta charset="utf-8" />
            <title>{$title}</title>
        </head>
        <body>
        </body>
    </html>
    
  • 创建新方法 extend 载入新模板 extend.html,之后加载基模版

    {extend name='public/base'}
    
    {extend name='../view/public/base.html'}
    
  • 在基模版 base.html 中,设置几个可替换的区块部分,通过 {block} 标签实现

    {block name='nav'}nav{/block}
    {block name='nav'}{include file='public/nav'}{/block}
    {block name='footer'} @ThinkPHP 版权所有 {/block}
    
  • 在 base.html中,{include} 可以加载内容,而在 extend.html 可以改变加载

    {block name='include'}{include file='public/header'}{/block}
    

(12)模板杂项

  • 如果需要输出类似模版标签或语法的数据,这时会被模版解析,此时就使用模版的原样输出标签 {literal}

    {literal}
    {$name}
    {/literal}
    
  • 注释

    {//$name}
    
    {/*$name*/}
    
    {/*
    	$a
    	$b
    */}
    
    • 注释和 { 之间不能有空格,否则无法实现注释隐藏
    • 生成编译文件后注释的内容会自动被删除

(13)表单令牌

  • 表单令牌是在表单中增加一个隐藏字段,随机生成一串字符,确定不是伪造,这种随机产生的字符和服务器的 Session 进行对比,通过则是合法表单

    <form action="http://test.com/verify/token" method="post">
        <input type="hidden" name="__token__" value="{:token()}" />
        <input type="submit" value="Submit" />
    </form>
    
  • 为验证系统内部的机制,可以通过输出测试出内部的构造

    //打打印出保存到 session 的 token
    echo Session::get('__token__');
    
  • 在验证端口,可以使用控制器验证单独验证 token 是否验证成功

    $check = Request::checkToken('__token__');
    if ($check == false) {
        throw new ValidateException('Token Wrong');
    }
    

0x07 高级

(1)存储

a. Session

  • 在 app/middleware.php 中开启 Session

    // Session 初始化
    \think\middleware\SessionInit::class
    
  • TP6 不支持 PHP 原生的 $_SESSION 的获取方式,也不支持 session_ 前缀的函数

  • 直接使用 set()get() 方法设置 Session 的存取

    Session::set('user', 'Alex');	// 设置 Session,名称是 user,值是 Alex
    Session::get('user');			// 读取 Session,参数是名称
    Session::all();					// 读取 Session 所有内容
    Request::session('user');		// 读取 Session,参数是名称
    Request::session();				// 读取 Session 所有内容
    
  • get() 方法返回内容为 null 时,设置第二参数可以作为默认值

    Session::get('user');				// null
    Session::get('user', 'None');	// None
    
  • has() 方法可以判断是否赋值

    Session::has('user');
    
  • delete() 方法用于删除,pull() 方法用于取值后删除

    Session::delete('user');
    Session::has('user');
    
    Session::pull('user');
    Session::has('user');
    
  • clear() 方法用于清空 Session

    Session::clear('');
    
  • flash() 方法可以设置闪存数据,即只请求一次有效,再请求无效

    Session::flash('user', 'Alex');
    
  • 二维存在就是对象和数组的调用方式

    // 赋值(当前作用域)
    Session::set('obj.user', 'Alex');
    
    // 判断(当前作用域)是否赋值
    Session::has('obj.user');
    
    // 取值(当前作用域)
    Session::get('obj.user');
    
    // 删除(当前作用域)
    Session::delete('obj.user');
    
  • 使用助手函数操作更加方便

    // 赋值
    session('user', 'Alex');
    
    // 判断
    session('?user');
    
    // 输出
    echo session('user');
    
    // 删除
    session('user', null);
    
    // 清空
    session(null);
    
  • Cookie 是客户端存储,默认情况下在 config/cookie.php 是开启初始化的

  • 使用 set() 方法创建一个最基本的 cookie,可以设置前缀、过期时间、数组等

    Cookie::set('user', 'Alex');		
    Cookie::set('user', 'Alex', 20);	// 保存 20 秒
    Request::cookie('user');
    Request::cookie();
    
  • 使用 forever() 方法可以永久保存 Cookie

    Cookie::forever('user', 'Alex');
    
  • 使用 has() 方法可以判断是否赋值

    Cookie::has('user');
    
  • 使用 get() 方法用于取值

    Cookie::get('user');
    
  • 使用 delete() 方法用于删除

    Cookie::delete('user');
    
  • 使用助手函数操作更加方便

    cookie('user', 'Alex', 3600);	// 设置
    echo cookie('user');			// 输出
    cookie('user', null);			// 删除
    

c. 缓存功能

  • 系统内置了很多类型的缓存,除了 File,其它均需要结合相关产品,以下主要演示 File 文本缓存,其它的需要学习相关产品

  • 通过 cache.php 文件进行缓存配置,该文件默认生成在 runtime/cache 目录

  • 使用 set() 方法可以设置一个缓存

    Cache::set('user', 'Alex');			// 临时保存,关闭浏览器则消失
    Cache::set('user', 'Alex', 20);		// 保存 20 秒
    
  • 使用 has() 方法判断缓存是否存在

    Cache::has('user');
    
  • 使用 get() 方法从缓存中获取到相应的数据,无数据则返回 null

    Cache::get('user');
    
  • 使用 inc() 方法和 dec() 方法可以使用缓存数据的自增和自减操作

    Cache::inc('id');
    Cache::inc('age', 3);
    
    Cache::dec('id');
    Cache::dec('age', 3);
    
  • 使用 push() 方法可以实现缓存的数组数据追加的功能

    Cache::set('array', [1,2,3]);
    Cache::push('array', 4);		// [1,2,3,4]
    
  • 使用 delete() 方法可以删除指定的缓存文件

    Cache::delete('user');
    
  • 使用 pull() 方法可以取值后删除,无数据则返回 null

    Cache::pull('user');
    
  • 使用 remember() 方法可以当数据不存在时写入数据,可以依赖注入

    Cache::remember('start_time', time());
    
    Cache::remember('start_time', function (Request $request) {});
    
  • 使用 clear() 方法可以清除所有缓存

    Cache::clear();
    
  • 使用 tag() 方法可以将多个缓存归类到标签中,方便统一管理

    Cache::tag('tag')->set('user', 'Alex');
    
    Cache::tag('tag')->set('age', 18);
    
    Cache::tag('tag')->clear();
    
  • 使用助手函数操作更加方便

    cache('user', 'Alex', 3600);
    echo cache('user');
    cache('user', null);
    

d. 上传功能

  • 建立一个上传表单

    <form action="http://test.com/upload" enctype="multipart/form-data" method="post">
        <input type="file" name="image" />
        <input type="submit" value="Submit"
    </form>
    
  • 创建一个控制器 upload.php 并使用 Request::file 来获取上传数据

    $file = Request::file('image');
    
  • 使用 Filesystem::putFile() 方法可以实现上传文件并写入指定目录,上传后返回的结果 $info 可以输出当前上传文件的地址

    // 目录在 runtime/storage/toppic/[时间]/[文件]
    $info = Filesystem::putFile('topic', $file);
    
  • 可以在 config/filesystem.php 更改上传文件的默认配置

    'root' => app()->getRuntimePath() . 'storage'
    
  • 生成的规则还支持另外两种方式:md5 和 sha1

    $info = Filesystem::putFile('topic', $file, 'md5');
    
  • 批量上传,使用 image[] 作为名称,并使用 foreach() 遍历上传

    <input type="file" name="image[]" />
    <input type="file" name="image[]" />
    <input type="file" name="image[]" />
    
    $files = Request::file('image');
    $info = [];
    foreach ($files as $file) {
        $info[] = Filesystem::puFile('topic', $file);
    }
    dump($info);
    

(2)多语言

  • 在 middleware.php 中开启多语言切换功能

    \think\middleware\LoadLangPack::class,
    
  • 配置文件是 config/lang.php,默认设置的是 zh-cn 中文语言

    'zh-hans-cn' => 'zh-cn',
    'detect_var' => 'lang',		// 自动监测的变量为 lang
    
  • 默认应用目录会调用 app\lang 目录下的语言包

    // 错误消息,zh-cn.php
    return [
        'require_name' => '名字不能为空',
    ];
    
    // error message, en-us.php
    return [
        'require_name' => 'The name cannot be empty',
    ];
    
    // エラーメッセージ、ja-jp.php
    return [
        'require_name' => 'ユーザー名は空ではいけません',
    ];
    
  • 系统默认会指定 zh-cn 语言包,可以通过 get() 方法来输出错误信息

    Lang::get('require_name');
    lang('require_name');		// 助手函数
    
  • 可以通过 URL 方式来切换语言 ?lang=en-us,也可以在配置文件中设置允许的语言包

    'allow_lang_list' => ['zh-cn', 'en-us', 'ja-jp'];
    
  • 可以使用 {$Think.lang.xxxx} 在模板中调用语言信息

    {$Think.lang.require_name}
    {:lang('require_name')}
    
  • 可以在配置文件中开启多语言分组,运行使用二维数组来实现语言包定义

    'allow_group' => true,
    
    // en-us.php
    'user' => [
        'login' => 'Log in',
        'logout' => 'Log out'
    ]
    

(3)验证码

  • 需要开启 Session 并且通过命令 composer require topthink/think-captcha 引入

  • 测试引入结果(两种方法引入)

    <div>{:captcha_img()}</div>
    
    <div><img src="{:captcha_src()}" alt="captcha" /></div>
    
  • 举例:验证码验证

    • 创建模板,设置一个验证码以及文本框提交比对

      {:captcha_img()}
      
      <form action="../code/check" method="post">
          <input type="text" name="code" />
          <input type="submit" value="Submit" />
      </form>
      
    • 创建控制器

      <?php
      
      namespace app\controller;
      
      use think\facade\Validate;
      use think\facade\View;
      
      class Code
      {
          public function form()
          {
              return View::fetch('form');
          }
      
          public function check()	// 使用验证器进行验证码检测
          {
              $validate = Validate::rule([
                  'captcha' => 'required|captcha'
              ]);
              $result = $validate->check([
                  'captcha' => input('post.code')
              ]);
              if (!$result) {
                  dump($validate->getError());
              }
          }
      }
      
    • 使用助手函数操作更加方便

      public function check()
      {
          if (!captcha_check(input('post.code'))) {
              dump('Error');
          }
      }
      
  • 验证码所有配置参数

    参数 说明 默认值
    codeSet 验证码字符集合 【略】
    expire 验证码过期时间(单位:秒) 1800
    math 使用算术验证码 false
    useZh 使用中文验证码 false
    zhSet 中文验证码字符串 【略】
    useImgBg 使用背景图片 false
    fontSize 字体大小(单位:px) 25
    useCurve 是否使用混淆曲线 true
    useNoise 是否使用杂点 true
    imageH 图片高度(值为 0 表示自动) 0
    imageW 图片宽度(值为 0 表示自动) 0
    length 位数 5
    fontttf 字体 【空】
    bg 背景颜色 (243, 251, 254)
    reset 验证成功后是否重置 true
    • 配置文件在 config/captcha.php 中,直接进行参数配置即可
  • 自定义独立验证码

    • 创建控制器

      public function verify()
      {
          return Captcha::create('verify');
      }
      
    • 在 captcha.php 中设置 verify

      // 添加额外的验证码设置
       'verify' => [
           'length'=>4,
           'useZh' =>false,
      ],
      

(4)分页

  • 查找 user 表中的元所有数据,每页显示 5 条

    return View::fetch('index', [
        'list' => User::paginate(5)
    ]);
    
  • 创建模板,使用 {volist} 标签遍历列表

    <table border="1">
        <tr>
            <th>编号</th>
            <th>姓名</th>
            <th>年龄</th>
        </tr>
        {volist name='list' id='user'}
        <tr>
            <td>{$user.id}</td>
            <td>{$user.name}</td>
            <td>{$user.age}</td>
        </tr>
        {/volist}
    </table>
    
  • 分页功能提供了固定方式实现分页按钮,需要使用 CSS 调整样式

    {$list|raw}
    <ul class="pagination"></ul>
    
    .pagination {
        list-style: none;
        margin: 0;
        padding: 0;
    }
    .pagination li {
        display: inline-block;
        padding: 20px;
    }
    
  • 可以使用数组传递多个分页参数

    参数 说明
    list_rows 每页数量
    page 当前页
    path URL 路径
    query URL 额外参数
    fragment URL 锚点
    var_page 分页变量
    $list = User::paginate([
        'list_rows' => 4,
        'var_page' => 'page'
    ]);
    
  • 可以单独赋值分页的模板变量

    $page = $list->render();
    
    {$page|raw}
    
  • 可以单独获取到总记录数量

    $total = $list->total();
    
  • 如果使用了模型方式分页,则可以提供获取器修改字段值

    $list->each(function ($item, $key) {
        $item['gender'] = '【' . $item['gender'] . '】';
        return $item;
    });
    
  • 可以限定总记录数

    $list->paginate(5, 10);
    
  • 可以设置只有上下页选择

    $list->paginate(5, true);
    

(5)图像处理

  • 需要通过命令 composer require topthink/think-image 引入

  • 创建图像处理对象

    $image = Image::open('image.png');
    
  • 获取图像的各种属性

    • 图片宽度:$image->width();
    • 图片高度:$image->height();
    • 图片类型:$image->type();
    • 图片 Mime:$image->mime();
    • 图片大小:$image->size();
  • 使用 save() 方法可以保存图片:save('路径', ['类型', '质量', '是否隔行扫描'])

    save($pathname, $type=null, $quality=80, $interlace=true);
    
  • 使用 crop() 方法可以裁剪图片

    $image->crop(100, 100)->save('crop.png');
    
  • 使用 thumb() 方法可以生成缩略图

    $image->thumb(100, 100)->save('thumb.png');
    
    • 常量设置的定义

      const THUMB_SCALING = 1;	// 等比例缩放
      const THUMB_FILLED = 2;		// 缩放后填充
      const THUMB_CENTER = 3;		// 居中裁剪
      const THUMB_NORTHWEST = 4;	// 左上角裁剪
      const THUMB_SOUTHEAST = 5;	// 右下角裁剪
      const THUMB_FIXED = 6;		// 固定尺寸缩放
      
  • 使用 rotate() 方法可以旋转图片,默认 90 度

    $image->rotate(180)->save('rotate.png');
    
  • 使用 water() 方法可以添加图片水印

    $image->water('a.png')->save('water.png');
    
  • 使用 text() 方法可以添加文字水印

    $image->text('Alex', getcwd().'/1.ttf', 20, '#ffffff')->save('text.png');
    

(6)异常处理

  • TP 输出的异常信息比 PHP 原生的要人性化的多,但需要开启调试模式

  • 可以在 vendor/topthink/framework/src/tpl/think_exception.tpl 中可以修改异常页面的样式与布局等

  • 可以在 config/app.php 中替换掉异常页面

    'exception_tmpl' => app()->getThinkPath() . 'tpl/think_exception.tpl'
    
  • 手动异常抛出

    throw new Exception('异常消息', '异常内容');
    
  • 可以使用 try...catch 对可能发生的异常进行手动捕获和抛出

    try {
        echo 0/0;
    } catch (ErrorException $e) {
        echo "Error: " . $e->getMessage();
    }
    
  • 可以抛出 HTTP 异常

    throw new HttpException(404, 'Not Found');
    
    • 助手函数:abort(404, 'Not Found');
  • 在部署环境中可以设置 HTTP 错误页面

    'http_exception_template' => [
        404 => \think\facade\App::getAppPath() . '404.html'
    ]
    

(7)日志处理

  • 日志操作由 Log 类完成,其中记录着所有程序中运行的错误信息

  • 可以在 config/log.php 中设置日志信息,在 runtime/log 中按按年月查看日志

  • TP 提供了不同的日志级别,默认为 info,从低到高排列如下:

    graph LR debug-->info-->notice-->warning-->error-->critical-->alert-->emergency-->sql
  • 使用 record() 方法记录一条测试日志,可以在第二参数指定日志级别

    Log::record('测试日志', 'error');
    
    • 并非实时记录,需要等程序运行完毕后决定是否写入日志
  • 使用 close() 方法可以关闭日志写入

    Log::close();
    
  • 使用 write() 方法可以实时日志写入,无视 close() 方法

    Log::write("测试日志");
    
  • 除 HTTP 异常外的异常出现后会自动写入 error 日志

  • 快捷方式

    Log::error('Error');
    Log::info('Info');
    
  • 助手函数

    trace('Error', 'error');
    trace('Info', 'info');
    
  • 自定义日志类型

    Log::diy("自定义日志");
    
  • 在 log.php 中可以设置限定日志文件的级别,不属于的级别无法写入

    'level' => ['error', 'info']
    
  • 在 log.php 中添加转换为 JSON 格式

    'json' => true
    
  • 使用 getLog() 方法可以获取写入到内存中的日志

    $logs = Log::getLog();
    dump($logs);
    
  • 使用 clear() 方法可以清理掉内存中的日志

    Log::clear();
    
  • 在 log.php 中可以设置以单独文件存储的方式

    'single' => true
    

(8)中间件

a. 定义中间件

  • 中间件的主要用于拦截和过滤 HTTP 请求并进行相应处理,这些请求的功能可以是 URL 重定向、权限验证等

  • 使用命令 php think make:middleware Check 在应用目录下生成一个中间件文件和文件夹

    <?php
    declare (strict_types = 1);
    
    namespace app\middleware;
    
    use think\Response;
    
    class Check
    {
        /**
         * 处理请求
         *
         * @param \think\Request $request
         * @param \Closure       $next
         * @return Response
         */
        public function handle($request, \Closure $next)
        {
            if ($request->param('name') == 'index') {
                return redirect('../');
            }
            return $next($request);
        }
    }
    
  • 将上述中间件进行注册,在 app 目录下创建 middleware.php 并在其中配置中间件

    return [
        app\middleware\Check::class
    ];
    
  • 中间件的入口执行方法必须是 handle() 方法,第一参数是请求,第二参数是闭包

  • 业务代码判断请求的 name 如果等于 index,就拦截住并执行中间件后跳转到首页,但如果请求的 name 是其他,那需要继续往下执行才行,不能被拦截,那么就需要 $next($request) 把这个请求去调用回调函数

  • 中间件 handle() 方法规定需要返回 response 对象,才能正常使用;而 $next($request) 执行后,就是返回的 response 对象

  • 为了测试拦截后,无法继续执行,可以使用 return response(); 助手函数测试

b. 前/后置中间件

  • 上述中间件将 $next($request) 放在方法底部,称为前置中间件

  • 前置中间件是请求阶段来进行拦截验证,如登录跳转

    public function handle($request, \Closure $next)
    {
        return $next($request);
    }
    
  • 后置中间件是请求完毕后进行拦截验证,如写入日志

    public function handle($request, \Closure $next)
    {
        $response = $next($request);
        return $response;
    }
    

c. 结束调度

  • 中间件提供了 end() 方法,可以在中间件执行到最后时执行

    public function end(Response $response)
    {
        echo $response->getData();
    }
    

d. 路由中间件

  • 创建一个路由中间件,用于通过判断路由的 ID 值实现相应的验证

    <?php
    declare (strict_types = 1);
    
    namespace app\middleware;
    
    use think\Response;
    
    class Auth
    {
        /**
         * 处理请求
         *
         * @param \think\Request $request
         * @param \Closure       $next
         * @return Response
         */
        public function handle($request, \Closure $next)
        {
            if ($request->param('id') == 10) {
                echo 'Admin';
            }
            return $next($request);
        }
    }
    
  • 路由方法提供了 middleware() 方法,让指定的路由采用指定的中间件

    Route::rule('read/:id', 'Address/read')->middleware(\app\middleware\Auth::class);
    
    • 支持多个中间件

      ->middleware([Auth::class, Check::class]);
      
  • 可以在 config.middleware.php 中配置别名支持

    'alias' => [
        'Auth' => app\middleware\Auth::class,
        'Check' => app\middleware\Check::class
    ]
    
    ->middleware(['Auth', 'Check']);
    
  • 可以给中间件传递额外参数,通过中间件入口方法的第三个参数接收

    public function handle($request, \Closure $next, $param, $param='') {...}
    
    ->middleware(Auth::class, 'ok');
    
  • 支持分组路由、闭包路由等

e. 控制器中间件

  • 可以让中间件在控制器中注册,此时控制器执行时执行中间件

    protected $middleware = ['Check'];
    
  • 默认情况下,控制器中间件对所有操作方法有效,支持做限制

    protected $middleware = [
    	'Auth' => ['only' => ['index']],
    	'Check' =>['except' => ['read']]
    ];
    
  • 可以通过 Request 对象实现通过中间件给控制器传递参数

    $request->name = 'Alex';
    

(9)服务系统

  • 服务系统可以将一个类的对象注册到容器中去,方便调用执行

  • 服务的执行优先级较高,在执行主体程序前就已经完成依赖注入,它的作用可以做一些初始化,配置一些参数,扩展插件等等

    • 验证码扩展类就使用了服务系统
  • 举例:创建一个简单的服务

    • 在 app/common 目录下创建一个 Shut.php 类,这个类是被服务的类

      <?php
      
      namespace app\common;
      
      class Shut
      {
          // 定义一个属性字段
          protected static $name = 'Alex';
      
          // 设置
          public static function setName(string $name): void
          {
              self::$name = $name;
          }
      
          // 获取
          public function run()
          {
              halt(self::$name . ' Shut');
          }
      }
      
    • 使用命令 php think make:service ShutService 生成一个对 Shut.php 的服务类 ShutService.php

      <?php
      declare (strict_types = 1);
      
      namespace app\service;
      
      use app\common\Shut;
      
      class ShutService extends \think\Service
      {
          /**
           * 注册服务
           *
           * @return mixed
           */
          public function register()
          {
              // 绑定到容器,将被服务的类注册到容器中
          	$this->app->bind('shut', Shut::class);
          }
      
          /**
           * 执行服务
           *
           * @return mixed
           */
          public function boot()
          {
              // 执行
              Shut::setName('Bob');
          }
      }
      
    • 将系统服务配置到全局定义文件 service.php 里

      return [
          \app\service\ShutService::class
      ];
      
    • 在控制器中测试

      public function index(Shut $shut)
      {
          // 依赖注入调用
          $shut->run();
          
          // 容器标识调用
          $this->app->shut->run();
          
          return 'index';
      }
      

(10)事件

  • 事件和中间件相似,区别在于事件的定位更加精准、业务场景更细腻

  • 事件可定义,分为:事件类、事件监听类、事件订阅类

  • 示例:手动创建一个测试类

    <?php
    
    namespace app\controller;
    
    use think\facade\Event;
    
    class TestEvent
    {
        public function __construct()
        {
            // 注册监听器
            Event::listen('TestListen', function ($param) {
                echo 'Listener: ' . $param;
            });
        }
    
        public function info()
        {
            echo 'Ready';
    
            // 触发监听器
            // Event::trigger('TestListen', 'ok');
            event('TestListen');    // 助手函数方式
        }
    }
    
  • 可以使用监听类设计监听器,使用命令 php think make:listener TestListen 创建

    public function info()
    {
        echo 'Ready';
        Event::listen('TestListen', TestListen::class);
        Event::trigger('TestListen');
    }
    
    <?php
    declare (strict_types = 1);
    
    namespace app\listener;
    
    class TestListen
    {
        /**
         * 事件监听处理
         *
         * @return mixed
         */
        public function handle($event)
        {
            //
        }
    }
    
  • 在 app/event.php 中配置监听器类

    'listen' => [
        'TestListen' => [\app\listener\TestListen::class]
    ]
    
  • 监听类被触发会自动执行 handle() 方法实现监听功能

    public function handle($event)
    {
        echo 'Listen: ' . $event;
    }
    
  • 内置的系统触发事件

    事件 说明 参数
    AppInit 应用初始化标签位
    HttpRun 应用开始标签位
    HttpEnd 应用结束标签位 当前响应对象实例
    LogWrite 日志 write() 方法标签位 当前写入的日志信息
    RouteLoaded 路由加载完成
  • 事件监听类可以同时监听多个类,只需要绑定到一个标识符即可

    'TestListen' => [
        \app\listener\TestOne::class,
        \app\listener\TestTwo::class,
        \app\listener\TestThree::class
    ]
    

    对于需要多个监听,监听类可能不够灵活,而且类会创建很多,此时可以使用订阅类

  • 订阅类是将监听事件作为内部的方法用 on+方法名 来实现,使用命令 php think make:subscribe UserSub 创建

    class UserSub
    {
        public function onUserLogin() {
            echo 'Login';
        }
        
        public function onUserLogout() {
            echo 'Logout';
        }
    }
    
  • 在 app/event.php 中注册订阅类

    'subscribe' => [
        'UserSub' => \app\subsrcibe\UserSub::class
    ]
    
  • 在事件类 TestEvent.php 中使用

    public function login()
    {
        echo 'LoginEvent';
        event('UserLogin');
    }
    
    public function logout()
    {
        echo 'LogoutEvent';
        event('UserLogout');
    }
    
  • 使用命令 php think make:event UserEvent 创建事件类

(11)多应用模式

  • 需要通过命令 composer require topthink/think-mulit-app 引入

  • 引入后创建并在 index 和 admin 两个应用目录文件夹,将 controller 和 model 移入并修改相应的命名空间

  • 在 view 中也创建 index 和 admin 两个应用目录文件夹

  • 默认的应用为 index,可以在 app.php 中修改

    'default_app' => 'index'
    
  • 应用映射

    • 示例:将 admin 目录映射为 think 并将 admin 废弃

      'app_map' => [
          'think' => 'admin'
      ]
      
  • 域名绑定

    • 示例:后台用域名绑定并直接访问

      'domain_bind' => [
          'www.test.com' => 'admin',
          '*' => 'index'
      ]
      
  • 路由修改:需要在应用目录单独建立路由,内部编码不需要更改

-End-