初次接触 Yii

Yii2.0

记录下首次接触这个框架学习的点点滴滴

目录

关于 .env

Yii默认是使用 main-local.php 来代表本地环境,会覆盖掉main.php 的配置。

参考 Tinkphp5 源码,自定义函数 env() 让框架支持读取 .env 文件配置。

使用方法:

在项目入口文件前引用 env.php 文件即可。

env.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<?php

defined('ENV_PREFIX') or define('ENV_PREFIX', 'PHP_'); // 环境变量的配置前缀

// 加载环境变量配置文件,根据各个项目入口文件的不同,这里需要自己定义
$envFilePath = __DIR__ . '/../.env';
// 当项目中存在 .env 文件时加载
if (is_file($envFilePath)) {
$env = parse_ini_file(__DIR__ . '/../.env', true);
// 全部转换为大写且以_分割做的命名
foreach ($env as $key => $val) {
$name = ENV_PREFIX . strtoupper($key);

if (is_array($val)) {
foreach ($val as $k => $v) {
$item = $name . '_' . strtoupper($k);
putenv("$item=$v");
}
} else {
putenv("$name=$val");
}
}
}

if (!function_exists('env')) {
function env($name, $default = null)
{
// 支持以 . 符号分隔数组
$result = getenv(ENV_PREFIX . strtoupper(str_replace('.', '_', $name)));
if (false !== $result) {
// 如果是布尔值相关的字符串转为布尔值
if ('false' === $result) {
$result = false;
} elseif ('true' === $result) {
$result = true;
}

return $result;
}

return $default;
}
}

init() 代替 __construct

Yii 里大部分类的构造方法都需要传参并调用父类构造方法。

为了方便实现构造方法,Yii 每个父类都会定义一个 init() 空方法,并在构造方法使用。

所以当你需要使用构造方法的时候请选择 init()

控制器

控制器中允许路由访问的方法都需要加上 action + 方法名才可以访问。例: actionIndex()

actions()

比如访问 http://host/site/test 的时候,会先在控制器的 action() 方法中找到对应请求的 test 方法

如果没有那么就会在控制器中找 actionTest() 方法

把公共的方法放在 actions() 中,这样要对调用一些公共的静态页面时就可以不用写控制器方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public function actions() {
return [
'error' => [
'class' => 'yii\web\ErrorAction',
],
'captcha' => [
'class' => 'yii\captcha\CaptchaAction',
'fixedVerifyCode' => YII_ENV_TEST ? 'testme' : null, // 该值的是传入类的变量名
],
//返回验证
'tests'=>[
'class'=>'backend\models\TestAction',
]
];
}
behaviors()

在控制器方法执行之前,使用指定的 过滤器 处理数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public function behaviors() {
return [
'access' => [
'class' => AccessControl::className(), // 使用核心过滤器Access 对执行动作进行验证
'only' => ['logout'], // 对logout动作进行验证
'rules' => [ // 规则
[
'actions' => ['logout'],
'allow' => true, // 只允许认证用户进行访问
'roles' => ['@'],
],
],
],
'verbs' => [ // 设置curd动作 所运行的请求方式
'class' => VerbFilter::className(),
'actions' => [
'logout' => ['post'], // post 方法
],
],
];
}

验证器

定义验证器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php

namespace app\forms\common;

use app\forms\BaseForm;

class PhoneVerifyCodeForm extends BaseForm
{
public $phone; // 定义需要验证的字段跟数据库字段名保持一致

public $captcha;

public $method = 'get';

public function attributeLabels() {
return [
'phone' => '手机号',
];
}
// 验证规则
public function rules()
{
return [
[['phone'], 'required']
];
}
}

验证数据

1
2
3
4
$form = new PhoneVerifyCodeForm();


$form->load(['PhoneVerifyCodeForm' => \Yii::$app->request->get();]);

路由

定义路由

对应模块的 config 目录下的 main.php

1
2
3
4
5
6
7
8
9
10
......

'urlManager' => [
'enablePrettyUrl' => true,
'showScriptName' => false,
'rules' => [
'login' => 'site/show-login-form', // http://host/login
'news/detail/<id:\d+>' => 'news/detail', // http://host/news/detail/1
]
],
生成路由
1
2
// 传入路由和参数
\yii\helpers\Url::toRoute(["news/detail", 'id' => $v['id']])

模型

查询器
1
2
3
4
5
6
7
8
9
10
11
12
13
// 模型静态方法find()返回对应模型的查询器
$query = Model::find();
// where()
$query->where(['id' => 1]); // 查询id=1
$query->where(['in', 'id', [1, 2, 3]); // 查新 id IN (1,2,3)

\Yii::$app->db->createCommand($sql)->queryAll(); // 使用原生的sql语句查询

$query->asArray() // 查询结果以数组返回

$query->orderBy('sort desc, push_time desc'); // 排序

$query->all(); // 返回所有结果
查看生成的sql语句
1
$query->where(['id' => 1])->createCommand()->getRawSql(); // 将all()方法替换即可

视图

渲染
布局

当控制器调用 render() 渲染视图时,默认会使用 @app/views/layouts/main.php 作为布局文件。

例如:下面代码使用 post.php 作为布局文件。

1
2
3
4
5
6
7
8
9
10
namespace app\controllers;

use yii\web\Controller;

class PostController extends Controller
{
public $layout = 'post';

// ...
}
小部件

可以重复利用的视图都可以将其用小部件来展示

小部件类:model/widget/DemoWidget

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php

namespace app\widgets;

use app\services\DemoService;
use yii\base\Widget;

class DemoWidget extends Widget
{
public $data;

public function init()
{
$service = new DemoService();
// 获取数据并注入到data属性
$this->data = $service->getData(10);
}

public function run()
{
// 去渲染该路径的视图文件 model/widget/views/demo.php
return $this->render('demo', [
'data' => $this->data
]);
}
}

小部件视图文件: model/widget/views/demo.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<div class="xw_wp sw">
<div class="con_r">
<span class="ln"></span>
<span class="ti">新闻排行</span>
</div>
<ul class="xw">
<?php
$newsNum = 1;
if (is_array($data)) { foreach ($data as $v) {
?>
<li>
<a href="<?= \yii\helpers\Url::toRoute(["news/detail", 'id' => $v['id']])?>">
<span class="ic dls"><?=$newsNum ?></span>
<span class="ti dls"><?=\yii\helpers\Html::encode($v['title']) ?></span>
</a>
</li>
<?php
$newsNum++;
}}
?>
</ul>
</div>
分页

控制器方法 app/controller

1
2
3
4
5
6
7
8
9
10
11
12
13
......
public function actionList()
{
$count = $this->newsService->getNewsCount();
//
$pagination = new Pagination([
'totalCount' => $count, // 数据总数
'defaultPageSize' => 2, // 每页显示的数据条数
]);
$list = $this->newsService->getNewsList($pagination->limit, $pagination->offset);

return $this->render('list', compact('list', 'pagination'));
}

视图 app/views

1
2
3
4
5
6
7
8
9
10
11
12
<?php
......
// 显示分页
echo \yii\widgets\LinkPager::widget([
'pagination' => $pagination,
'nextPageLabel' => '下一页', // 设置下一页的显示文本
'prevPageLabel' => '上一页', // 设置上一页的显示文本
'options' => [ // 该项的设置的属性都会添加到分页组件的 <ul> 标签上
'class' => 'my-class-name',
]
]);
?>

响应

响应对象包含的信息有HTTP状态码,HTTP头和主体内容等。

从本质上说,网页应用开发最终的目标就是根据不同的请求去构建这些响应对象

状态码

手动设置状态码:

1
Yii::$app->response->statusCode = 200;

如果需要指定请求失败,可抛出对应的 HTTP 异常

响应头
1
2
3
4
5
6
7
8
9
10
$headers = Yii::$app->response->headers;

// 增加一个 Pragma 头,已存在的Pragma 头不会被覆盖。
$headers->add('Pragma', 'no-cache');

// 设置一个Pragma 头. 任何已存在的Pragma 头都会被丢弃
$headers->set('Pragma', 'no-cache');

// 删除Pragma 头并返回删除的Pragma 头的值到数组
$values = $headers->remove('Pragma');

请求头大小写敏感。

响应主体

返回数据前先设置格式,format 属性指定 data 中数据格式化后的样式,例如:

1
2
3
$response = Yii::$app->response;
$response->format = \yii\web\Response::FORMAT_JSON;
$response->data = ['message' => 'hello world'];

Yii 支持五种格式:

跳转

控制器中直接使用 redirect() 方法进行重定向

1
2
3
4
public function actionDemo()
{
return $this->redirect('http://example.com/new', 301);
}

redirect() 方法默认为302,该状态码会告诉浏览器请求的资源临时放到了另一个URL地址上。

可传递 301 状态码告诉浏览器请求的资源已经永久重定向到新的URL地址。

如果请求为 Ajax 请求的时候,发一个 Localtion 头不会使浏览器自动跳转。

可设置一个 X-Redirect 头,让客户端用 js 获取并实现跳转。

非控制器中使用如下代码完成跳转

1
\Yii::$app->response->redirect('/', 301)->send();

错误处理

Yii 内置了一个 ErrorHandler 用来处理错误。

ErrorHandler 默认启用,可以在应用入口脚本定义 YII_ENABLE_ERROR_HANDLER 来禁用

自定义错误处理动作

应用配置文件 main.php

1
2
3
4
5
6
7
return [
'components' => [
'errorHandler' => [
'errorAction' => 'site/error', // 指定错误处理动作,使用控制器来处理错误
],
]
];

如果异常不是继承于 UserException ,且 debugtrue时。

例: ErrorException 是不会走配置的方法而是直接使用默认的视图显示错误。

所以上线时必须关闭 debug 才能让所有异常错误走自定义的错误动作。

方便记录日志。

错误处理器 yii\base\ErrorHandler 中注释掉105行的 $this->logException($exception)

错误处理器默认会把每次异常记录为 error 级别的日志。

获取异常相关信息

错误处理动作中获取异常相关信息:

1
2
3
4
$this->exception = \Yii::$app->errorHandler->exception;   // 获取抛出的异常类
$code = $this->exception->statusCode // 获取异常状态码是属性statusCode,而不是getCode()
$message = $this->exception->getMessage(); // 获取异常信息
$trace = $this->exception->getTrace(); // 获取异常堆栈信息

可通过判断异常类是否继承于 UserException 决定是否记录日志

1
2
3
if (!$exception instanceof UserException) {
// write error log
}

如果异常继承于 UserException 会被认为是用户产生的错误,开发人员不需要去修正。

如果是 UserException 只需要返回友好的提示信息给用户即可。

日志

Yii 提供了一个日志框架,记录各种类型的消息,过滤它们,并将它们收集到不同目标,例:文件,数据库。

log 组件必须在 bootstrapping 期间就被加载,以便它能够及时调度日志信息到目标里。

日志消息
  • Yii::trace()-detail):记录一条消息去跟踪一段代码是怎样运行的。这主要在开发的时候使用。
  • Yii::info()-detail):记录一条消息来传达一些有用的信息。
  • Yii::warning()-detail):记录一个警告消息用来指示一些已经发生的意外。
  • Yii::error()-detail):记录一个致命的错误,这个错误应该尽快被检查。

这些方法可填两个参数

message 代表要被记录的日志信息

category 代表要被记录的日志类别

日志消息可以是字符串,也可以是复杂的数据,诸如数组或者对象。

可用魔术常量 METHOD 等当作日志类别区分日志

日志目标

一个日志目标是一个 yii\log\Target 类或者它的子类的实例。

它通过严重级别和类别过滤日志信息,然后将它们导入一些媒介中。

config\main.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
return [
'bootstrap' => ['log'],
'components' => [
'log' => [
// 开启两个文件的日志目标
'targets' => [
[
// 该目标记录一些自定义的的错误日志信息,例如一些数据查询之类的日志
// logVars 定义只记录请求的get post 和 cookie
// logFile 定义日志的路径 model/runtime/logs/app.log
// 该日志目标只记录 app 分类下的日志
'class' => 'yii\log\FileTarget',
'categories' => ['app',],
'logFile' => '@runtime/logs/app.log',
'logVars' => ['_GET','_POST','_COOKIE']
],
[
// 该日志目标记录下应用所有未知的bug
'class' => 'yii\log\FileTarget',
'levels' => ['error', 'warning'],
'logFile' => '@runtime/logs/bug.log',
],
],
],
],
];
自定义日志格式

重写 yii\log\FileTarget 中的 getContextMessage()getMessagePrefix()

app\exceptions\FileLogHandler.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
<?php
namespace app\exceptions;

use yii\log\FileTarget;
use Yii;

class FileLogHandler extends FileTarget
{
// 该方法定义日志格式,定义为json格式 请求参数为一行 请求报文相关为另一行json
protected function getContextMessage()
{
$json = [];
$server = [];
$result = '';
foreach ($this->logVars as $name) {
if (!empty($GLOBALS[$name])) {
$key = strtolower(substr($name, 1));
// $_SERVER 只记录关键字段
if ($key == 'server') {
$server['SERVER_PROTOCOL'] = empty($GLOBALS[$name]['SERVER_PROTOCOL']) ? '' : $GLOBALS[$name]['SERVER_PROTOCOL'];
$server['REDIRECT_STATUS'] = empty($GLOBALS[$name]['REDIRECT_STATUS']) ? '' : $GLOBALS[$name]['REDIRECT_STATUS'];
$server['REQUEST_METHOD'] = empty($GLOBALS[$name]['REQUEST_METHOD']) ? '' : $GLOBALS[$name]['REQUEST_METHOD'];
$server['REQUEST_URI'] = empty($GLOBALS[$name]['REQUEST_URI']) ? '' : $GLOBALS[$name]['REQUEST_URI'];
$server['HTTP_ACCEPT'] = empty($GLOBALS[$name]['HTTP_ACCEPT']) ? '' : $GLOBALS[$name]['HTTP_ACCEPT'];
$server['HTTP_USER_AGENT'] = empty($GLOBALS[$name]['HTTP_USER_AGENT']) ? '' : $GLOBALS[$name]['HTTP_USER_AGENT'];
} else {
$json[$key] = $GLOBALS[$name];
}
}
}
if (!empty($json)) {
$result .= json_encode($json, JSON_UNESCAPED_UNICODE);
}
if (!empty($server)) {
if ($result) {
$result .= PHP_EOL;
}
$result .= json_encode($server, JSON_UNESCAPED_UNICODE);
}

return $result;
}

// 删除日志前缀的sessionId,只记录用户id和访问ip
public function getMessagePrefix($message)
{
if ($this->prefix !== null) {
return call_user_func($this->prefix, $message);
}

if (Yii::$app === null) {
return '';
}

$request = Yii::$app->getRequest();
$ip = $request instanceof Request ? $request->getUserIP() : '-';

/* @var $user \yii\web\User */
$user = Yii::$app->has('user', true) ? Yii::$app->get('user') : null;
if ($user && ($identity = $user->getIdentity(false))) {
$userID = $identity->getId();
} else {
$userID = '-';
}

return "[$ip][$userID]";
}
}

对应的 config\main.php 中的 class 需要设置为自定义的日志类

1
2
3
4
5
6
7
8
9
10
11
12
.....
'components' => [
'log' => [
'targets' => [
[
'class' => 'app\exceptions\FileLogHandler',
'levels' => ['error', 'warning'],
'logFile' => '@runtime/logs/bug.log',
],
],
],
]
注释抛出异常会自动记录日志

yii\base\ErrorHandler 中的 handleException()

注释掉 $this->logException($exception)

否则每次抛出异常的时候 Yii 都会自动记录一个 error 级别的日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
........
public function handleException($exception)
{
if ($exception instanceof ExitException) {
return;
}
$this->exception = $exception;
$this->unregister();

if (PHP_SAPI !== 'cli') {
http_response_code(500);
}
try {
// $this->logException($exception); // 自定义异常的日志处理,避免日志过于混乱

......

缓存

Redis

composer 安装

1
composer require --prefer-dist yiisoft/yii2-redis

添加配置文件 main.php

1
2
3
4
5
6
7
.......
'redis' => [
'class' => 'yii\redis\Connection',
'hostname' => 'localhost',
'port' => 6379,
'database' => 0,
],

调用

1
2
3
4
5
$redis = Yii::$app->redis;
$redis->set('test', 123);
$redis->expire('test', 60); // 设置缓存过期的秒数
$redis->get('test'); // 123
$redis->del('test'); // 删除该键

用户认证

配置

main.php 配置文件下在 components 下添加 user 组件

1
2
3
4
5
6
7
8
......
'components' => [
'user' => [
'identityClass' => 'common\models\User',
'enableAutoLogin' => true,
'identityCookie' => ['name' => '_identity-frontend', 'httpOnly' => true],
]
]

cookie中设置了HttpOnly属性,那么通过js脚本将无法读取到cookie信息,这样能有效的防止XSS攻击。

模型

common\models\User 对应的用户表模型

使用 cookie 登录的话表中必须有字段 auth_key

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
<?php
namespace common\models;

use yii\db\ActiveRecord;
use yii\web\IdentityInterface;

class User extends ActiveRecord implements IdentityInterface
{
public static function tableName()
{
return '{{%users}}';
}

/**
* 根据给到的ID查询身份。
*
* @param string|integer $id 被查询的ID
* @return IdentityInterface|null 通过ID匹配到的身份对象
*/
public static function findIdentity($id)
{
return static::findOne($id);
}

/**
* 根据 token 查询身份。
*
* @param string $token 被查询的 token
* @return IdentityInterface|null 通过 token 得到的身份对象
*/
public static function findIdentityByAccessToken($token, $type = null)
{
return static::findOne(['access_token' => $token]);
}

/**
* @return int|string 当前用户ID
*/
public function getId()
{
return $this->id;
}

/**
* @return string 当前用户的(cookie)认证密钥
*/
public function getAuthKey()
{
return $this->auth_key;
}

/**
* @param string $authKey
* @return boolean if auth key is valid for current user
*/
public function validateAuthKey($authKey)
{
return $this->getAuthKey() === $authKey;
}

/**
* 每次注册用户的时候为用户生成一个对应的key
*/
public function beforeSave($insert)
{
if (parent::beforeSave($insert)) {
if ($this->isNewRecord) {
$this->auth_key = \Yii::$app->security->generateRandomString();
}
return true;
}
return false;
}

}
调用
1
2
3
4
// 传入验证完毕的登陆模型即可登陆,第二个参数设置登陆cookie有效期(秒)
\Yii::$app->user->login($user, 3600 * 24);
// 获取用户id,未登录返回空
\Yii::$app->user->getId();
感谢您的阅读,本文由 Double-c 版权所有。如若转载,请注明出处:Double-c(https://double-c.github.io/2018/08/22/yii2-learn-note/
PHP 标准规范
Laravel 项目开发规范