thinkphp8使用指南

模型的新增和删除

19. 模型的新增和删除

1. 新增操作

  • 用模型新增数据,首先要实例化模型,开发工具会补全use,非集成工具别忘了;

    use app\model\User;  // 手册new User,这里括号是工具补全的,都可以
    $user = new User();
    
  • 使用实例化的方式添加一条数据,并使用 save() 方法进行保存;

  • 注意:使用模型时,会自动给时间字段 create_timeupdate_time 写入当前时间;

    $user = new User();
    $user->name = "李白";
    $user->age = 28;
    $user->gender = "男";
    $user->details = "床前明月光,好诗!";  // 成功返回true,失败抛异常,其它看手册
    $user->save();
    
  • 也可以通过 save() 传递数据数组的方式,来新增数据;

    $user->save([
        "name"  =>  "杜甫",
        "age"   =>  19,
        "gender"  =>  "男",
        "details" =>  "一行白鹭上青天,好诗!"
    ]);
    
  • 使用 allowField() 方法,允许要写入的字段,其它字段就无法写入了;

    $user->allowField(["name","age","gender"])->save([
        "name"  =>  "蒲松龄",
        "age"   =>  25,
        "gender"  =>  "男",
        "details" =>  "十里平湖霜满天,好诗!"
    ]);
    
  • 模型新增也提供了 replace() 方法来实现 REPLACE into 新增;

    $user->replace()->save([
        "id"    =>  15,
        "name"  =>  "蒲松龄",
        "age"   =>  25,
        "gender"  =>  "男",
        "details" =>  "十里平湖霜满天,好诗!"
    ]);
    
  • 当新增成功后,使用 $user->id ,可以获得自增 ID(主键需是 id);

    return $user->id;
    
  • 使用 saveAll()方法,可以批量新增数据,返回批量新增的数组;

    return $user->saveAll([
        [
            "name"  =>  "赵六",
            "age"   =>  19,
            "gender"=>  "男"
        ],
        [
            "name"  =>  "钱七",
            "age"   =>  22,
            "gender"=>  "男",
            "details"   =>  "我很有钱,排行老七!"
        ]
    ]);
    
  • 使用 ::create() 静态方法,来创建要新增的数据;

    
    $user = User::create([
        "name"  =>  "李逍遥",
        "age"   =>  18,
        "gender"=>  "男",
        "details"   =>  "我是一代主角!"
    ], ["name", "age", "gender", "details"], false);  //参数 1 是新增数据数组,必选
    //参数 2 是允许写入的字段,可选
    //参数 3 为是否 replace 写入,可选,默认 false 为 Insert 写入  return $user->id;
    

2. 删除操作

  • 使用 find() 方法,通过主键 (id) 查询到想要删除的数据;

  • 然后再通过 delete()方法,将数据删除,返回布尔值;

    // 根据主键值,删除数据
    $user = User::find(20);
    return $user->delete();
    
  • 也可以使用静态方法调用 destroy()方法,通过主键(id)删除数据;

    // 单条删除
    return User::destroy(21);
    // 批量删除
    return User::destroy([22, 33, 44]);
    // 条件删除
    return User::where("id", ">", 15)->delete();
    
  • destroy() 方法,使用闭包的方式进行删除;

    // 闭包模式
    User::destroy(function ($query) {
        $query->where("id", ">", 15);
    });
    

20. 模型的更新操作

1. 数据更新

  • 使用 find()方法获取数据,然后通过 save()方法保存修改,返回布尔值;

    $user = User::find(19);
    $user->details = "我是一代主角!";
    return $user->save();
    
  • 通过 where()方法结合 find()方法的查询条件获取的数据,进行修改;

    $user = User::where("name", "李逍遥")->find();
    $user->details = "我想做二代主角!";
    return $user->save();
    
  • save()方法只会更新变化的数据,如果提交的修改数据没有变化,则不更新;

  • 但如果你想强制更新数据,即使数据一样,那么可以使用 force()方法;

    // 如何验证被强制了,查看update_time字段是否更新了
    $user->force()->save();
    
  • Db::raw()执行 SQL 函数的方式,同样在这里有效;

    $user->age = Db::raw("age + 2");
    
  • 关于验证过滤,后续学习Request再说,手册中 模型 -> 更新 里也有说明:

    $user->allowField(["name","age"])->save(...)
    
  • 通过 saveAll()方法,可以批量修改数据,返回被修改的数据集合;

    $user = new User;
    return $user->saveAll([
        ["id"=>17, "gender"=>"女"],
        ["id"=>18, "gender"=>"女"],
        ["id"=>19, "gender"=>"女"],
    ]);
    
  • 使用静态方法::update()更新,返回的是对象实例;

    return User::update(["id"=>17, "gender"=>"男"]);
    // ID放在后面,返回数据不含ID
    return User::update(["gender"=>"男"], ["id"=>18]);
    // 限制更新的内容,只允许gender被修改
    return User::update(["gender"=>"男", "name"=>"可笑的人"], ["id"=>19], ["gender"]);
    

    21. 模型的查询都一样

1. 模型的查询

  • 模型的绝大部分语法基本都来自于 Db::name() 的查询:

  • 在手册 模型 -> 查询 中可以查阅,这里就演示几个常用的意思一下:

  • find() 单个 和 select() 多个;

    $user = User::find(1);
    $user = User::select();
    $user = User::select([1, 3, 5]);
    
  • 也可以使用 where()方法进行条件筛选查询数据;

    $user = User::where("id", "<", 5)->select();
    
  • 模型方法也可以使用 where 等连缀查询,和数据库查询方式一样;

    $user = User::limit(3)->order("id", "desc")->select();
    
  • 模型支持聚合查询:max、min、sum、count、avg 等;

    $user = User::count();
    
  • 模型也支持大量的快捷方式,这里演示一个:

    $user = User::whereLike("name", "%王%")->select();
    

22. 模型的字段设置

1. 字段设置

  • 模型的数据字段和表字段是对应关系,默认会自动获取,包括字段的类型;

  • 自动获取会导致增加一次查询,如果在模型中配置字段信息,会减少内存开销;

  • 可以在模型设置$schema 字段,明确定义字段信息,字段需要对应表写完整;

  • 字段类型的定义可以使用PHP类型或者数据库的字段类型都可以,以便自动绑定类型;

    // 设置字段信息,需要写完整的数据表字段
    protected $schema = [
        "id"    =>  "int",
        "name"  =>  "string",
        ...
    ];
    
  • 设置模型字段,只能对模型有效,对于 Db::name() 查询无法作用。

  • 要让模型和Db查询都支持字段类型设置,分三步:

    • 把上面的$schema先注释掉;

    • 在 config/database.php 开启缓存字段;

      // 开启字段缓存
      "fields_cache"    => true,
      
    • 在根目录命令行执行命令:

      php think optimize:schema
      

2. 废弃字段

  • 由于历史遗留问题,我们不再想使用某些字段,可以在模型里设置;

  • 设置后,我们在查询和写入时将忽略这些字段;

    // 设置废弃字段
    protected $disuse = ["age", "details"];
    

3. 只读字段

  • 只读字段用来保护某些特殊的字段值不被更改,这个字段的值一旦写入,就无法更改;

    // 设置只读字段
    protected $readonly = ["age", "details"];
    
  • 然后在控制器端进行修改测试:

    // 修改查看只读字段
    return User::update(["id"=>19, "age"=>22, "name"=>"李逍遥2", "details"=>"可笑"]);
    

23. 获取器和修改器

1. 获取器

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

  • 一个获取器对应模型的一个特殊方法,该方法为 public;

  • 方法名的命名规范为:getFieldAttr();

  • 举个例子,数据库表示状态 status 字段采用的是数值;

  • 而页面上,我们需要输出 status 字段希望是中文,就可以使用获取器;

  • 在 User 模型端,我创建一个对外的方法,如下:

    // 获取器,改变字段的值
    public function getStatusAttr($value)
    {
        $status = [-1=>"删除", 0=>"冻结", 1=>"正常", 2=>"待审核"];
        return $status[$value];
    }
    
  • 控制器端,正常输出数据:

    public function attr()
    {
        $user = User::select();
        return json($user);
    }
    
  • 如果你定义了获取器,并且想获取原始值,可以使用 getData()方法;

    $user = User::find(1);
    echo $user->getData("status");
    
  • 使用 withAttr 在控制器端实现动态获取器,比如让年龄+100岁;

    // 可以传入参数二 $data,获得所有数据,方便数据获取和判断
    $user = User::select()->withAttr("age", function($value) {
        return $value + 100;
    });
    

2. 修改器

  • 模型修改器的作用,就是对模型设置对象的值进行处理;

  • 比如,我们要新增数据的时候,对数据就行格式化、过滤、转换等处理;

  • 模型修改器的命名规则为:setFieldAttr

  • 我们要设置一个新增,规定输入的年龄都自动+100岁,修改器如下:

    // 修改器,写入时改变字段的值
    public function setAgeAttr($value)
    {
        return $value + 100;
    }
    
    return User::create([
        "name"  =>  "酒剑仙",
        "age"   =>  58,
        "gender"=>  "男",
        "details"   =>  "我是隐藏主角!"
    ]);
    
  • 除了新增,会调用修改器,修改更新也会触发修改器;

  • 模型修改器只对模型方法有效,调用数据库的方法是无效的,比如->insert();

24. 搜索器和自动时间戳

1. 搜索器

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

  • 一个搜索器对应模型的一个特殊方法,该方法为 public;

  • 方法名的命名规范为:searchFieldAttr()

  • 举个例子,我们要封装一个 name 字段的模糊查询,然后封装一个时间限定查询;

  • 在 User 模型端,我创建两个对外的方法,如下:

    // 搜索器,模糊查找姓名
    public function searchNameAttr($query, $value, $data)
    {
        $query->where("name", "like", "%".$value."%");
    }  // 搜索器,限定时间
    public function searchCreateTimeAttr($query, $value, $data)
    {
        $query->whereBetweenTime("create_time", $value[0], $value[1]);
    }
    
  • 在控制器端,通过 withSearch()方法实现模型搜索器的调用;

    $user = User::withSearch(["name", "create_time"],[
        "name"          =>  "李",
        "create_time"   =>  ["2023-10-19", "2023-10-20 23:59:59"]
    ])->select();
    
  • withSearch()中第一个数组参数,限定搜索器的字段,第二个则是表达式值;

  • 如果想在搜索器查询的基础上再增加查询条件,直接使用链式查询即可;

    User::withSearch(...)->where("gender", "女")->select();
    
  • 在获取器和修改器都有一个 $data 参数,它的作用是什么?

    // 搜索器,模糊查找姓名
    public function searchNameAttr($query, $value, $data)
    {
        $query->where("name", "like", "%".$value."%");
        // 按年龄排序
        if (isset($data["sort"])) {
            $query->order($data["sort"]);
        }
    }
    

2. 自动时间戳

  • 如果你想全局开启,在 database.php 中,设置为 true;

  • 此时,写入操作时,会自动对 create_timeupdate_time 进行写入;

    "auto_timestamp"  => true,
    
  • 如果你只想设置某一个模型开启,需要设置特有字段;

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

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

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

    protected $updateTime = false;
    
  • 也可以动态实现不修改 update_time,具体如下:

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

    25. 软删除和事件

1. 软删除

  • 软删除也称为逻辑删除,只是给数据标记 “已删除” 的状态,不是真实的物理删除;

  • 为何要对数据进行软删除,因为真实的物理删除,删了就没了呀。

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

    // 会自动引入SoftDelete
    use think\model\concern\SoftDelete;  // 开启软删除,创建delete_time字段,并设置默认为 NULL
    use SoftDelete;
    protected $deleteTime = "delete_time";
    
  • delete_time 默认设置的是 null,如果你想更改这个默认值,可以设置:

    protected $defaultSoftDelete = 0;
    
  • 由于我们之前演示过字段缓存,会导致无法软删除,你可以删除字段缓存,或者重新更新下:

    php think optimize:schema
    
  • 删除分为两种:destroy() 和 delete(),具体如下:

    // 软删除
    User::destroy(1);
    // 真实删除
    User::destroy(1,true);  $user = User::find(1);
    // 软删除
    $user->delete();
    // 真实删除
    $user->force()->delete();
    
  • 软删除后,数据库内的数据只是被标记了删除时间,而搜索数据时,会自动屏蔽这些数据;

  • 在开启软删除功能的前提下,使用 withTrashed() 方法取消屏蔽软删除的数据;

    User::withTrashed()->select();
    
  • 如果只想查询被软删除的数据,使用 onlyTrashed()方法即可;

    User::onlyTrashed()->select()
    
  • 如果想让某一条被软删除的数据恢复到正常数据,可以使用 restore()方法;

    $user = User::onlyTrashed()->find(23);
    $user->restore();
    
  • 如何要将软删除后的数据库真实的物理删除,需要先将它恢复,再真实删除;

2. 事件

  • 在手册 -> 模型 -> 模型事件中罗列了所有的模型,这里演示常用的意思一下;

  • 在模型端使用静态方法调用即可完成事件触发,比如执行了查询方法后调用事件:

    // 当执行了查询方法,则调用我
    protected static function onAfterRead($user)
    {
        // $query是数据集,可用于数据判断
        echo "执行了查询!".$user->id;
    }
    

26. 关联模型入门

1. 关联表

  • 我们已经有了一张 tp_user 表,主键为:id;我们需要一个附属表,来进行关联;

  • 附属表:tp_profile,建立两个字段:user_idhobby,外键是 user_id

    ### 2. 关联查询

  • 关联模型,顾名思义,就是将表与表之间进行关联和对象化,更高效的操作数据;

  • 创建 User 模型和 Profile 模型,均为空模型;如果已有User,改名UserBak备份起来;

    namespace app\model;
    use think\Model;  class User extends Model {}
    
    namespace app\model;
    use think\Model;  class Profile extends Model {}
    
  • User 模型端,需要关联 Profile,具体方式如下:

    class User extends Model {
        public function profile()
        {
            // 一对一关联,
            // 参数1:关联的表模型
            // 参数2:默认为 user_id (外键)
            return $this->hasOne(Profile::class);
        }
    }
    
  • 创建一个控制器用于测试输出:Link.php;

    public function index()
    {
        // 主表
        $user = User::find(19);
        // 访问关联从表
        return json($user->profile);
    }
    

27. 一对一关联查询

1. hasOne 模式

  • hasOne 模式,适合主表关联附表,具体设置方式如下:

    hasOne("关联模型",["外键","主键"]);
    return $this->hasOne(Profile::class,"user_id", "id");  //关联模型(必须):关联的模型名或者类名
    //外键:默认的外键规则是当前模型名(不含命名空间,下同)+_id ,例如 user_id
    //主键:当前模型主键,默认会自动获取也可以指定传入
    
  • 在上一节课,我们了解了表与表关联后,实现的查询方案:

    // 主表
    $user = User::find(19);
    // 访问关联从表
    return json($user->profile->hobby);
    
  • 使用 save()方法,可以设置关联修改,通过主表修改附表字段的值:

    $user = User::find(19);
    return $user->profile->save(["hobby"=>"和蛇妖玩耍!"]);
    
  • ->profile 属性方式可以修改数据,->profile()方法方式可以新增数据:

    // 新增附表数据,先找到主表数据
    $user = User::find(1);
    // 然后通过profile()方法实现新增
    return $user->profile()->save(["hobby"=>"不喜欢吃青椒!"]);
    

2. belongsTo 模式

  • belongsTo 模式,适合附表关联主表,具体设置方式如下:

    // 注意:此时绑定需要Profile模型创建user()方法执行
    belongsTo("关联模型",["外键","关联主键"]);  class Profile extends Model {
        public function user()
        {
            return $this->belongsTo(User::class);
        }
    }
    
  • 查询方式,和主附查询方式一致:

    // 附表
    $profile = Profile::find(1);
    // 访问关联主表
    return $profile->user->name;
    
  • 使用 hasOne()也能模拟 belongsTo() 来进行查询,这样就可以不用在 Profile 模型进行设置:

    // hasWhere
    // 注意:参数1的profile是方法名,不是模型名
    $user = User::hasWhere("profile", ["id"=>2])->find();
    return json($user);  
    // 闭包方式
    $user = User::hasWhere("profile", function ($query) {
        $query->where("id", 2);
    })->find();  return json($user);
    

    28. 一对多关联查询

1. hasMany 模式

  • hasMany 模式,适合主表关联附表,实现一对多查询,具体设置方式如下:

    // 由于是一对多,需要在附表添加多个相同user_id的数据测试
    hasMany("关联模型",["外键","主键"]);
    return $this->hasMany(Profile::class,"user_id", "id");
    
  • 查询方案和一对一相同:

    // 主表一对多
    $user = User::find(1);
    return json($user->profile);
    
  • 使用->profile()方法模式,可以进一步进行数据的筛选;

    // 主表一对多
    $user = User::find(1);
    // 进一步筛选数据,保留实际顺序的下标
    $data = $user->profile->where("id", ">", 10);
    // 进一步筛选数据,下标重新从0开始,需要连缀select()
    $data = $user->profile()->where("id", ">", 10)->select();
    return json($data);
    
  • 使用 has()方法,查询关联附表的主表内容,比如大于等于 2 条的主表记录;

    // 参数1:profile是方法名
    $user = User::has("profile", ">=", 2)->select();
    return json($user);
    
  • 使用 hasWhere()方法,查询附表中,可见兴趣的主表记录;

    // 查询附表profile中visible为1的兴趣关联主表的记录
    $user = User::hasWhere("profile", ["visible"=>1])->select();
    return json($user);
    
  • 使用 save()和 saveAll()进行关联新增和批量关联新增,方法如下:

    // 主表数据
    $user = User::find(24);
    // 新增附表关联数据
    $user->profile()->save(["hobby"=>"测试喜欢1", "visible"=>1]);  // 批量新增
    $user->profile()->saveAll([
        ["hobby"=>"测试喜欢2", "visible"=>1],
        ["hobby"=>"测试喜欢3", "visible"=>1],
    ]);
    
  • 使用 together()方法,可以删除主表内容时,将附表关联的内容全部删除;

    // 删除主数据,并清空关联附表数据
    $user = User::with("profile")->find(29);
    $user->together(["profile"])->delete();
    
  • 特别注意:由于外键约束设置问题,默认情况下,关联操作可能会导致1451错误;

  • 解决方案:在 profile 表中 设置外键 删除和修改时 为:CASCADE即可,详细阅读如下:

    https://blog.csdn.net/qq_23994787/article/details/86063623
    

29. 模型预载入和统计

1. 预载入

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

    // 主表三条记录
    $list = User::select([10, 11, 12]);
    // 遍历附表
    foreach ($list as $user)
    {
        dump($user->profile);
    }
    
  • 上面继续采用普通关联查询的构建方式,打开 trace 调试工具,会得到四次查询;

  • 如果采用关联预载入的方式,将会减少到两次,也就是起步一次,循环一次;

    $list = User::with(["profile"])->select([10, 11, 12, 17, 18, 19]);
    foreach ($list as $user)
    {
        dump($user->profile);
    }
    
    // 查看显示结构
    $list = User::with(["profile"])->select([1, 10, 22]);
    return json($list);
    
  • 关联预载入减少了查询次数提高了性能,但是不支持多次调用;

  • 如果你有主表关联了多个附表,都想要进行预载入,可以传入多个模型方法即可;

    User::with(["profile", "book"])
    
  • 上面显示结构中,主表和附表的字段已经非常多了,需要对两个表字段进行筛略:

    // 注意1:withField对应另一个是withoutField
    // 注意2:关联字段一定要包含外键:user_id,否则空
    $list = User::field("id,age,gender,details")->with(["profile" => function($query) {
        $query->withField(["user_id, hobby"]);
    }])->select([1, 10, 22]);
    return json($list);  
    // 或者简单些
    $list = User::with("profile")->select();
    // 直接字段,隐藏主表,加上profile隐藏附表,除了hidden,还有对应的visible方法
    return json($list->hidden(["status", "profile.visible"]));
    
  • 还有一些 预载入缓存、延迟预载入,可以参考手册;

2. 关联统计

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

    // 统计这三条数据关联的附表数据的个数
    $list = User::withCount(["profile"])->select([1, 10, 22]);
    foreach ($list as $user)
    {
        echo $user->profile_count."<br>";
    }
    
  • 关联统计的输出采用“关联方法名” _count,这种结构输出;

  • 不单单支持 Count,还有如下统计方法,均可支持;

  • withMax()withMin()withSum()withAvg()等;

  • 除了 withCount()不需要指定字段,其它均需要指定统计字段;

    // 统计附表关联字段的累加和
    $list = User::withSum(["profile"], "visible")->select([1, 10, 22]);
    foreach ($list as $user) {
        echo $user->profile_sum."<br>";
    }
    
  • 对于输出的属性,可以自定义:

    User::withSum(["profile"=>"p_s"], "visible")
    $user->p_s
    

    30. 多对多关联查询

1. 建立三张表

  • 复习一下一对一,一个用户对应一个用户档案资料,是一对一关联;

  • 复习一下一对多,一篇文章对应多个评论,是一对多关联;

  • 多对多怎么理解,分解来看,一个用户对应多个角色,而一个角色对应多个用户;

  • 那么这种对应关系,就是多对多关系,最经典的应用就是权限控制;

  • tp_user:用户表;tp_role:角色表;tp_access:中间表;

  • access 表包含了 user 和 role 表的关联 id,多对多模式;

    class User extends Model {
        public function role()
        {
            // belongsToMany("关联模型","中间表",["外键","关联键"]);
            return $this->belongsToMany(Role::class, Access::class);
        }
    }
    
    class Role extends Model{}
    

    php // 这里继承的是Pivot,它本身也继承了Model // Pivoit是中间表基类,多对多专用模型 class Access extends Pivot{}### 2. 权限控制

  • 在控制器段,我们查询一下id为1的用户,并关联查询它的权限:

    // 获取一个用户,张三
    $user = User::find(1);
    // 获取这个用户所有角色
    $role = $user->role;
    return json($role);
    
  • 当我们要给一个用户创建一个角色时,用到多对多关联新增;

    // 如果这个角色不存在,则会给角色表增加一条信息
    // 并且,会在中间表关联角色和用户
    $user->role()->save(["type"=>"测试管理专员"]);  // 也支持批量
    $user->role()->saveAll([[...],[...]]);
    
  • 一般来说,上面的这种新增方式,用于初始化角色比较合适;

  • 但是,很多情况下,角色权限是初始化好的,只需要添加中间表,而不是角色表;

  • 那么,我们真正需要就是通过用户表新增到中间表关联即可;

    // 给张三添加一个已经存在的角色,直接传角色ID即可
    $user->role()->save(1);  // 或
    $user->role()->save(Role::find(1));
    $user->role()->saveAll([1,2,3]);   // 或,如果有其它字段,可以通过中括号添加
    $user->role()->attach(1);
    $user->role()->attach(2, ["details"=>"测试详情"]);
    
  • 除了新增,还有直接删除中间表数据的方法:

    // 取消掉张三的所有,1,2,6,这里的值是角色的ID,不是中间表ID
    $user->role()->detach(1);
    $user->role()->detach([2, 6]);
    

    31. 路由定义入门

1. 路由入门

  • 路由的作用就是让 URL 地址更加的规范和优雅,或者说更加简洁;

  • 设置路由对 URL 的检测、验证等一系列操作提供了极大的便利性;

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

    // 是否启用路由
    "with_route"       => true,
    
  • 路由的配置文件在 config/route.php 中,定义文件在 route/app.php

  • 我们还回到最初的 Index 控制器,创建一个 details 带 参数的方法;

    public function details($id)
    {
        return "details ID:".$id;
    }
    
    http://www.tp.com:8000/index/details/id/5
    
  • 此时,我们在根目录 route 下的 app.php 里配置路由:

    // 参数1:url/参数
    // 参数2:控制器/方法
    Route::rule("details/:id", "Index/details");  // 访问地址
    http://www.tp.com:8000/details/5
    
  • rule()方法是默认请求是 any,即任何请求类型均可,第三参数可以限制:

    Route::rule("details/:id", "Index/xxx", "GET");
    Route::rule("details/:id", "Index/xxx", "POST");
    Route::rule("details/:id", "Index/xxx", "GET|POST");
    
  • 所有的请求方式均有快捷方式,比如 ::get()::post() 等,具体查看手册:路由 -> 路由定义;

    // 快捷方式,无须第三参数了
    Route::get(...)
    Route::post(...)
    Route::delete(...)
    
  • 在路由的规则表达式中,有多种地址的配置规则:

    // 静态路由
    Route::rule("test", "Index/test");  // 带一个参数
    Route::rule("details/:id", "Index/details");  // 带两个参数
    Route::rule("details/:id/:uid", "Index/details");  // 带可选参数,一般在后面
    Route::rule("details/:id/[:uid]", "Index/details");  // 全动态地址,不限定details,url可以是:abc/5/6,后面details也可以动态
    Route::rule(":details/:id/:uid", "Index/details");  // 正则规则,完全匹配
    Route::rule("details/:id/:uid$", "Index/details");
    

    2. 强制路由

  • 目前来说,路由访问模式和URL访问模式都可以使用,但我们强制路由访问;

  • 开始强制路由,需要在 route.php 里面进行配置:

    // 是否强制使用路由
    "url_route_must"        => true,
    
    // 首页也必须设置路由
    Route::rule("/", "Index/index");
    

32. 路由闭包.变量规则

1. 闭包

  • 闭包支持我们可以通过 URL 直接执行,而不需要通过控制器和方法;

    // 闭包
    Route::get("think", function () {
        return "hello, ThinkPHP8!";
    });
    
  • 闭包支持也可以传递参数和动态规则:

    // 带参数闭包,如果不带:version,那么地址:php?version=8
    Route::get("php/:version", function ($version) {
        return "PHP".$version;
    });
    

2. 变量规则

  • 系统默认的路由变量规则为\w+,即字母、数字、中文和下划线;

  • 如果你想更改默认的匹配规则,可以修改 config/route.php 配置;

    // 默认的路由变量规则
    "default_route_pattern" => "[\w\.]+",
    
  • 如果我们需要对于具体的变量进行单独的规则设置,则需要通过 pattern() 方法;

  • 将 details 方法里的 id 传值,严格限制必须只能是数字\d+;

    // 正则规则 \d+ 限定id为数字
    Route::rule("details/:id", "Index/details")
                ->pattern(["id"=>"\d+"]);  // 多个参数
    ->pattern([
        "id"  => "\d+",
        "uid" => "\d+"
    ]);
    
  • 如果让指定的参数统一限定为数字,比如id和uid,也就是全局设置,在app.php顶部设置:

    Route::pattern([
        "id"  => "\d+",
        "uid" => "\d+"
    ]);
    
  • 不喜欢斜杠怎么办?能换成减号吗?可以的:

    // 支持替换斜杠
    Route::rule("details-:id", "Index/details");
    
    // 支持组合变量<id>方式
    Route::rule("details-<id>", "Index/details");
    

    33. 路由参数.域名.MISS

1. 参数

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

  • ext 方法作用是检测 URL 后缀,比如:我们强制所有 URL 后缀为.html;

    Route::rule("test", "Index/test")->ext("html");
    Route::rule("test", "Index/test")->ext("html|shtml");
    
  • https 方法作用是检测是否为 https 请求,结合 ext 强制 html 如下:

    Route::rule("test", "Index/test")->ext("html")->https();
    
  • 如果想让全局统一配置 URL 后缀的话,可以在 config/route.php 中设置;

  • 具体值可以是单个或多个后缀,也可以是空字符串(任意后缀),false 禁止后缀;

    // URL伪静态后缀
    "url_html_suffix"       => "html",
    
  • denyExt 方法作用是禁止某些后缀的使用,使用后直接报错;

    // 可以将url_html_suffix 设置为空测试
    Route::rule("test", "Index/test")->denyExt("jpg|gif|png");
    
  • domain 方法作用是检测当前的域名是否匹配,完整域名和子域名均可;

    Route::rule("test", "Index/test")->domain("localhost");
    Route::rule("test", "Index/test")->domain("new.tp.com");
    Route::rule("test", "Index/test")->domain("new");
    
  • 还有ajax/pjax/json检测、filter 额外参数检测、append追加额外参数、option统一管理检测,可参考手册;

    Route::rule("test", "Index/test")->option([
        "ext"       =>  "html",
        "https"     =>  false,
        "domain"    =>  "www.tp.com"
    ]);
    

2. 域名

  • 如果想限定的某个域名下生效的路由,比如 news.tp.com 可以通过域名闭包方式:

    Route::domain("news.tp.com", function () {
        Route::rule("details/:id", "Index/details");
    });
    // 或
    Route::domain("news", function () {
        Route::rule("details/:id", "Index/details");
    });
    
  • 路由域名也支持:ext、pattern、append 等路由参数方法的操作;### 3. MISS

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

    Route::miss("Error/miss");  // 闭包模式
    Route::miss(function () {
        return "MISS 404";
    });
    

34. 路由分组.URL生成

1. 分组

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

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

    Route::group("index", function () {
        Route::rule(":id", "Index/details");
        Route::rule(":name", "Index/hello");
    })->ext("html")->pattern(["id"=>"\d+"]);  // URL1: http://www.tp.com:8000/index/5.html
    // URL2: http://www.tp.com:8000/index/world.html
    
  • 也可以省去第一参数,让分组路由更灵活一些;

    Route::group(function() {
        Route::rule("test", "Index/test");
        Route::rule("h/:name", "Index/hello");
    })->ext("html");  // URL1: http://www.tp.com:8000/test.html
    // URL2: http://www.tp.com:8000/h/world.html
    
  • 使用 prefix()方法,可以省略掉分组地址里的控制器;

    Route::group("index", function () {
        Route::rule("test", "test");
        Route::rule(":name", "hello");
    })->prefix("Index/")->ext("html");  // URL1: http://www.tp.com:8000/index/test.html
    // URL2: http://www.tp.com:8000/index/world.html
    

2. URL生成

  • 使用 url() 助手函数来生成 定义好的路由地址,放在在控制器使用;

    // 静态不带参数的
    Route::rule("test", "Index/test")->ext("html");
    // 控制器段获取url:/test.html
    return url("Index/test");  // 动态带参数的
    Route::rule("details/:id", "Index/details")->ext("html");
    // 控制器段获取url:/details/5.html
    return url("Index/details", ["id"=>5]);  // url参数一和路由的rule的参数二是一致的,可以通过name方法复刻;
    Route::rule("details/:id", "Index/details")->name("de")->ext("html");
    // 控制器段获取url:/details/5.html
    return url("de", ["id"=>5]);  // 完整带域名的地址:http://www.tp.com:8000/details/5.html
    return url("de", ["id"=>5])->domain(true);
    return url("de", ["id"=>5])->domain("www.tp.com");
    
  • 在手册 -> 路由 -> URL 生成 有 Route::buildUrl() 源方法,只不过助手函数,更方便;

35. 资源路由

1. 创建资源

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

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

    php think make:controller Blog
    
  • 以上的两部操作,创建了一个控制器Blog类,并生成了增删改查操作的方法,而且还实现了全部路由:

    | 标识 | 请求类型 | 路由规则 | 操作方法 |
    | :——: | :———: | :—————-: | :———: |
    | index | GET | blog | index |
    | create | GET | blog/create | create |
    | save | POST | blog | save |
    | read | GET | blog/:id | read |
    | edit | GET | blog/:id/edit | edit |
    | update | PUT | blog/:id | update |
    | delete | DELETE | blog/:id | delete |

2. 地址URL

  • 资源路由注册好后,所有地址都是全自动生成,具体如下:

    http://www.tp.com:8000/blog                //GET 访问的是index方法,用于显示数据
    http://www.tp.com:8000/blog/create        //GET 访问的是create方法,新增数据的表单页面
    http://www.tp.com:8000/blog/5            //GET 访问的是read方法,读取指定id的一条数据
    http://www.tp.com:8000/blog/5/edit        //GET 访问的是edit方法,读取指定id数据并显示修改表单
    
    http://www.tp.com:8000/blog                //POST 访问的是save方法,用于接收表单提交的新增数据
    http://www.tp.com:8000/blog/5            //PUT  访问的是update方法,用于接收表单提交的修改数据
    http://www.tp.com:8000/blog/5            //DELETE 访问的是delete方法,用于接收数据删除信息
    
  • 默认的参数采用 id 名称,如果你想别的,比如:blog_id;

    //相应的 delete($blog_id)
    ...->vars(["blog"=>"blog_id"]);
    
  • 也可以通过 only() 方法限定系统提供的资源方法:

    // 只允许指定的这些操作
    ...->only(["index","save","create"]);
    
  • 还可以通过 except() 方法排除系统提供的资源方法:

    // only相反操作
    ...->except(["read","delete","update"])
    
  • 使用 rest() 方法,更改系统给予的默认方法,1.请求方式;2.地址;3.操作;

    Route::rest("create", ["GET", "/add", "add"]);  // 批量
    Route::rest([
        "save" => ["POST", "", "store"],
        "update" => ["PUT", "/:id", "save"],
        "delete" => ["DELETE", "/:id", "destory"],
    ]);
    
  • 支持嵌套资源路由,类似于一对多关联的感觉,实战中用到再操作,详情查看手册:

36. 视图.变量.渲染

1. 视图操作

  • 首先,为了方便后续课程学习,先把路由给关闭了;并创建一个用于测试视图的控制器:

    // 是否启用路由
    "with_route"       => false,  // 视图控制器
    class ViewPage extends BaseController
    {
        public function index()
        {
            return "view";
        }
    }
    
  • 由于我们不用模板引擎,直接使用php原生,就需要使用 engine() 方法,载入 test 模板;

    // 载入原生php模板
    return View::engine("php")->fetch("test");  // 模板地址为:view/view_page/test.html
    // 或修改配置文件,将Think改为php就可以使用助手函数
    return view("test");
    
  • 如果希望模板后缀为 .php,方便 php + html5 混编,在 config/view 设置:

    // 模板后缀
    "view_suffix"   => "php",
    
  • 在 fetch() 方法的第二参数,通过数组方式给模板传递变量:

    return View::engine("php")->fetch("test", [
        "name"  =>  "ThinkPHP8"
    ]);  // 或   return view("test",[
        "name"  =>  "ThinkPHP8"
    ]);
    

2. 表单提交

  • 先载入一个表单页面:

    return View::engine("php")->fetch("input");
    
  • 创建一个表单:

    <form action="/view_page/save" method="post">
        <input type="text" name="username">
        <input type="submit" value="提交"></input>
    </form>
    
  • 接受数据:

    public function save()
    {
        return $this->request->post("username");
    }
    

37. 请求对象.变量.信息

1. 请求对象

  • 上一节课中表单提交时,我们接受数据使用了 $this->request->post() 方法,这哪里来的?

  • 因为我们的控制器继承了 BaseController 追踪进去,可以看到 $request 成员字段;

  • 关于这个知识点的源知识点,可以参考:手册 -> 架构 -> 容器和依赖注入,TP6讲过,8不讲了;

  • 在没有继承 BaseController 时,我们需要自己手动注入请求:

    namespace app\controller;
    use think\Request;  class Rely
    {
        protected $request;      // 依赖注入
        public function __construct(Request $request)
        {
            $this->request = $request;
        }      public function index()
        {
            halt($this->request->get());
        }
    }  // 上面的请求方式比较原始,过于麻烦,不推荐了
    
  • 第二种方式:门面Facade,它相关的知识点在手册 -> 架构 -> 门面:

    namespace app\controller;
    use think\facade\Request;  class Rely
    {
        public function index()
        {
            halt(Request::get());
        }
    }
    
  • 第三种方式:继承 BaseController,其实就是第一种,只不过被封装到基类中去了:

    namespace app\controller;
    use app\BaseController;  class Rely extends BaseController
    {
        public function index()
        {
            halt($this->request->get());
        }
    }
    
  • 第四种方式:终极方法 request() 助手函数:

    halt(request()->get());
    

    2. 请求信息

  • 在手册 请求 -> 请求信息 里有全部的请求方法模块,这里列举几个意思一下:

    | 方法 | 含义 |
    | :——: | :————————- |
    | host | 当前访问域名或者IP |
    | port | 当前访问的端口 |
    | url | 当前完整URL |
    | root | URL访问根地址 |
    | method | 当前请求类型 |

  • 我们三种方法演示一遍,最终选一种你喜欢的即可:

    // 当前url
    echo $this->request->url();
    echo Request::url();
    echo request()->url();  // 请求方法
    echo request()->method();  // 更多的对照手册自行测试即可
    

    3. 请求变量

  • Request 对象支持全局变量的检测、获取和安全过滤,支持$_GET、$_POST…等;

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

    // 判断是否有GET模式下id的值
    echo request()->has("id", "get");
    
  • 更多方法,参看手册 请求 -> 输入变量, 这里意思几个:

    | 方法 | 描述 |
    | :——-: | ————————- |
    | param | 获取当前请求变量 |
    | get | 获取 $_GET 变量 |
    | post | 获取 $_POST 变量 |
    | put | 获取 PUT 变量 |
    | delete | 获取 DELETE 变量 |
    | session | 获取 SESSION 变量 |

  • param() 方法是框架推荐的方法,可以自动识别诸如 get、post等数据信息;

    // url: http://www.tp.com:8000/rely/index?id=5
    // 可以获取 get 模式 id 的值
    echo request()->param("id");
    echo request()->get("id");  // url: http://www.tp.com:8000/rely/index/id/5
    // 此时,只能是param获取
    echo request()->param("id");
    
    // 默认值
    request()->param("name")            // null,实际上页面也转行成空,判断null也成立
    request()->param("name", "")        // 空字符串
    request()->param("name", "无名氏");      // 无名氏
    
  • 可在 app\Request.php 配置过滤器:

    http://www.tp.com:8000/rely/index?name=我<b>你</b>  // 将特殊字符转换HTML实体
    protected $filter = ["htmlspecialchars"];  // 如果不想要全局过滤器,可以直接局部
    request()->param("name", "", "htmlspecialchars");  // 设置了全局过滤器,但某个不想用
    request()->param("name", "", null)  // 使用变量修饰符,可以将参数强制转换成指定的类型;
    // /s(字符串)、/d(整型)、/b(布尔)、/a(数组)、/f(浮点);
    request()->param("id/d");
    
  • only()except() 设置允许和排查可接受的变量:

    // 允许id和name变量
    request()->only(["id","name"]);
    // 默认值设置
    request()->only(["id"=>1,"name"=>"默认值"]);
    // 参数二可设置GET还是POST等
    request()->only(["id","name"], "GET");
    
  • 以上所有,都封装到助手函数 input() 里了:

    input("?get.id");         //判断 get 下的 id 是否存在
    input("?post.name");     //判断 post 下的 name 是否存在
    input("param.name");     //获取 param 下的 name 值
    input("param.name", "默认值");     //默认值
    input("param.name", "", "htmlspecialchars");     //过滤器
    input("param.id/d");     //设置强制转换
    

38. 请求类型.输出.重定向

1. 请求类型

  • Request 对象提供了一个方法 method() 来获取当前请求类型,也提供了判断当前的请求类型:

    | 方法 | 说明 |
    | :———: | :————————- |
    | method | 获取当前请求类型 |
    | isGet | 判断是否GET请求 |
    | isPost | 判断是否POST请求 |
    | isPut | 判断是否PUT请求 |
    | isDelete | 判断是否DELETE请求 |
    | isAjax | 判断是否AJAX请求 |

  • 使用请求类型伪装,可以提交 PUT、DELETE 类型:

    // 载入表单模板
    public function create()
    {
        return View::engine("php")->fetch("create");
    }  // 表单
    <form action="/rely" method="post">
        <input type="text" name="name">
        <input type="hidden" name="_method" value="PUT">
        <input type="submit" value="提交">
    </form>
    
    // 判断是否PUT请求
    if (request()->isPut()) {
        echo input("put.name");
    }  // 直接ajax、pjax伪装,在url后续添加?_ajax=1即可,结合前段时再研究
    

2. 响应输出

  • 响应输出,有好几种:包括 returnjson()view() 等等;

  • 默认输出方式是以 html