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
框架请求流程
- 用户向入口脚本 web/index.php 发起请求。
- 入口脚本加载应用配置并创建一个应用 实例去处理请求。
- 应用通过请求组件解析请求的 路由。
- 应用创建一个控制器实例去处理请求。
- 控制器创建一个动作实例并针对操作执行过滤器。
- 如果任何一个过滤器返回失败,则动作取消。
- 如果所有过滤器都通过,动作将被执行。
- 动作会加载一个数据模型,或许是来自数据库。
- 动作会渲染一个视图,把数据模型提供给它。
- 渲染结果返回给响应组件。
- 响应组件发送渲染结果给用户浏览器。
模块
模块是一个独立的软件单元,由 模型、视图、控制器和其他组件组成,终端用户可以访问应用主体中已安装的模块的控制器,模块应该当作小应用主体来看待,和应用主体不同的是,模块不能单独部署,必须属于某个应用主体。
可以看作是 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 外还会用 POST
,PUT
,PATCH
等其他方法传递。可以使用 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 语句当中 的 LIMIT
和 OFFSET
子句,如果设定了一个错误的值比如 负值,则会忽略这个语句
// ... LIMIT 10 OFFSET 20
$query->limit(10)->offset(20);
JOIN
用来指定SQL 中的 JOIN 子句。
$query->join('type', 'table', 'on','params');
- type:连接类型,例如
INNER JOIN
,LEFT JOIN
、RIGHT JOIN
- table:连接表名称
- on:连接条件,只能用 字符串格式的 Where 条件
- params:绑定参数,on用的
可以分别调用如下的快捷方法来指定 INNER JOIN
, LEFT JOIN
和 RIGHT 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 表里的一行数据
}
打印 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
{
}
设置表名称
设置模型的表名称有两种方式,第一种就是使用模型类名,第二种方式是手动指定一个表名。
使用模型类名
类名使用大驼峰,例如:
类名 | 表名 |
---|---|
User | user |
AdminUser | admin_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 的,非空的,字符串类型的字段。