MENU

Yii2 学习笔记

September 29, 2024 • Read: 100 • PHP,编码

Yii 2 开发文档,主要参考 Yii 2.0 权威指南

两双筷子: https://www.dbkuaizi.com

Yii 2 安装

Composer

composer create-project --prefer-dist yiisoft/yii2-app-basic basic

download

下载地址:https://www.yiiframework.com/download

修改 config/web.php 文件,给 cookieValidationKey 配置项 添加一个密钥。(使用 Composre 安装自动完成该步骤)

'cookieValidationKey' => '在此处输入你的密钥',

内置服务器

使用 serve 命令启动 php 内置服务器,启动后可通过:http://localhost:8080/ 访问

php yii serve

::: alert-info
默认情况下会使用 8080 端口,如果需要手动指定端口可以 加上 --port 来指定
:::

php yii serve --port=8888

基础

目录结构

有部分精简,主要列出核心的目录结构

 ├── assets                      // 目录用于存放前端资源包PHP类
 ├── codeception.yml
 ├── commands                    // 控制器
 ├── config                      // 应用配置及其他配置
 │   ├── console.php             // 控制台应用配置信息
 │   ├── db.php                  // 数据库配置文件
 │   ├── params.php              // 参数配置文件
 │   ├── test.php
 │   ├── test_db.php
 │   └── web.php                 // Web 应用配置信息
 ├── controllers                 // 用于存放控制器的文件夹
 ├── mail                        // 与邮件相关的布局文件
 ├── models                      // 模型文件夹
 ├── runtime                     // 记录日志和缓存,要求权限 777
 ├── tests                       // 用于存放一些测试类
 ├── vendor                      // composer 包及 Yii 框架核心
 ├── views                       // 视图目录
 ├── web                         // web服务根目录(同public)
 │   ├── index.php               // 入口文件
 ├── widgets                     // 存放一些常用的小挂件的类文件
 ├── yii                         // yii 控制台入口 (同 think 与 artisan )
 └── yii.bat                     // 与 yii 文件 功能一致,运行时可省略开头的 php 

框架请求流程

  1. 用户向入口脚本 web/index.php 发起请求。
  2. 入口脚本加载应用配置并创建一个应用 实例去处理请求。
  3. 应用通过请求组件解析请求的 路由。
  4. 应用创建一个控制器实例去处理请求。
  5. 控制器创建一个动作实例并针对操作执行过滤器。
  6. 如果任何一个过滤器返回失败,则动作取消。
  7. 如果所有过滤器都通过,动作将被执行。
  8. 动作会加载一个数据模型,或许是来自数据库。
  9. 动作会渲染一个视图,把数据模型提供给它。
  10. 渲染结果返回给响应组件。
  11. 响应组件发送渲染结果给用户浏览器。

模块

模块是一个独立的软件单元,由 模型、视图、控制器和其他组件组成,终端用户可以访问应用主体中已安装的模块的控制器,模块应该当作小应用主体来看待,和应用主体不同的是,模块不能单独部署,必须属于某个应用主体。

可以看作是 ThinkPHP 的多应用模式

模块的目录结构

modules                                    // 模块目录
 └── admin                                 // admin 模块
     ├── controllers                       // 控制器目录
     │   └── DefaultController.php         // 模块默认控制器
     ├── models                            // 模型目录
     ├── Module.php                        // 模块
     └── views                             // 视图目录
         └── default                       
             └── index.php

模块构建有两种方法,一种是 使用 Gii 工具创建,另一种是手动创建。

Gii 创建模块 参考:https://www.cnblogs.com/liuwanqiu/p/6815817.html

以 Admin 模块为例

创建模块类

<?php
modules/admin/Module.php
namespace app\modules\admin;

class Module extends \yii\base\Module
{
    public $controllerNamespace = 'app\modules\admin\controllers';

    public function init()
    {
        parent::init();

        // 在这里可以引入模块自己的配置项
         \Yii::configure($this, require __DIR__ . '/config.php');
    }
}

给模块定义一个控制器

// modules/admin/controllers/DefaultController.php
<?php

namespace app\modules\admin\controllers;

use yii\web\Controller;

class DefaultController extends Controller
{

    public function actionIndex()
    {
        return $this->render('index');
    }
}

通过命令调用模块

php yii <模块名>/<控制器>/<操作>

URL 重写

Nginx URl 重写

if (!-e $request_filename)
{
    rewrite ^/(.*)$ /index.php?s=$1 last;
    break;
}

请求 Request

请求数据

::: alert-warning
当获取的参数不存在时,返回 NULL
:::

GET 请求

$request = Yii::$app->request;
// 获取所有 get 数据
$request->get();
// 获取 get 中的 id
$id = $request->get('id');
// 获取 get 中的 type,如果未传递 就返回 default
$type = $request->get('type','default');

POST 请求

与 GET用法 一致

$request = Yii::$app->request;
$post = $request->post();
$name = $request->post('name');
$name = $request->post('name','default');

所有参数

有时候前端传参除了 POST GET 外还会用 POSTPUTPATCH 等其他方法传递。可以使用 getBodyParam 获取

$request = Yii::$app->request;

// 返回所有参数
$params = $request->bodyParams;

// 返回参数 "id"
$param = $request->getBodyParam('id');

请求方法

获取当前请求使用的HTTP方法,也可以直接使用方法进行判断

$request = Yii::$app->request;

// 获取当前请求的方法
$request->method;

if ($request->isAjax) { /* 该请求是一个 AJAX 请求 */ }
if ($request->isGet)  { /* 请求方法是 GET */ }
if ($request->isPost) { /* 请求方法是 POST */ }
if ($request->isPut)  { /* 请求方法是 PUT */ }

请求URL

Yii 提供了一些成员属性用来获取 URL 的信息,我们以 http://example.com/admin/index.php/product?id=100 为例:

属性名说明
$request->url获取URL,不包含主机部分:
返回 /admin/index.php/product?id=100
$request->absoluteUrl获取完整URL
返回:http://example.com/admin/index.php/product?id=100
$request->hostInfo获取主机部分,返回 http://example.com
$request->pathInfo获取入口文件之后的路径部分,返回 /product
$request->queryString获取 问号后面的部分,返回 id=100
$request->baseUrl获取 主机之后,入口脚本之前的内容,返回 /admin
$request->scriptUrl获取没有路径信息和查询字符串的部分,返回 /admin/index.php
$request->serverName获取主机名称,返回 example.com
$request->serverPort获取客户端接口

Header 头

获取 Header 的方法

$headers = Yii::$app->request->headers;

// 获取 header 头中,Accept 的值
$headers->get('Accept');

// 判断 header 头是否存在
$headers->has('Token');

客户端信息

可以通过 userHost 和 userIP 来获取客户端的 信息

// 获取主机名
Yii::$app->request->userHost;

// 获取 ip
Yii::$app->request->userIP;

数据库操作

数据库配置

在配置文件中 配置数据的 dsn 以及账户密码

'db' => [
    'class' => 'yii\db\Connection',
    'driverName' => 'mysql',
    'dsn' => ' mysql:host=localhost;dbname=mydatabase',
    'username' => 'root',
    'password' => '',
],

配置完成之后就可以使用 Yii::$app->db 来操作数据库了

创建数据库连接

如果只需要在某个地方单独使用数据库,创建一个 yii\db\Connection 实例来与之建立连接。

$db = new yii\db\Connection([
    'dsn' => 'mysql:host=localhost;dbname=example',
    'username' => 'root',
    'password' => '',
    'charset' => 'utf8',
]);

查询数据库

查询多条数据

返回二维数组,如果查询为空返回空数组。

Yii::$app->db->createCommand('SELECT * FROM post')->queryAll();

查询单条

返回一维数组,为空则返回空数组

Yii::$app->db->createCommand('SELECT * FROM post WHERE id=1')->queryOne();

查询一列

返回一维数组,为空则返回空数组

Yii::$app->db->createCommand('SELECT title FROM post')->queryColumn();

查询一个

只获取一个字段的时候,有点像tp中的 value()

$count = Yii::$app->db->createCommand('SELECT COUNT(*) FROM post')->queryScalar();

绑定参数

创建 SQL 的时候,可以通过绑定参数来防止 SQL 注入。

$parame = ['username' => '两双筷子'];
// 批量绑定
Yii::$app->db->createCommand('SELECT * FROM `user` WHERE username =:username',$parame)
    ->queryAll();
// 或
Yii::$app->db->createCommand('SELECT * FROM `user` WHERE username =:username')
    ->bindValues($parame)->queryAll();

也可以使用一个预处理语句实现多次查询

$command = Yii::$app->db->createCommand('SELECT * FROM `user` WHERE username =:username');
$query1 = $command->bindValue('username','张三')->queryOne();
$query2 = $command->bindValue('username','李四')->queryOne();

通过引用绑定参数

$username = ''; // 要先声明变量
$command = Yii::$app->db->createCommand('SELECT * FROM `user` WHERE username =:username')
            ->bindParam(':username',$username);
$username = '张三';
$query1 = $command->queryOne();
$username = '李四';
$query2 = $command->queryOne();

非查询语句

对数据进行非查询操作需要使用 execute() 方法,返回影响行数。

Yii::$app->db->createCommand('UPDATE `gm_managers` SET username = "王五" WHERE id = 20')
    ->execute();

对于 INSERT()UPDATE()DELETE() 语句,可以不用写 SQL 而直接使用相应的方法来构建SQL。

插入单条数据

插入单条数据,返回影响行数。

// INSERT("表名",[字段数组])
Yii::$app->db->createCommand()
    ->insert('user', ['username' => '赵六','age' => 18])
    ->execute();

插入多条数据

批量插入多条数据

// batchInsert("表明",[列数组],[插入数组])
Yii::$app->db->createCommand()->batchInsert('user', ['name', 'age'], [
    ['Tom', 30],
    ['Jane', 20],
    ['Linda', 25],
])->execute();

更新数据

更新数据,返回影响行数

// UPDATE("表名",[更新字段],[条件数组]|”条件字符串“)
Yii::$app->db->createCommand()
    ->update('user', ['username' => '赵六'],['id' => 20])
    ->execute();

删除数据

删除数据,返回影响行数

// DELETE(”表名“,"条件")
Yii::$app->db->createCommand()
    ->delete('user', 'status = 0')
    ->execute();
注意: 上面只是构建 SQL 语句,最后在调用 execute() 时才会执行。

执行事务

自动事务

执行出错时将自动回滚。

Yii::$app->db->transaction(function($db) {
    $db->createCommand($sql1)->execute();
    $db->createCommand($sql2)->execute();
});

手动事务

通过手动事务可以更精准的进行错误控制

$db = Yii::$app->db;
$transaction = $db->beginTransaction();
try {
    $db->createCommand($sql1)->execute();
    $db->createCommand($sql2)->execute();
    
    $transaction->commit();
} catch(\Exception $e) {
    $transaction->rollBack();
    throw $e;
} catch(\Throwable $e) {
    $transaction->rollBack();
    throw $e;
}

指定隔离级别

默认情况下直接使用数据库设置的隔离级别,Yii 也支持设定其他的事务隔离级别。

$isolationLevel = \yii\db\Transaction::REPEATABLE_READ;

// 自动事务设定
Yii::$app->db->transaction(function ($db) {
    ....
}, $isolationLevel);
 

// 手动事务 设定
$transaction = Yii::$app->db->beginTransaction($isolationLevel);
注意: 隔离级别是针对整个链接设置的,后续的事务操作即使没有指定,也会使用相同的隔离级别。

查询构建器

使用 查询构造器可以更灵活的构建SQL,同时也提高了 SQL 的安全性。

SELECT

指定SELECT 子句

$query = new Query();
// 以下的写法都是等同的
$query->select('user.id AS user_id, email');
$query->select(['user.id AS user_id', 'email']);
$query->select(['user_id' => 'user.id', 'email']);

// 追加 SELECT 子句
$query->addSelect(['email']);

子查询

从 Yii 2.0.1 开始,就可以使用子查询来嵌套构建SQL了

$subQuery = (new Query())->select('COUNT(*)')->from('user');

// SELECT `id`, (SELECT COUNT(*) FROM `user`) AS `count` FROM `post`
$query = (new Query())->select(['id', 'count' => $subQuery])->from('post');

Distinct

调用 distinct 过滤重复行

// SELECT DISTINCT `user_id` ...
$query->select('user_id')->distinct();

FROM

通过 form() 方法指定 SQL 中的 FROM 子句

// SELECT * FROM `user`
$query->from('user');

// 以下的写法都是等同的
$query->from('public.user u, public.post p');
$query->from(['public.user u', 'public.post p']);
$query->from(['u' => 'public.user', 'p' => 'public.post']);

FROM 中使用子查询

$subQuery = (new Query())->select('id')->from('user')->where('status=1');

// SELECT * FROM (SELECT `id` FROM `user` WHERE status=1) u 
$query->from(['u' => $subQuery]);

WHERE

使用 where() 可以构建 WHERE 子句,支持以下四种格式:

  • 字符串格式:status=1
  • 哈希格式:["status" => 1,"type" => 2]
  • 操作符格式:['like' 'name' ,'test']
  • 对象格式:new LikeCondition('name','LIKE','test')

字符串格式

$query->where('status=1');

// 或使用参数绑定来绑定动态参数值
$query->where('status=:status', [':status' => $status]);

// 原生 SQL 在日期字段上使用 MySQL YEAR() 函数
$query->where('YEAR(somedate) = 2015');

// 使用 addParams 进行参数绑定
$query->where('status=:status')->addParams([':status' => $status]);

哈希格式

将多个数组条件使用 AND 拼接起来,使用哈希格式,Yii 会在内部对相应的值进行参数绑定。

// ...WHERE (`status` = 10) AND (`type` IS NULL) AND (`id` IN (4, 8, 15))
$query->where([
    'status' => 10,
    'type' => null,
    'id' => [4, 8, 15],
]);

操作符格式

操作符格式可以指定类程序任意风格的条件语句:

["操作符", "操作数1", "操作数2" ...]

每一个操作数都可以是 字符串格式、哈希格式或嵌套的操作符格式,而操作符可以是下面中的一个:

操作符用法说明
AND字符串:['and', 'id=1', 'id=2']
哈希:['AND',['name' => "张三"],['age' => 18]]
AND 查询
OR哈希:['or',['name' => "张三"],['age' => 18]]OR 查询
NOT['not', ['status' => 'draft', 'name' => 'example'],['delete_time' => null]]NOT 取反查询
BETWEEN['between', 'id', 1, 10]范围查找 id 1~ 10
NOT BETWEEN['not between', 'id', 1, 10]查找不在 1~10 范围
IN['in', 'id', [1, 2, 3]]in 查询
NOT IN['not in', 'id', [1, 2, 3]]not in
LIKE全模糊:['like','name',"张三"]
右模糊:['like','name',"张三%",false] <br/> 查询数组 ['like', 'name', ['test', 'sample']]
模糊查询
OR LIKE['or like','name',["张三","李四"]]多个 LIKE 查询 使用 or 拼接
NOT LIKE['not like','name','张三']模糊查询 取反
OR NOT LIKE['or not like','name',["张三","李四"]]多个 not like 查询 使用 or 拼接
EXISTS['exists',(yii\db\Query 实例的子查询)]EXISTS 查询
NOT EXISTS['not exists',(yii\db\Query 实例的子查询)]NOT EXISTS 查询
>,<,>=,<=['>','age',18]比较运算符,生成 age > 18

追加条件

可以使用 andWhere()orWhere() 在原有条件的基础上追加额外的条件,可以多次调用追加不同的条件。

$query->where(['status' => 1]);

$search Yii::$app->request->get('title');
if (!empty($search)) {
    $query->andWhere(['like', 'title', $search]);
}

// ... WHERE (`status` = 10) AND (`title` LIKE '%yii%')

过滤条件

使用 filterWhere() 方法可以过滤掉 值为空的条件( 当一个值为 null、空数组、空字符串或者一个只包含空格的字符串时,那么它将被判定为空值)。

$query->from('users');
$username = '张三';
$email = '';
$query->filterWhere([
    'name' => $username,
    'email' => $email,
]);
$result = $query->createCommand()->getRawSql();

// SELECT * FROM `users` WHERE `name`='张三'

同样可以使用 andFilterWhere()orFilterWhere() 追加过滤条件。

此外可以使用 andFilterCompare() 可以根据值中的内容智能地确定运算符;

$query->andFilterCompare('name', 'John Doe');
$query->andFilterCompare('rating', '>9');
$query->andFilterCompare('value', '<=100');

也可以显式的指定运算符:

$query->andFilterCompare('name', 'Doe', 'like');

HAVING

Yii 从2.0.11 版本开始,提供了 HAVING 条件的构建方法:

$query->having(['age' => 20]);
// 追加 AND
$query->andHaving(['>', 'age', 30]);
// 追加 or
$query->orHaving(['>', 'age', 30]);
$query->filterHaving(['name' => null, 'age' => 20]);
$query->andFilterHaving();

OrderBy

使用 ORDER BY 子句来构建排序语句

// ... ORDER BY `id` ASC, `name` DESC
$query->orderBy([
    'id' => SORT_ASC,
    'name' => SORT_DESC,
]);

// 简单的排序可以直接使用字符串声明
$query->orderBy('id ASC, name DESC');

groupBy()

用来指定分组,简单的字段名称,你可以使用字符串来声明它。

$query->groupBy(['id', 'status']);
$query->groupBy('id, status');

limit 与 offset

用来指定 SQL 语句当中 的 LIMITOFFSET 子句,如果设定了一个错误的值比如 负值,则会忽略这个语句

// ... LIMIT 10 OFFSET 20
$query->limit(10)->offset(20);

JOIN

用来指定SQL 中的 JOIN 子句。

$query->join('type', 'table', 'on','params');
  • type:连接类型,例如 INNER JOINLEFT JOINRIGHT JOIN
  • table:连接表名称
  • on:连接条件,只能用 字符串格式的 Where 条件
  • params:绑定参数,on用的

可以分别调用如下的快捷方法来指定 INNER JOIN, LEFT JOINRIGHT JOIN

$query->innerJoin('post', 'post.user_id = user.id');
$query->leftJoin('post', 'post.user_id = user.id');
$query->rightJoin('post', 'post.user_id = user.id');

除了连接表之外,也可以使用子查询:

$subQuery = (new \yii\db\Query())->from('post');
$query->leftJoin(['u' => $subQuery], 'u.id = author_id');

查询方法

查询构造器提供了一套用于查询不同数据的方法

  • all()多条查询,返回二维数组
  • one() 返回结果集的第一行记录
  • column() 返回结果集的第一列
  • scalar() 返回结果集的第一行第一列标量值
  • exists() 返回一个表示该查询是否包含结果集的值。
  • count() 返回 COUNT 查询的结果
  • 其他方法:sum($q)average($q)max($q)min(q) 等。$q 是必选字段,可以是一个字段也可以是一个表达式。

indexBy

使用 all() 查询数据,会返回一个整数的索引数组,有时候我们想要指定某个字段做这个数组的键。

可以在 all() 方法之前调用 indexBy() 方法,并传递一个字段或表达式

// 使用字段做key
$query = (new \yii\db\Query())
    ->from('user')
    ->limit(10)
    ->indexBy('id')
    ->all();

// 使用闭包函数的返回值做 key
$query = (new \yii\db\Query())
    ->from('user')
    ->indexBy(function ($row) {
        return $row['id'] . $row['username'];
    })->all();

批量查询处理

当处理大数据的时候,使用all()一次性将所有数据加载在内存上就不太合适了,Yii 提供了批量查询功能,MySQL 先保存查询结果,然后 Yii 使用游标分批获取数据。

use yii\db\Query;

$query = (new Query())
    ->from('user')
    ->orderBy('id');

foreach ($query->batch() as $users) {
    // $users 是一个包含100条或小于100条用户表数据的数组
}

// or to iterate the row one by one
foreach ($query->each() as $user) {
    // 数据从服务端中以 100 个为一组批量获取,
    // 但是 $user 代表 user 表里的一行数据
}

MySQL中批量查询的局限性

打印 SQL

可以使用 createCommand() 函数返回的对象打印 SQL

$query = new Query();
$command = $query->select(['id', 'email'])->from('user')->createCommand();

// 打印 SQL 语句
echo $command->sql;
// 或者使用 getRawSql() 函数
echo $command->getRawSql();

// 打印被绑定的参数
print_r($command->params);

模型操作

Yii2 提供了一个面向对象模型类,用以访问和操作数据库中的数据。

模型基类

Yii2 中基础模型类 yii\base\Model,官方基于这个类提供了很多高级模型,例如 yii\db\ActiveRecord

而基类自身提供如下特性:

属性

模型通过属性代表业务数据,就像Thinkphp 及 Laravel 那样,结果集既可以对象访问,也可以数组访问。

$FilmModel = \app\models\Film::findOne(1);
echo $FilmModel->title;
echo $FilmModel['title'];

场景

一个模型可能会在多个场景下使用,在不同的场景下我们希望校验不同的值,比如我们注册用户的时候邮箱是必填的,但登录的时候却不是。这种情况下可以配合验证规则实现不同场景的数据校验。

设置场景

// 场景作为属性来设置
$model = new User;
$model->scenario = 'login';

// 场景通过构造初始化配置来设置
$model = new User(['scenario' => 'login']);

验证规则 核心验证器(Core Validators)

当接收到了用户提交的数据,可以通过验证规则来进行校验,而不需要手动判断。

使用验证

$model = new \app\models\ContactForm;

// 用户输入数据赋值到模型属性
$model->attributes = \Yii::$app->request->post('ContactForm');

if ($model->validate()) {
    // 所有输入数据都有效 all inputs are valid
} else {
    // 验证失败:$errors 是一个包含错误信息的数组
    $errors = $model->errors;
}

定义验证规则
如果想让校验在所有场景下都生效,去掉 on 即可。

public function rules()
{
    return [
        // 在"register" 场景下 username, email 和 password 必须有值
        [['username', 'email', 'password'], 'required', 'on' => 'register'],

        // 在 "login" 场景下 username 和 password 必须有值
        [['username', 'password'], 'required', 'on' => 'login'],
    ];
}

块赋值

块赋值只用一行代码将用户所有输入填充到一个模型,非常方便, 它直接将输入数据对应填充到 yii\base\Model::attributes() 属性。

$model = new \app\models\ContactForm;
$model->attributes = \Yii::$app->request->post('ContactForm');

安全属性

因为块赋值会将用户提交的所有数据都放模型中,有时候我们只希望用户修改部分数据,这时可以通过设置 安全属性来实现(个人理解就是块赋值的黑白名单)

在登陆场景下允许提交 用户名和密码,注册场景下,允许提交 用户名、密码和邮箱。

通过别名为 safe 的验证器来表示这些字段是安全的

public function scenarios()
{
    return [
        'login' => ['username', 'password'],
        'register' => ['username', 'email', 'password'],
    ];
}

不安全属性

如果要表示不安全属性,则在属性前面加一个 ! 即可。

public function rules()
{
    return [
        [['username', 'password', '!secret'], 'required', 'on' => 'login']
    ];
}

不安全的属性在块赋值的时候会被过滤掉,如果需要必须手动指定。

$model->secret = $secret;

快捷填充

使用 load() 填充可以简化代码:

例如:

if (isset($_POST['FormName'])) {
    $model->attributes = $_POST['FormName'];
    if ($model->save()) {
        // handle success
    }
}

简化写法:

if ($model->load($_POST) && $model->save()) {
    // handle success
}

声明模型类

声明一个模型类 需要继承 yii\db\ActiveRecord 类。

::: alert-warning
需要注意的是 Yii2 提供的 base 只是一个基类,并没有模型的操作方法,所以我们需要继承的是 yii\db\ActiveRecord 类,而不是 yii\base\Model
:::

<?php
namespace app\models;
use yii\db\ActiveRecord;

class Film extends ActiveRecord
{

}

设置表名称

设置模型的表名称有两种方式,第一种就是使用模型类名,第二种方式是手动指定一个表名。

使用模型类名

类名使用大驼峰,例如:

类名表名
Useruser
AdminUseradmin_user

手动指定

// 手动指定表名称
public static function tableName()
{
    return 'film';
}

// 手动指定表名并使用前缀
public static function tableName()
{
    return '{{%film}}';
}

指定数据库

如果想在这个类中单独使用其他数据库配置,可以重写 getDb()方法:

public static function getDb()
{
    // 使用 "db2" 组件
    return \Yii::$app->db2;
}

查询数据

Find

使用 Active Query 查询数据,这种操作和直接使用查询生成器差不多,唯一的区别在于 使用 find()方法获取查询生成器对象,而不是 new 一个新对象。


// 返回 ID 为 123 的客户:
// SELECT * FROM `customer` WHERE `id` = 123
$customer = Customer::find()
    ->where(['id' => 123])
    ->one();

// 取回所有活跃客户并以他们的 ID 排序:
// SELECT * FROM `customer` WHERE `status` = 1 ORDER BY `id`
$customers = Customer::find()
    ->where(['status' => Customer::STATUS_ACTIVE])
    ->orderBy('id')
    ->all();

// 取回活跃客户的数量:
// SELECT COUNT(*) FROM `customer` WHERE `status` = 1
$count = Customer::find()
    ->where(['status' => Customer::STATUS_ACTIVE])
    ->count();

// 以客户 ID 索引结果集:
// SELECT * FROM `customer`
$customers = Customer::find()
    ->indexBy('id')
    ->all();

findOne 与 findAll

根据主键获取 id 是比较常用的功能,所以 Yii 提供了两个快捷方法:

  • findOne() 查询单条数据
  • findAll() 查询多条数据

这两个方法接受如下传参:

  • 整数值:这个值会当作主键去查询,Yii 通过读取数据库信息来识别主键列。
  • 整数数组:主键的 IN 查询
  • 关联数组:需要传递一个哈希格式的条件数组。
// 返回 id 为 123 的客户 
// SELECT * FROM `customer` WHERE `id` = 123
$customer = Customer::findOne(123);

// 返回 id 是 100, 101, 123, 124 的客户
// SELECT * FROM `customer` WHERE `id` IN (100, 101, 123, 124)
$customers = Customer::findAll([100, 101, 123, 124]);

// 返回 id 是 123 的活跃客户
// SELECT * FROM `customer` WHERE `id` = 123 AND `status` = 1
$customer = Customer::findOne([
    'id' => 123,
    'status' => Customer::STATUS_ACTIVE,
]);

// 返回所有不活跃的客户
// SELECT * FROM `customer` WHERE `status` = 0
$customers = Customer::findAll([
    'status' => Customer::STATUS_INACTIVE,
]);

::: alert-danger
如果你需要将用户输入传递给这些方法,请确保输入值是标量或者是 数组条件,确保数组结构不能被外部所改变:

// yii\web\Controller 确保了 $id 是标量
public function actionView($id)
{
    $model = Post::findOne($id);
    // ...
}

// 明确了指定要搜索的列,在此处传递标量或数组将始终只是查找出单个记录而已
$model = Post::findOne(['id' => Yii::$app->request->get('id')]);

// 不要使用下面的代码!可以注入一个数组条件来匹配任意列的值!
$model = Post::findOne(Yii::$app->request->get('id'));

:::

原生SQL

除了查询生成器之外,模型还支持使用原生SQL查询,使用 findBySql() 方法,需要注意的是 findBySql()后面追加的其他查询方法都会被忽略

$sql = 'SELECT * FROM customer WHERE status=:status';
$customers = Customer::findBySql($sql, [':status' => Customer::STATUS_INACTIVE])->all();

访问数据

查询的数据被返回在模型实例中,可以通过模型属性来访问结果集。

// "id" 和 "email" 是 "customer" 表中的列名
$customer = Customer::findOne(123);
$id = $customer->id;
$email = $customer->email;

修改器与获取器

官方的叫法是“数据转换”,但在其他框架中更通用的名字叫做“获取器”。

class Customer extends ActiveRecord
{

    // 设置获取器
    public function getBirthdayText()
    {
        return date('Y/m/d', $this->birthday);
    }
    
    // 设置修改器
    public function setBirthdayText($value)
    {
        $this->birthday = strtotime($value);
    }
}

asArray

虽然 Yii 中的模型操作十分灵活,但如果查询大量数据时,对象会造成大量的内存占用。我们可以在查询前调用 asArray() 方法来转换为数组,以节省内存。

$customers = Customer::find()->asArray()->all();

分批获取数据

使用分批获取数据,可以最小化内存使用。

// 每次获取 10 条客户数据
foreach (Customer::find()->batch(10) as $customers) {
    // $customers 是个最多拥有 10 条数据的数组
}

// 每次获取 10 条客户数据,然后一条一条迭代它们
foreach (Customer::find()->each(10) as $customer) {
    // $customer 是个 `Customer` 对象
}

// 贪婪加载模式的批处理查询
foreach (Customer::find()->with('orders')->each() as $customer) {
    // $customer 是个 `Customer` 对象,并附带关联的 `'orders'`
}

保存数据

save()

Active Record 类提供了 save 方法,可以轻松的将数据存入数据库。

// 插入新记录
$customer = new Customer();
$customer->name = 'James';
$customer->email = 'james@example.com';
$customer->save();

// 更新已存在的记录
$customer = Customer::findOne(123);
$customer->email = 'james@newexample.com';
$customer->save();

插入还是写入数据,这取决于模型实例是通过 new 获取的还是通过查询获取的。

:::alert-info
在保存时如果确认数据不需要校验,可以通过 save(false) 的方式来跳过验证过程。
:::

insert()

插入数据,返回布尔值 表示成功还是失败

$customer = new Customer;
$customer->name = $name;
$customer->email = $email;
$customer->insert();

update()

更新数据,返回影响行数

:::alert-w
需要注意的是,更新可能不会影响表中的任何行。 在该情况下,此方法有可能返回 0。 因此,需要使用 !== false 的方式判断 update() 是否成功:
:::

$customer = Customer::findOne($id);
$customer->name = $name;
$customer->email = $email;
$customer->update();

更新计数

在数据库中经常会遇到需要自增或自减的场景,我们将这些列称之为计数列,可以使用 updateCounters() 更新一个或多个计数列。
如果是递减 使用负值即可。

$post = Post::findOne(100);

// UPDATE `post` SET `view_count` = `view_count` + 1 WHERE `id` = 100
$post->updateCounters(['view_count' => 1]);

:::alert-warning
如果使用 save() 更新一个计数列,可能会因为并发请求导致出现错误的结果。
:::

脏属性

调用 save() 保存数据的时候,只有“脏属性”即被修改的属性才会被保存。需要注意的是,无论如何 Active Record 都会执行数据验证 不管有没有脏属性。

Active Record 类会自动维护一个脏属性列表,并且保存所有属性的旧值,并于新值比较(使用 === 比较)。可以通过 getDirtyAttributes() 来获取当前的脏属性,也可以通过 markAttributeDirty('field_name') 将属性显式的标注为脏属性。

如果需要获取原先的旧值,可以通过 getOldAttributes() 来一次性获取所有旧值,或使用 getOldAttribute('field_name') 获取单个旧值。

批量更新

上面所说的保存数据是针对单条的,而批量更新多条数据时我们可以使用 updateAll() 方法。

updateAll($attributes, $condition = '', $params = [])

// UPDATE `customer` SET `status` = 1 WHERE `email` LIKE `%@example.com%`
Customer::updateAll(['status' => Customer::STATUS_ACTIVE], ['like', 'email', '@example.com']);

批量更新计数

计数列也可以通过批量的方式来更新:
updateAllCounters($counters, $condition = '', $params = [])

// UPDATE `customer` SET `age` = `age` + 1
Customer::updateAllCounters(['age' => 1]);

删除数据

删除单条

返回 删除的行数,如果由于某种原因删除失败,则为 false。 注意,即使删除执行成功,删除的行数也可能为 0。

$customer = Customer::findOne(123);
$customer->delete();

删除多条

批量删除数据,传参是删除条件,返回删除行数。

Customer::deleteAll(['status' => 1]);

:::alert-danger
需要注意的是,Yii2 模型方法中没有软删除,如果批量删除条件出错,可能会导致数据表被清空。
:::

生命周期

通过模型的生命周期,可以在不同的节点调用想要执行的代码,具体参考:Active Record 的生命周期

下面以 Yii2 的删除方法为例,实现软删除的扩展:

class Film extends ActiveRecord
{
    public function beforeDelete()
    {
        if (!parent::beforeDelete()) {
            return false;
        }

        // 如果存在软删除字段,就修改软删除状态,并返回false
        if(isset($this->is_delete))
        {
            $this->is_delete = 1;
            $this->save();
            return false;
        }

        // 如果不存在 就走正常删除
        return true;
    }

}

:::alert-info
调用以下方法,不会触发任何生命周期,因为这些方法是直接操作数据库而不是 ActiveRecord 模型:

  • updateAll()
  • deleteAll()
  • updateCounters()
  • updateAllCounters()
    :::

事务操作

Yii 包含两种事务方法。

显式事务

$customer = Customer::findOne(123);

Customer::getDb()->transaction(function($db) use ($customer) {
    $customer->id = 200;
    $customer->save();
    // ...其他 DB 操作...
});

// 或
$transaction = Customer::getDb()->beginTransaction();
try {
    $customer->id = 200;
    $customer->save();
    // ...other DB operations...
    $transaction->commit();
} catch(\Exception $e) {
    $transaction->rollBack();
    throw $e;
} catch(\Throwable $e) {
    $transaction->rollBack();
    throw $e;
}

::: alert-warning
上面的 \Exception 用于兼容 PHP5,如果开发环境是 PHP7+,则可以跳过这部分。
:::

自动事务

在模型中的 transactions() 方法里声明需要事务支持的DB操作:

class Customer extends ActiveRecord
{
    public function transactions()
    {
        return [
            'admin' => self::OP_INSERT,
            'api' => self::OP_INSERT | self::OP_UPDATE | self::OP_DELETE,
            // 上面等价于:
            // 'api' => self::OP_ALL,
        ];
    }
}

key 是场景,描述了在不同场景下的事务要求:

  • OP_INSERT 执行插入操作时触发事务
  • OP_UPDATE 执行更新操作时触发事务
  • OP_DELETE 执行删除操作时触发事务

多个操作使用 | 拼接,如果需要全部触发,OP_ALL 执行 插入、更新、删除操作时都会触发。

自动事务的执行逻辑是:相应的事务在调用 beforeSave() 方法时开启, 在调用 afterSave() 方法时被提交。

模型关联

模型管理以 MySQL 官方示例库 sakila 为例,展示模型关联用法。

一对一关联

语法:return $this->hasOne($class, $link);

  • $class 关联类
  • $link 主外键按约束,数组格式。key是关联表字段 ,value 是主表字段
  • return 返回关联查询对象
// 电影演员表模型
class FilmActor extends ActiveRecord
{
    // 一对一关联 获取演员信息
    public function getInfo()
    {
        return $this->hasOne(Actor::class, ['actor_id' => 'actor_id']);
    }
}

一对多关联

语法:return $this->hasMany($class, $link);

  • $class 关联类
  • $link 主外键按约束,数组格式。key是关联表字段 ,value 是主表字段
  • return 返回关联查询对象
// 电影表模型
class Film extends ActiveRecord
{

    // 一对多关联,获取电影演员
    public function getActor()
    {
        return $this->hasMany(FilmActor::class, ['film_id' => 'film_id']);
    }

}

访问关联数据

定义好关联关系后,我们就可以通过关联属性来访问数据了。而 Yii2 提供了两种方法用以访问这些数据:

通过方法

直接通过关联方法名获取的是一个 ActiveRecord 查询对象。

$FilmModel = Film::findOne(1);
var_dump($FilmModel->getActor());

通过属性
如果通过属性获取,则定义关联时方法名必须是 'getXxx()' 的命名形式,否则无法获取到属性。

使用时 去掉 前缀get,并且首字母小写,例:getActor() -> actor、getAcTor-> acTor

一对多关联时,通过属性获取到的是一个二维数组,数组的元素是 yii\db\BaseActiveRecord 对象。

$FilmModel = Film::findOne(1);
foreach ($FilmModel->actor as $key => $value) {
    var_dump($value->info->first_name);
}

动态关联查询

由于关联方法返回的是 ActiveRecord 查询对象,所以你可以在这个对象上继续进行查询操作。

需要注意的是,每次动态的关联查询时,都会执行SQL语句。

$FilmModel = Film::findOne(1);
// 获取结果中的第一列
var_dump($FilmModel->getActor()->column());
// 获取关联表的 数组数据
var_dump($FilmModel->getActor()->asArray()->all());
// 查询演员id 小于50 的数据
$actorModel = $FilmModel->getActor()->where(['<','actor_id','50'])->all();
var_dump($actorModel);

也可以通过传参的方式简化动态查询

我们以查询演员id 小于50的为例。

class Film extends ActiveRecord
{

    // 获取id小于某个值的演员,
    public function getMinActor($number = 50)
    {
        return $this->hasMany(FilmActor::class, ['film_id' => 'film_id'])
        ->where(['<','actor_id',$number]);
    }

}

$FilmModel->getMinActor(40)->asArray()->all()

::: alert-w
如果需要通过属性获取,则必须给关联方法设置一个默认值。
:::

多对多关联

在日常的查询中,当两张表的关系是 多对多,通常会使用一个中间表来关联。例如,一个电影有多个演员,而一个演员也会出演多部电影。
那么演员表 actor 和电影表 film 之间就会有一个电影演员关系表 film_actor

Yii2 有两种方法声明 多对多关联,一种是使用 via() 引用现有的一对多关联方法,另一种是使用 viaTable() 来直接声明中间表。

viaTable

语法:viaTable($tableName, $link, callable $callable = null);

  • $tableName 中间表名
  • $link 主表与中间表关联条件
  • $callable 一个 PHP 的回调,用于定制与连接表相关的关联。
class Film extends ActiveRecord
{
    // 多对多关联
    public function getActorInfo()
    {
        // hasMany 第二个参数是 演员表和电影演员关系表的关联关系
        return $this->hasMany(Actor::class, ['actor_id' => 'actor_id'])
        ->viaTable('film_actor', ['film_id' => 'film_id']);
    }

via
使用已有的一对多关联方法来指定连接表

class Film extends ActiveRecord
{
    // 多对多关联
    public function getActorInfo()
    {
        return $this->hasMany(Actor::class, ['actor_id' => 'actor_id'])
        ->via('actor');
    }

    // 一对多关联,获取电影演员
    public function getActor()
    {
        return $this->hasMany(FilmActor::class, ['film_id' => 'film_id']);
    }

多表连续关联

通过使用 via() 方法们可以通过多个表来定义关联声明。
比如 一个演员会出演多部电影 ,每部电影会有不同的分类,我们可以通过如下方法获取:

class Actor extends ActiveRecord
{

    // 通过电影类别中间表关联类别
    public function getActorCate()
    {
        return $this->hasMany(Category::class,['category_id' => 'category_id'])
        ->via('filmCate');
    }

    // 关联电影的类别中间表
    public function getFilmCate()
    {
        return $this->hasMany(FilmCategory::class, ['film_id' => 'film_id'])
        ->via('film');
    }


    // 演员关联电影
    public function getFilm()
    {
        return $this->hasMany(FilmActor::class,['actor_id' => 'actor_id']);
    }

}

预加载

前面有提到 访问关联数据的时候才会执行SQL,这样就会存在一个性能问题,比如下面这段代码,执行了101次SQL查询。

// SELECT * FROM `customer` LIMIT 100
$customers = Customer::find()->limit(100)->all();

foreach ($customers as $customer) {
    // 每次循环都会被执行一次
    // SELECT * FROM `order` WHERE `customer_id` = ...
    $orders = $customer->orders;
}

我们可以使用 with()方法进行预加载,这时需要执行的 SQL 语句只有两条。

// SELECT * FROM `customer` LIMIT 100;
// SELECT * FROM `orders` WHERE `customer_id` IN (...)
$customers = Customer::find()
    ->with('orders')
    ->limit(100)
    ->all();

foreach ($customers as $customer) {
    // 没有任何的 SQL 执行
    $orders = $customer->orders;
}

with各种用法

//  即时加载 "orders" and "country"
$customers = Customer::find()->with('orders', 'country')->all();
// 等同于使用数组语法 如下
$customers = Customer::find()->with(['orders', 'country'])->all();

// 即时加载“订单”和嵌套关系“orders.items”,这个嵌套层级是没有限制的,可以“a.b.c.d”
$customers = Customer::find()->with('orders.items')->all();

使用预加载的时候可以通过闭包函数实现条件过滤:

// 查找所有客户,并带上他们国家和活跃订单
// SELECT * FROM `customer`
// SELECT * FROM `country` WHERE `id` IN (...)
// SELECT * FROM `order` WHERE `customer_id` IN (...) AND `status` = 1
$customers = Customer::find()->with([
    'country',
    'orders' => function ($query) {
        $query->andWhere(['status' => Order::STATUS_ACTIVE]);
    },
])->all();

::: alert-warning
如果你在即时加载的关联中调用 select() 方法,你要确保 在关联声明中引用的列必须被 select。
:::


其他操作可以参考 Yii2 的模型文档:活动记录(Active Record)

数据库迁移

通过数据库迁移,可以记录数据库结构的变化,使数据库可以和源代码一样受版本控制。

迁移命令

提交迁移

命令: yii migrate

该操作会列出所有未提交的迁移,如果确定要提交这些迁移,将会按照类名中的时间戳,逐个运行里面的 up() 方法或 safup() 方法。
其中任意一个迁移执行失败(比如执行报错),那么这条命令将会退出并停止剩下未执行的迁移。

每次迁移成功,都会在数据库的 migration 表中添加一条迁移成功的记录,用于判断那些迁移是已提交的,那些是未提交的。

有时候篇,我们只想提交前几个迁移,而不是提交所有可用迁移。下面的命令则表示只提交前三个可用迁移:

php yii migrate 3

提交特定迁移

也可以只提交特定的迁移,使用 migrate/to 命令。

yii migrate/to 150101_185401                      # 使用时间戳来指定迁移
yii migrate/to "2015-01-01 18:54:01"              # 使用一个可以被 strtotime() 解析的字符串
yii migrate/to m150101_185401_create_news_table   # 使用全名
yii migrate/to 1392853618                         # 使用 UNIX 时间戳

如果指定的迁移前面还有未提交的迁移,则未提交的迁移将会被提交。
如果指定的迁移在之前已经被提交过,则在其之后的迁移将会被还原。

创建迁移

命令:yii migrate/create <name>

可以使用此命令创建一个新的迁移文件,创建的迁移文件类名将按照 m<YYMMDD_HHMMSS>_<Name> 的格式自动生成,其中:

  • <YYMMDD_HHMMSS> 指执行创建迁移命令的 UTC 时间。
  • <Name> 指创建迁移命令所带的 Name 参数值

创建命令执行后会生成一个类文件,里面包含 up()down() 方法。当提交迁移文件时 up() 将会被执行,当还原迁移时 down() 将会被提交。

如果迁移文件无法被还原时,例如删除了一条记录,则应该在 down() 方法中返回 false 来表示这个 migration 是无法恢复的。

还原迁移

命令:yii migrate/down

可以通过此命令还原之前提交的一个或多个迁移:

yii migrate/down     # 还原最近一次提交的迁移
yii migrate/down 3   # 还原最近三次提交的迁移

重做迁移

命令:yii migrate/redo

重做的意思是,先还原指定的迁移,然后再次提交。

yii migrate/redo        # 重做最近一次提交的迁移
yii migrate/redo 3      # 重做最近三次提交的迁移

刷新迁移

命令:yii migrate/fresh

删除数据表中所有的表和外键,从头开始重新提交所有迁移。

列出迁移

可以通过命令列出已提交了那些或未提交那些迁移:

yii migrate/history     # 显示最近10次提交的迁移
yii migrate/history 5   # 显示最近5次提交的迁移
yii migrate/history all # 显示所有已经提交过的迁移

yii migrate/new         # 显示前10个还未提交的迁移
yii migrate/new 5       # 显示前5个还未提交的迁移
yii migrate/new all     # 显示所有还未提交的迁移

修改迁移历史

有时候我们只需要标记一下你的数据库升级到一个特定的迁移,而不是实际提交或还原迁移。这个经常发生在已经手动修改了数据库的结构,而又不想相应的迁移被重复提交,那么你可以使用如下命令来达到目的:

yii migrate/mark 150101_185401                      # 使用时间戳来指定迁移
yii migrate/mark "2015-01-01 18:54:01"              # 使用一个可以被 strtotime() 解析的字符串
yii migrate/mark m150101_185401_create_news_table   # 使用全名
yii migrate/mark 1392853618                         # 使用 UNIX 时间戳

自定义迁移

可以通过很多方法来自定义迁移命令。

命令行选项

迁移命令附带了几个命令行选项,可以用自定义它的行为:

  • interactive 布尔值(默认为true)指定是否通过交互模式来提示用户将执行那些迁移,如果希望在后台执行该命令,应该设置为 false。
  • migrationPath 字符串|数组(默认值为 @app/migrations) 指定存放迁移类文件的目录。该选项可以是路径,也可以是路径别名。需要注意的是指定的目录必须存在,否则将会触发一个错误。从 2.0.12 版本开始,可以使用数组来指定多个来源读取迁移文件。
  • migrationTable 字符串(默认值为 migration)指定用于存储迁移历史信息的数据库表名称,如果这张表不存在,那么迁移命令将自动的创建这张表。
  • db 字符串(默认值为 db) 用于指定被该命令迁移的数据库
  • templateFile:字符串 (默认值为 @yii/views/migration.php), 指定生产迁移框架代码类文件的模版文件路径。
  • fields 由用来创建迁移代码的多个字段定义字符串所组成的数组。默认是 []。 字段定义的格式是 COLUMN_NAME:COLUMN_TYPE:COLUMN_DECORATOR。例如,--fields=name:string(12):notNull 会创建 一个长度为 12 的,非空的,字符串类型的字段。