1、设置数据库的默认排序规则为:utf8mb4_unicode_ci,如图1
2、修改用于数据库连接的默认字符集为:utf8mb4,编辑开发环境下的配置文件,/environments/dev/common/config/main-local.php,编辑生产环境下的配置文件,/environments/prod/common/config/main-local.php
<?php
return [
'components' => [
'db' => [
'class' => 'yii/db/Connection',
'dsn' => 'mysql:host=localhost;dbname=yii2advanced',
'username' => 'root',
'password' => '',
'charset' => 'utf8mb4',
],
'mailer' => [
'class' => 'yii/swiftmailer/Mailer',
'viewPath' => '@common/mail',
// send all mails to a file by default. You have to set
// 'useFileTransport' to false and configure a transport
// for the mailer to send real emails.
'useFileTransport' => true,
],
],
];
<?php
return [
'components' => [
'db' => [
'class' => 'yii/db/Connection',
'dsn' => 'mysql:host=localhost;dbname=yii2advanced',
'username' => 'root',
'password' => '',
'charset' => 'utf8mb4',
],
'mailer' => [
'class' => 'yii/swiftmailer/Mailer',
'viewPath' => '@common/mail',
],
],
];
3、执行初始化命令,如图2
./init
4、编辑数据库配置,/common/config/main-local.php
'db' => [
'class' => 'yii/db/Connection',
'dsn' => 'mysql:host=localhost;dbname=g-s-yii2-app-advanced',
'username' => 'g-s-yii2-app-advanced',
'password' => 'IADO0x7uK4UpaRRM',
'charset' => 'utf8mb4',
],
5、清空数据库,执行数据库迁移命令,如图3
./yii migrate
6、日志的数据库模式可以通过应用迁移来初始化,执行如下命令,如图4
./yii migrate --migrationPath=@yii/log/migrations/
7、浏览数据库表,user、log表的排序规则为:utf8_unicode_ci,如图5
8、新建一个数据库迁移文件,调整排序规则为:utf8mb4_unicode_ci,执行命令,如图6
./yii migrate/create update_table_options_to_log
9、修改表(user、log)默认的字符集和所有字符列的字符集,编辑 /console/migrations/m180620_105204_update_table_options_to_log.php
<?php
use yii/db/Migration;
/**
* Class m180620_105204_update_table_options_to_log
*/
class m180620_105204_update_table_options_to_log extends Migration
{
/**
* {@inheritdoc}
*/
public function safeUp()
{
$tableOptions = null;
if ($this->db->driverName === 'mysql') {
// http://stackoverflow.com/questions/766809/whats-the-difference-between-utf8-general-ci-and-utf8-unicode-ci
$tableOptions = 'CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE=InnoDB';
$this->execute('ALTER TABLE {{%user}} CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci');
$this->execute('ALTER TABLE {{%log}} CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci');
}
$this->addCommentOnTable('{{%user}}', '用户', $tableOptions);
$this->addCommentOnTable('{{%log}}', '日志', $tableOptions);
}
/**
* {@inheritdoc}
*/
public function safeDown()
{
echo "m180620_105204_update_table_options_to_log cannot be reverted./n";
return false;
}
/*
// Use up()/down() to run migration code without a transaction.
public function up()
{
}
public function down()
{
echo "m180620_105204_update_table_options_to_log cannot be reverted./n";
return false;
}
*/
}
10、一个 [[yii/log/DbTarget|database target]] 目标导出已经过滤的日志消息到一个数据的表里面,设置日志目标为 DbTarget
11、控制台应用的配置文件,/console/config/main.php,代码
'components' => [
'log' => [
'targets' => [
[
'class' => 'yii/log/FileTarget',
'levels' => ['error', 'warning'],
],
],
],
],
12、控制台应用的配置文件,/console/config/main.php,设置日志目标为 DbTarget,编辑代码
'components' => [
'log' => [
'traceLevel' => YII_DEBUG ? 3 : 0,
'targets' => [
'file' => [
'class' => 'yii/log/FileTarget',
'levels' => ['error', 'warning'],
],
'db' => [
'class' => 'yii/log/DbTarget',
'except' => ['*'],
'prefix' => function () {
$url = !Yii::$app->request->isConsoleRequest ? Yii::$app->request->getUrl() : null;
$user = Yii::$app->has('user', true) ? Yii::$app->get('user') : null;
$userId = $user ? $user->getId(false) : '-';
return sprintf('[%s][%s][%s]', Yii::$app->id, $url, $userId);
},
'logVars' => [],
]
],
],
],
13、接口应用的配置文件,/api/config/main.php,代码
<?php
$params = array_merge(
require __DIR__ . '/../../common/config/params.php',
require __DIR__ . '/../../common/config/params-local.php',
require __DIR__ . '/params.php',
require __DIR__ . '/params-local.php'
);
return [
'id' => 'app-api',
'basePath' => dirname(__DIR__),
'bootstrap' => ['log', 'contentNegotiator'],
'controllerNamespace' => 'api/controllers',
'version' => '1.0.0',
'components' => [
'request' => [
'csrfParam' => '_csrf-api',
],
'user' => [
'identityClass' => 'api/models/User',
'enableSession' => false,
'loginUrl' => null,
'enableAutoLogin' => false,
],
'session' => [
// this is the name of the session cookie used for login on the api
'name' => 'advanced-api',
],
'log' => [
'traceLevel' => YII_DEBUG ? 3 : 0,
'targets' => [
[
'class' => 'yii/log/FileTarget',
'levels' => ['error', 'warning'],
],
],
],
'urlManager' => require __DIR__ . '/urlManager.php',
'i18n' => [
'translations' => [
'model/*'=> [
'class' => 'yii/i18n/PhpMessageSource',
'forceTranslation' => true,
'basePath'=>'@common/messages',
'fileMap'=>[
],
],
'*'=> [
'class' => 'yii/i18n/PhpMessageSource',
'forceTranslation' => true,
'basePath'=>'@api/messages',
'fileMap'=>[
],
],
],
],
'contentNegotiator' => [
'class' => 'yii/filters/ContentNegotiator',
'formats' => [
'application/json' => yii/web/Response::FORMAT_JSON,
'application/xml' => yii/web/Response::FORMAT_XML,
],
'languages' => [
'en-US',
'zh-CN',
],
],
],
'modules' => [
'v1' => [
'class' => api/modules/v1/Module::class,
],
],
'params' => $params,
];
14、接口应用的配置文件,/api/config/main.php,设置日志目标为 DbTarget,编辑代码
'log' => [
'traceLevel' => YII_DEBUG ? 3 : 0,
'targets' => [
'file' => [
'class' => 'yii/log/FileTarget',
'levels' => ['error', 'warning'],
],
'db' => [
'class' => 'yii/log/DbTarget',
'categories' => [
'api/behaviors/RequestLogBehavior::beforeRequest',
],
'prefix' => function () {
$url = !Yii::$app->request->isConsoleRequest ? Yii::$app->request->getUrl() : null;
$user = Yii::$app->has('user', true) ? Yii::$app->get('user') : null;
$userId = $user ? $user->getId(false) : '-';
return sprintf('[%s][%s][%s]', Yii::$app->id, $url, $userId);
},
'logVars' => [],
]
],
],
15、删除数据库中的所有表,重新执行命令,如图7
./yii migrate --migrationPath=@yii/log/migrations/ ./yii migrate
16、浏览数据库表,user、log表的排序规则为:utf8mb4_unicode_ci,且表列的排序规则也为:utf8mb4_unicode_ci,如图8
17、定义请求日志行为,触发 [[yii/base/Application::EVENT_BEFORE_REQUEST|EVENT_BEFORE_REQUEST]] 事件时,写入日志至数据库,新建 /api/behaviors/RequestLogBehavior.php
<?php
namespace api/behaviors;
use Yii;
use yii/base/Behavior;
/**
* Class RequestLogBehavior
* @package api/behaviors
* @author Qiang Wang <shuijingwanwq@163.com>
* @since 1.0
*/
class RequestLogBehavior extends Behavior
{
public function events()
{
return [
Yii::$app::EVENT_BEFORE_REQUEST => 'beforeRequest',
];
}
/**
* @inheritdoc
*/
public function beforeRequest($event)
{
$url = !Yii::$app->request->isConsoleRequest ? Yii::$app->request->getUrl() : null;
$requestQueryParams = Yii::$app->getRequest()->getQueryParams();
$requestBodyParams = Yii::$app->getRequest()->getBodyParams();
$user = Yii::$app->has('user', true) ? Yii::$app->get('user') : null;
$userId = $user ? $user->getId(false) : '-';
$message = [
'url' => $url,
'requestQueryParams' => $requestQueryParams,
'requestBodyParams' => $requestBodyParams,
'userId' => $userId,
'$_SERVER' => [
'HTTP_ACCEPT_LANGUAGE' => $_SERVER['HTTP_ACCEPT_LANGUAGE'],
'HTTP_ACCEPT' => $_SERVER['HTTP_ACCEPT'],
'HTTP_HOST' => $_SERVER['HTTP_HOST'],
'REMOTE_ADDR' => $_SERVER['REMOTE_ADDR'],
'REQUEST_URI' => $_SERVER['REQUEST_URI'],
'REQUEST_METHOD' => $_SERVER['REQUEST_METHOD'],
'CONTENT_TYPE' => $_SERVER['CONTENT_TYPE'],
],
];
Yii::info(serialize($message), __METHOD__);
}
}
18、基于配置将行为附加到应用主体,编辑 /api/config/main.php
'components' => [
],
'as requestLog' => [
'class' => api/behaviors/RequestLogBehavior::class,
],
19、查看日志表,控制台应用的运行未写入日志,因为通过 [[yii/log/Target::except|except]] 属性来设置所有分类作为黑名单,如图9
20、运行接口应用,在 Postman 上执行1个不存在的接口请求,响应如下
{
"name": "Not Found",
"message": "页面未找到。",
"code": 0,
"status": 404,
"type": "yii//web//NotFoundHttpException"
}
21、运行接口应用,在 Postman 上执行3个已存在的接口请求,依次响应如下
{
"name": "Not Found",
"message": "User ID: 1, does not exist",
"code": 20002,
"status": 404,
"type": "yii//web//NotFoundHttpException"
}
{
"code": 10000,
"message": "Create user success",
"data": {
"username": "111111",
"email": "111111@163.com",
"password_hash": "$2y$13$2PjzCSRtyblFnpfgAW6HL.LUqVLzWqHcOtmKgttpcGtpXY6DtKRmy",
"auth_key": "gz-Cv8BczFGy2dFyd8ULjA_m1FK56vST",
"status": 10,
"created_at": 1529564925,
"updated_at": 1529564925,
"id": 1
}
}
{
"code": 10000,
"message": "获取用户列表成功",
"data": {
"items": [
{
"id": 1,
"username": "111111",
"auth_key": "gz-Cv8BczFGy2dFyd8ULjA_m1FK56vST",
"password_hash": "$2y$13$2PjzCSRtyblFnpfgAW6HL.LUqVLzWqHcOtmKgttpcGtpXY6DtKRmy",
"password_reset_token": null,
"email": "111111@163.com",
"status": 10,
"created_at": 1529564925,
"updated_at": 1529564925
}
],
"_links": {
"self": {
"href": "http://api.github-shuijingwan-yii2-app-advanced.localhost/v1/users?page=1"
}
},
"_meta": {
"totalCount": 1,
"pageCount": 1,
"currentPage": 1,
"perPage": 20
}
}
}
22、查看日志表,接口应用的运行已写入日志,因为通过 [[yii/log/Target::categories|categories]] 属性来设置 api/behaviors/RequestLogBehavior::beforeRequest 分类作为白名单,因此仅有 api/behaviors/RequestLogBehavior::beforeRequest 分类下的日志被写入,由于总计执行了4次,写入了4条日志,如图10
23、实现日志功能的相应接口,打开网址:http://www.github-shuijingwan-yii2-app-advanced.localhost/gii/model ,选项,命名空间为common/models,此时需支持国际化,生成 /common/models/Log.php,如图11
<?php
namespace common/models;
use Yii;
/**
* This is the model class for table "{{%log}}".
*
* @property int $id
* @property int $level
* @property string $category
* @property double $log_time
* @property string $prefix
* @property string $message
*/
class Log extends /yii/db/ActiveRecord
{
/**
* @inheritdoc
*/
public static function tableName()
{
return '{{%log}}';
}
/**
* @inheritdoc
*/
public function rules()
{
return [
[['level'], 'integer'],
[['log_time'], 'number'],
[['prefix', 'message'], 'string'],
[['category'], 'string', 'max' => 255],
];
}
/**
* @inheritdoc
*/
public function attributeLabels()
{
return [
'id' => Yii::t('model/log', 'ID'),
'level' => Yii::t('model/log', 'Level'),
'category' => Yii::t('model/log', 'Category'),
'log_time' => Yii::t('model/log', 'Log Time'),
'prefix' => Yii::t('model/log', 'Prefix'),
'message' => Yii::t('model/log', 'Message'),
];
}
}
24、新建 /common/logics/Log.php,在common/logics目录中的MySQL模型文件为业务逻辑相关,继承至 /common/models/Log 数据层
<?php
namespace common/logics;
use Yii;
/**
* This is the model class for table "{{%log}}".
*
* @property int $id
* @property int $level
* @property string $category
* @property double $log_time
* @property string $prefix
* @property string $message
*/
class Log extends /common/models/Log
{
}
25、新建 /common/messages/en-US/model/log.php,支持目标语言为英语美国时的消息翻译
<?php
/**
* Created by PhpStorm.
* User: WangQiang
* Date: 2018/06/21
* Time: 15:41
*/
return [
'ID' => 'ID',
'Level' => 'Level',
'Category' => 'Category',
'Log Time' => 'Log Time',
'Prefix' => 'Prefix',
'Message' => 'Message',
];
26、新建 /common/messages/zh-CN/model/log.php,支持目标语言为简体中文时的消息翻译
<?php
/**
* Created by PhpStorm.
* User: WangQiang
* Date: 2018/06/21
* Time: 15:44
*/
return [
'ID' => 'ID',
'Level' => '等级',
'Category' => '分类',
'Log Time' => '日志时间',
'Prefix' => '前缀',
'Message' => '消息',
];
27、新建 /api/models/Log.php,在api/models目录中的MySQL模型文件为业务逻辑相关(仅与api相关),继承至 /common/logics/Log 逻辑层
<?php
/**
* Created by PhpStorm.
* User: WangQiang
* Date: 2018/06/21
* Time: 15:50
*/
namespace api/models;
class Log extends /common/logics/Log
{
}
28、新建 /api/modules/v1/models/Log.php,继承至 /api/models/Log.php
注:/api/modules/v1/models/Log(仅用于 v1 模块) > /api/models/Log(仅用于 api 应用) > /common/logics/Log.php(可用于 api、frontend 等多个应用) > /common/models/Log.php(仅限于 Gii 生成) > /yii/db/ActiveRecord
<?php
/**
* Created by PhpStorm.
* User: WangQiang
* Date: 2018/06/21
* Time: 15:53
*/
namespace api/modules/v1/models;
class Log extends /api/models/Log
{
}
29、实现日志功能的相应接口,编辑 /api/config/urlManager.php,仅支持列表与详情
<?php
return [
'class' => yii/web/UrlManager::class,
'enablePrettyUrl' => true,
'enableStrictParsing' => true,
'showScriptName' => false,
'rules' => [
[
'class' => 'yii/rest/UrlRule',
'controller' => ['v1/user'],
],
[
'class' => 'yii/rest/UrlRule',
'controller' => ['v1/log'],
'only' => ['index', 'view'],
],
],
];
30、新建控制器类 /api/controllers/LogController.php
<?php
/**
* Created by PhpStorm.
* User: WangQiang
* Date: 2018/06/21
* Time: 15:29
*/
namespace api/controllers;
use yii/rest/ActiveController;
class LogController extends ActiveController
{
public $serializer = [
'class' => 'api/rests/log/Serializer',
'collectionEnvelope' => 'items',
];
/**
* @inheritdoc
*/
public function actions()
{
$actions = parent::actions();
// 禁用"create"、"update"、"delete"、"options"动作
unset($actions['create'], $actions['update'], $actions['delete'], $actions['options']);
$actions['index']['class'] = 'api/rests/log/IndexAction';
$actions['view']['class'] = 'api/rests/log/ViewAction';
return $actions;
}
}
31、新建 /api/modules/v1/controllers/LogController.php,通过指定 [[yii/rest/ActiveController::modelClass|modelClass]] 作为 api/modules/v1/models/Log, 控制器就能知道使用哪个模型去获取和处理数据
注:/api/modules/v1/controllers/LogController.php(仅用于 v1 模块) > /api/controllers/LogController.php(仅用于 api 应用) > /yii/rest/ActiveController
<?php
namespace api/modules/v1/controllers;
/**
* Log controller for the `v1` module
*/
class LogController extends /api/controllers/LogController
{
public $modelClass = 'api/modules/v1/models/Log';
}
32、复制目录 /api/rests/user 下的 Action.php、IndexAction.php、ViewAction.php、Serializer.php 至目录 /api/rests/log
33、编辑 /api/rests/log/IndexAction.php,调整命名空间、继承关系、查询条件等
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace api/rests/log;
use Yii;
use yii/base/DynamicModel;
use yii/data/ActiveDataProvider;
/**
* IndexAction implements the API endpoint for listing multiple models.
*
* For more details and usage information on IndexAction, see the [guide article on rest controllers](guide:rest-controllers).
*
* @author Qiang Wang <shuijingwanwq@163.com>
* @since 1.0
*/
class IndexAction extends /yii/rest/IndexAction
{
/**
* Prepares the data provider that should return the requested collection of the models.
* @return ActiveDataProvider
*/
protected function prepareDataProvider()
{
$requestParams = Yii::$app->getRequest()->getBodyParams();
if (empty($requestParams)) {
$requestParams = Yii::$app->getRequest()->getQueryParams();
}
/* 数据过滤器 */
$this->dataFilter = [
'class' => 'yii/data/ActiveDataFilter',
'searchModel' => function () {
return (new DynamicModel(['level' => null, 'category' => null, 'log_time' => null, 'prefix' => null]))
->addRule('level', 'integer')
->addRule(['category', 'prefix'], 'trim')
->addRule('log_time', 'double')
->addRule(['category', 'prefix'], 'string');
},
];
$filter = null;
if ($this->dataFilter !== null) {
$this->dataFilter = Yii::createObject($this->dataFilter);
if ($this->dataFilter->load($requestParams)) {
$filter = $this->dataFilter->build();
if ($filter === false) {
foreach ($this->dataFilter->getFirstErrors() as $message) {
$firstErrors = $message;
break;
}
return ['code' => 20803, 'message' => Yii::t('error', Yii::t('error', Yii::t('error', '20803'), ['firstErrors' => $firstErrors]))];
}
}
}
if ($this->prepareDataProvider !== null) {
return call_user_func($this->prepareDataProvider, $this, $filter);
}
/* @var $modelClass /yii/db/BaseActiveRecord */
$modelClass = $this->modelClass;
$query = $modelClass::find();
if (!empty($filter)) {
$query->andFilterWhere($filter);
}
return Yii::createObject([
'class' => ActiveDataProvider::className(),
'query' => $query,
'pagination' => [
'params' => $requestParams,
],
'sort' => [
'params' => $requestParams,
],
]);
}
}
34、编辑 /api/rests/log/Serializer.php,调整命名空间、继承关系、响应结构(响应成功:”code”: 10000,”message”,”data”;响应失败:”code”: 不等于10000的其他数字,”message”)等
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace api/rests/log;
use Yii;
use yii/data/DataProviderInterface;
/**
* Serializer converts resource objects and collections into array representation.
*
* Serializer is mainly used by REST controllers to convert different objects into array representation
* so that they can be further turned into different formats, such as JSON, XML, by response formatters.
*
* The default implementation handles resources as [[Model]] objects and collections as objects
* implementing [[DataProviderInterface]]. You may override [[serialize()]] to handle more types.
*
* @author Qiang Wang <shuijingwanwq@163.com>
* @since 1.0
*/
class Serializer extends /yii/rest/Serializer
{
/**
* Serializes a data provider.
* @param DataProviderInterface $dataProvider
* @return array the array representation of the data provider.
*/
protected function serializeDataProvider($dataProvider)
{
if ($this->preserveKeys) {
$models = $dataProvider->getModels();
} else {
$models = array_values($dataProvider->getModels());
}
$models = $this->serializeModels($models);
if (($pagination = $dataProvider->getPagination()) !== false) {
$this->addPaginationHeaders($pagination);
}
if ($this->request->getIsHead()) {
return null;
} elseif ($this->collectionEnvelope === null) {
return $models;
}
$result = [
$this->collectionEnvelope => $models,
];
if (empty($result['items'])) {
return ['code' => 20801, 'message' => Yii::t('error', '20801')];
}
foreach ($result['items'] as $key => $item) {
$result['items'][$key]['message'] = $item['message'] = unserialize($item['message']);
if (empty($item['message']['userId'])) {
$result['items'][$key]['message']['userId'] = '0';
}
if (empty($item['message']['requestQueryParams'])) {
$result['items'][$key]['message']['requestQueryParams'] = (object)[];
}
if (empty($item['message']['requestBodyParams'])) {
$result['items'][$key]['message']['requestBodyParams'] = (object)[];
}
}
if ($pagination !== false) {
return ['code' => 10000, 'message' => Yii::t('success', '10801'), 'data' => array_merge($result, $this->serializePagination($pagination))];
}
return ['code' => 10000, 'message' => Yii::t('success', '10801'), 'data' => $result];
}
}
35、编辑语言包文件:/api/messages/zh-CN/success.php(简体中文、响应成功)
<?php
return [
10000 => 'success',
10001 => '获取用户列表成功',
10002 => '获取用户详情成功',
10003 => '创建用户成功',
10004 => '更新用户成功',
10005 => '删除用户成功',
10801 => '获取日志列表成功',
10802 => '获取日志详情成功',
];
36、编辑语言包文件:/api/messages/zh-CN/error.php(简体中文、响应失败)
<?php
return [
20000 => 'error',
20001 => '用户列表为空',
20002 => '用户ID:{id},不存在',
20003 => '用户ID:{id},的状态为已删除',
20004 => '数据验证失败:{firstErrors}',
20801 => '日志列表为空',
20802 => '日志ID:{id},不存在',
20803 => '数据过滤器验证失败:{firstErrors}',
];
37、编辑语言包文件:/api/messages/en-US/success.php(英语美国、响应成功)
<?php
return [
10000 => 'success',
10001 => 'Get user list success',
10002 => 'Get user details success',
10003 => 'Create user success',
10004 => 'Update user success',
10005 => 'Delete user success',
10801 => 'Get log list success',
10802 => 'Get log details success',
];
38、编辑语言包文件:/api/messages/en-US/error.php(英语美国、响应失败)
<?php
return [
20000 => 'error',
20001 => 'User list is empty',
20002 => 'User ID: {id}, does not exist',
20003 => 'User ID: {id}, status is deleted',
20004 => 'Data validation failed: {firstErrors}',
20801 => 'Log list is empty',
20802 => 'Log ID: {id}, does not exist',
20803 => 'Data filter validation failed: {firstErrors}',
];
39、在 Postman 中,GET http://api.github-shuijingwan-yii2-app-advanced.localhost/v1/logs ,200响应,requestQueryParams、requestBodyParams的格式有时为数组(为空时),有时为对象,如图12
{
"code": 10000,
"message": "获取日志列表成功",
"data": {
"items": [
{
"id": 1,
"level": 4,
"category": "api//behaviors//RequestLogBehavior::beforeRequest",
"log_time": 1529564823.0948,
"prefix": "[app-api][/v1/menus][]",
"message": {
"url": "/v1/menus",
"requestQueryParams": {},
"requestBodyParams": {},
"userId": "0",
"$_SERVER": {
"HTTP_ACCEPT_LANGUAGE": "zh-CN",
"HTTP_ACCEPT": "application/json; version=0.0; cookie=enable",
"HTTP_HOST": "api.github-shuijingwan-yii2-app-advanced.localhost",
"REMOTE_ADDR": "127.0.0.1",
"REQUEST_URI": "/v1/menus",
"REQUEST_METHOD": "GET",
"CONTENT_TYPE": "application/x-www-form-urlencoded"
}
}
},
{
"id": 2,
"level": 4,
"category": "api//behaviors//RequestLogBehavior::beforeRequest",
"log_time": 1529564916.6603,
"prefix": "[app-api][/v1/users/1][]",
"message": {
"url": "/v1/users/1",
"requestQueryParams": {},
"requestBodyParams": {
"email": "222222@qq.com",
"password": "222222",
"status": "0"
},
"userId": "0",
"$_SERVER": {
"HTTP_ACCEPT_LANGUAGE": "en-US",
"HTTP_ACCEPT": "application/json; version=0.0",
"HTTP_HOST": "api.github-shuijingwan-yii2-app-advanced.localhost",
"REMOTE_ADDR": "127.0.0.1",
"REQUEST_URI": "/v1/users/1",
"REQUEST_METHOD": "PUT",
"CONTENT_TYPE": "application/x-www-form-urlencoded"
}
}
},
{
"id": 3,
"level": 4,
"category": "api//behaviors//RequestLogBehavior::beforeRequest",
"log_time": 1529564924.6648,
"prefix": "[app-api][/v1/users][]",
"message": {
"url": "/v1/users",
"requestQueryParams": {},
"requestBodyParams": {
"email": "111111@163.com",
"password": "111111",
"username": "111111"
},
"userId": "0",
"$_SERVER": {
"HTTP_ACCEPT_LANGUAGE": "en-US",
"HTTP_ACCEPT": "application/json; version=0.0",
"HTTP_HOST": "api.github-shuijingwan-yii2-app-advanced.localhost",
"REMOTE_ADDR": "127.0.0.1",
"REQUEST_URI": "/v1/users",
"REQUEST_METHOD": "POST",
"CONTENT_TYPE": "application/x-www-form-urlencoded"
}
}
}
],
"_links": {
"self": {
"href": "http://api.github-shuijingwan-yii2-app-advanced.localhost/v1/logs?per-page=3&page=1"
},
"next": {
"href": "http://api.github-shuijingwan-yii2-app-advanced.localhost/v1/logs?per-page=3&page=2"
},
"last": {
"href": "http://api.github-shuijingwan-yii2-app-advanced.localhost/v1/logs?per-page=3&page=71"
}
},
"_meta": {
"totalCount": 212,
"pageCount": 71,
"currentPage": 1,
"perPage": 3
}
}
}
40、在 Postman 中,GET http://api.github-shuijingwan-yii2-app-advanced.localhost/v1/logs?filter[level]=a ,200响应,如图13
{
"code": 20803,
"message": "数据过滤器验证失败:Level必须是整数。"
}
41、在 Postman 中,GET http://api.github-shuijingwan-yii2-app-advanced.localhost/v1/logs?filter[level]=4&filter[category][like]=RequestLogBehavior&filter[prefix][like]=app-api&filter[log_time][gte]=1528090828&filter[log_time][lte]=1529564924.6648 ,测试数据过滤器,200响应,如图14
filter[level]:4 filter[category][like]:RequestLogBehavior filter[prefix][like]:app-api filter[log_time][gte]:1528090828 filter[log_time][lte]:1529564924.6648
{
"code": 10000,
"message": "获取日志列表成功",
"data": {
"items": [
{
"id": 1,
"level": 4,
"category": "api//behaviors//RequestLogBehavior::beforeRequest",
"log_time": 1529564823.0948,
"prefix": "[app-api][/v1/menus][]",
"message": {
"url": "/v1/menus",
"requestQueryParams": {},
"requestBodyParams": {},
"userId": "0",
"$_SERVER": {
"HTTP_ACCEPT_LANGUAGE": "zh-CN",
"HTTP_ACCEPT": "application/json; version=0.0; cookie=enable",
"HTTP_HOST": "api.github-shuijingwan-yii2-app-advanced.localhost",
"REMOTE_ADDR": "127.0.0.1",
"REQUEST_URI": "/v1/menus",
"REQUEST_METHOD": "GET",
"CONTENT_TYPE": "application/x-www-form-urlencoded"
}
}
},
{
"id": 2,
"level": 4,
"category": "api//behaviors//RequestLogBehavior::beforeRequest",
"log_time": 1529564916.6603,
"prefix": "[app-api][/v1/users/1][]",
"message": {
"url": "/v1/users/1",
"requestQueryParams": {},
"requestBodyParams": {
"email": "222222@qq.com",
"password": "222222",
"status": "0"
},
"userId": "0",
"$_SERVER": {
"HTTP_ACCEPT_LANGUAGE": "en-US",
"HTTP_ACCEPT": "application/json; version=0.0",
"HTTP_HOST": "api.github-shuijingwan-yii2-app-advanced.localhost",
"REMOTE_ADDR": "127.0.0.1",
"REQUEST_URI": "/v1/users/1",
"REQUEST_METHOD": "PUT",
"CONTENT_TYPE": "application/x-www-form-urlencoded"
}
}
},
{
"id": 3,
"level": 4,
"category": "api//behaviors//RequestLogBehavior::beforeRequest",
"log_time": 1529564924.6648,
"prefix": "[app-api][/v1/users][]",
"message": {
"url": "/v1/users",
"requestQueryParams": {},
"requestBodyParams": {
"email": "111111@163.com",
"password": "111111",
"username": "111111"
},
"userId": "0",
"$_SERVER": {
"HTTP_ACCEPT_LANGUAGE": "en-US",
"HTTP_ACCEPT": "application/json; version=0.0",
"HTTP_HOST": "api.github-shuijingwan-yii2-app-advanced.localhost",
"REMOTE_ADDR": "127.0.0.1",
"REQUEST_URI": "/v1/users",
"REQUEST_METHOD": "POST",
"CONTENT_TYPE": "application/x-www-form-urlencoded"
}
}
}
],
"_links": {
"self": {
"href": "http://api.github-shuijingwan-yii2-app-advanced.localhost/v1/logs?filter%5Blevel%5D=4&filter%5Bcategory%5D%5Blike%5D=RequestLogBehavior&filter%5Bprefix%5D%5Blike%5D=app-api&filter%5Blog_time%5D%5Bgte%5D=1528090828&filter%5Blog_time%5D%5Blte%5D=1529564924.6648&page=1"
}
},
"_meta": {
"totalCount": 3,
"pageCount": 1,
"currentPage": 1,
"perPage": 20
}
}
}
SELECT COUNT(*) FROM `log` WHERE (`level`='4') AND (`category` LIKE '%RequestLogBehavior%') AND (`prefix` LIKE '%app-api%') AND ((`log_time` >= '1528090828') AND (`log_time` <= '1529564924.6648'))
42、定义一个搜索模型,此搜索模型应声明所有可用的搜索属性及其验证规则,新建 /common/logics/LogSearch.php
<?php
namespace common/logics;
use Yii;
use yii/base/Model;
/**
* LogSearch represents the model behind the search form about `common/models/Log`.
*/
class LogSearch extends Model
{
public $level;
public $category;
public $log_time;
public $prefix;
/**
* @inheritdoc
*/
public function rules()
{
return [
[['level'], 'integer'],
[['log_time'], 'number'],
[['prefix', 'message'], 'string'],
[['category', 'prefix'], 'trim'],
];
}
}
43、新建 /api/models/LogSearch.php,在api/models目录中的MySQL模型文件为业务逻辑相关(仅与api相关),继承至 /common/logics/LogSearch 逻辑层
<?php
/**
* Created by PhpStorm.
* User: WangQiang
* Date: 2018/06/22
* Time: 13:43
*/
namespace api/models;
class LogSearch extends /common/logics/LogSearch
{
}
44、新建 /api/modules/v1/models/LogSearch.php,继承至 /api/models/LogSearch.php
<?php
/**
* Created by PhpStorm.
* User: WangQiang
* Date: 2018/06/22
* Time: 13:46
*/
namespace api/modules/v1/models;
class LogSearch extends /api/models/LogSearch
{
}
45、编辑 /api/rests/log/IndexAction.php,取消使用 yii/base/DynamicModel实例作为$searchModel,设置数据过滤器以启用筛选器处理,生成SQL语句,如图15
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace api/rests/log;
use Yii;
use yii/data/ActiveDataProvider;
/**
* IndexAction implements the API endpoint for listing multiple models.
*
* For more details and usage information on IndexAction, see the [guide article on rest controllers](guide:rest-controllers).
*
* @author Qiang Wang <shuijingwanwq@163.com>
* @since 1.0
*/
class IndexAction extends /yii/rest/IndexAction
{
public $dataFilter = [
'class' => 'yii/data/ActiveDataFilter',
'searchModel' => 'api/modules/v1/models/LogSearch',
];
/**
* Prepares the data provider that should return the requested collection of the models.
* @return ActiveDataProvider
*/
protected function prepareDataProvider()
{
$requestParams = Yii::$app->getRequest()->getBodyParams();
if (empty($requestParams)) {
$requestParams = Yii::$app->getRequest()->getQueryParams();
}
$filter = null;
if ($this->dataFilter !== null) {
$this->dataFilter = Yii::createObject($this->dataFilter);
if ($this->dataFilter->load($requestParams)) {
$filter = $this->dataFilter->build();
if ($filter === false) {
foreach ($this->dataFilter->getFirstErrors() as $message) {
$firstErrors = $message;
break;
}
return ['code' => 20803, 'message' => Yii::t('error', Yii::t('error', Yii::t('error', '20803'), ['firstErrors' => $firstErrors]))];
}
}
}
if ($this->prepareDataProvider !== null) {
return call_user_func($this->prepareDataProvider, $this, $filter);
}
/* @var $modelClass /yii/db/BaseActiveRecord */
$modelClass = $this->modelClass;
$query = $modelClass::find();
if (!empty($filter)) {
$query->andFilterWhere($filter);
}
return Yii::createObject([
'class' => ActiveDataProvider::className(),
'query' => $query,
'pagination' => [
'params' => $requestParams,
],
'sort' => [
'params' => $requestParams,
],
]);
}
}
SELECT COUNT(*) FROM `log` WHERE (`level`='4') AND (`category` LIKE '%RequestLogBehavior%') AND (`prefix` LIKE '%app-api%') AND ((`log_time` >= '1528090828') AND (`log_time` <= '1529564924.6648'))
46、GET /logs/1: 返回日志 1 的详细信息,编辑 /api/rests/log/Action.php,调整命名空间、继承关系、响应结构等
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace api/rests/log;
use Yii;
use yii/db/ActiveRecordInterface;
use yii/web/NotFoundHttpException;
/**
* Action is the base class for action classes that implement RESTful API.
*
* For more details and usage information on Action, see the [guide article on rest controllers](guide:rest-controllers).
*
* @author Qiang Wang <shuijingwanwq@163.com>
* @since 1.0
*/
class Action extends /yii/rest/Action
{
/**
* Returns the data model based on the primary key given.
* If the data model is not found, a 404 HTTP exception will be raised.
* @param string $id the ID of the model to be loaded. If the model has a composite primary key,
* the ID must be a string of the primary key values separated by commas.
* The order of the primary key values should follow that returned by the `primaryKey()` method
* of the model.
* @return ActiveRecordInterface the model found
* @throws NotFoundHttpException if the model cannot be found
*/
public function findModel($id)
{
if ($this->findModel !== null) {
return call_user_func($this->findModel, $id, $this);
}
/* @var $modelClass ActiveRecordInterface */
$modelClass = $this->modelClass;
$keys = $modelClass::primaryKey();
if (count($keys) > 1) {
$values = explode(',', $id);
if (count($keys) === count($values)) {
$model = $modelClass::findOne(array_combine($keys, $values));
}
} elseif ($id !== null) {
$model = $modelClass::findOne($id);
}
if (isset($model)) {
return $model;
}
throw new NotFoundHttpException(Yii::t('error', Yii::t('error', Yii::t('error', '20802'), ['id' => $id])), 20802);
}
}
47、编辑 /api/rests/log/ViewAction.php,调整命名空间、继承关系、响应结构等。ContentNegotiator支持响应内容格式处理和语言处理。 通过检查 GET 参数和 Accept HTTP头部来决定响应内容格式和语言。配置ContentNegotiator支持英语美国和简体中文。配置响应组件,传递给 yii/helpers/Json::encode() 的编码选项,JSON_FORCE_OBJECT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE,JSON_FORCE_OBJECT:使一个非关联数组输出一个类(Object)而非数组。在数组为空而接受者需要一个类(Object)的时候尤其有用。避免手动处理空数组的转换。
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace api/rests/log;
use Yii;
/**
* ViewAction implements the API endpoint for returning the detailed information about a model.
*
* For more details and usage information on ViewAction, see the [guide article on rest controllers](guide:rest-controllers).
*
* @author Qiang Wang <shuijingwanwq@163.com>
* @since 1.0
*/
class ViewAction extends Action
{
/**
* Displays a model.
* @param string $id the primary key of the model.
* @return /yii/db/ActiveRecordInterface the model being displayed
*/
public function run($id)
{
$model = $this->findModel($id);
if ($this->checkAccess) {
call_user_func($this->checkAccess, $this->id, $model);
}
$model->message = $message = unserialize($model->message);
if (empty($message['userId'])) {
$message['userId'] = '0';
}
$model->message = $message;
$response = Yii::$app->response;
$response->formatters = [
yii/web/Response::FORMAT_JSON => [
'class' => 'yii/web/JsonResponseFormatter',
'encodeOptions' => 336,
],
yii/web/Response::FORMAT_XML => [
'class' => 'yii/web/XmlResponseFormatter',
],
];
return ['code' => 10000, 'message' => Yii::t('success', '10802'), 'data' => $model];
}
}
48、在 Postman 中,GET http://api.github-shuijingwan-yii2-app-advanced.localhost/v1/logs/3, 如图16
{
"code": 10000,
"message": "获取日志详情成功",
"data": {
"id": 3,
"level": 4,
"category": "api//behaviors//RequestLogBehavior::beforeRequest",
"log_time": 1529564924.6648,
"prefix": "[app-api][/v1/users][]",
"message": {
"url": "/v1/users",
"requestQueryParams": {},
"requestBodyParams": {
"email": "111111@163.com",
"password": "111111",
"username": "111111"
},
"userId": "0",
"$_SERVER": {
"HTTP_ACCEPT_LANGUAGE": "en-US",
"HTTP_ACCEPT": "application/json; version=0.0",
"HTTP_HOST": "api.github-shuijingwan-yii2-app-advanced.localhost",
"REMOTE_ADDR": "127.0.0.1",
"REQUEST_URI": "/v1/users",
"REQUEST_METHOD": "POST",
"CONTENT_TYPE": "application/x-www-form-urlencoded"
}
}
}
}
SELECT * FROM `log` WHERE `id`='3'
49、定义请求日志行为,触发 [[yii/base/Application::EVENT_BEFORE_REQUEST|EVENT_BEFORE_REQUEST]] 事件时,写入日志至数据库,编辑 /api/behaviors/RequestLogBehavior.php,将 null 替换为 ”,以保持字段格式统一。
50、接口应用的配置文件,/api/config/main.php,设置日志目标为 DbTarget,设置白名单分类为 api/behaviors/RequestLogBehavior::afterRequest 编辑代码,避免 message[‘userId’] 无法获取值的情况,注:会导致404等请求无法写入日志
'log' => [
'traceLevel' => YII_DEBUG ? 3 : 0,
'targets' => [
'file' => [
'class' => 'yii/log/FileTarget',
'levels' => ['error', 'warning'],
],
'db' => [
'class' => 'yii/log/DbTarget',
'categories' => [
'api/behaviors/RequestLogBehavior::afterRequest',
],
'prefix' => function () {
$url = !Yii::$app->request->isConsoleRequest ? Yii::$app->request->getUrl() : null;
$user = Yii::$app->has('user', true) ? Yii::$app->get('user') : null;
$userId = $user ? $user->getId(false) : '-';
return sprintf('[%s][%s][%s]', Yii::$app->id, $url, $userId);
},
'logVars' => [],
]
],
51、定义请求日志行为,触发 [[yii/base/Application::EVENT_AFTER_REQUEST|EVENT_AFTER_REQUEST]] 事件时,写入日志至数据库,编辑 /api/behaviors/RequestLogBehavior.php,且将字段调整为小写,将其他文件中使用对应字段处,同步修改
<?php
namespace api/behaviors;
use Yii;
use yii/base/Behavior;
/**
* Class RequestLogBehavior
* @package api/behaviors
* @author Qiang Wang <shuijingwanwq@163.com>
* @since 1.0
*/
class RequestLogBehavior extends Behavior
{
public function events()
{
return [
Yii::$app::EVENT_AFTER_REQUEST => 'afterRequest',
];
}
/**
* @inheritdoc
*/
public function afterRequest($event)
{
$url = !Yii::$app->request->isConsoleRequest ? Yii::$app->request->getUrl() : '';
$requestQueryParams = Yii::$app->getRequest()->getQueryParams();
$requestBodyParams = Yii::$app->getRequest()->getBodyParams();
$user = Yii::$app->has('user', true) ? Yii::$app->get('user') : null;
$userId = $user ? $user->getId(false) : '-';
$message = [
'url' => $url,
'request_query_params' => $requestQueryParams,
'request_body_params' => $requestBodyParams,
'user_id' => $userId,
'$_SERVER' => [
'HTTP_ACCEPT_LANGUAGE' => $_SERVER['HTTP_ACCEPT_LANGUAGE'],
'HTTP_ACCEPT' => $_SERVER['HTTP_ACCEPT'],
'HTTP_HOST' => $_SERVER['HTTP_HOST'],
'REMOTE_ADDR' => $_SERVER['REMOTE_ADDR'],
'REQUEST_URI' => $_SERVER['REQUEST_URI'],
'REQUEST_METHOD' => $_SERVER['REQUEST_METHOD'],
'CONTENT_TYPE' => $_SERVER['CONTENT_TYPE'],
],
];
Yii::info(serialize($message), __METHOD__);
}
}
52、在 Postman 中,GET http://api.github-shuijingwan-yii2-app-advanced.localhost/v1/logs?page=77&per-page=3 ,200响应
{
"code": 10000,
"message": "获取日志列表成功",
"data": {
"items": [
{
"id": 229,
"level": 4,
"category": "api//behaviors//RequestLogBehavior::afterRequest",
"log_time": 1529980262.0921,
"prefix": "[app-api][/v1/logs?page=12&per-page=76][]",
"message": {
"url": "/v1/logs?page=12&per-page=76",
"request_query_params": {
"page": "12",
"per-page": "76"
},
"request_body_params": {},
"user_id": "0",
"$_SERVER": {
"HTTP_ACCEPT_LANGUAGE": "zh-CN",
"HTTP_ACCEPT": "application/json; version=0.0",
"HTTP_HOST": "api.github-shuijingwan-yii2-app-advanced.localhost",
"REMOTE_ADDR": "127.0.0.1",
"REQUEST_URI": "/v1/logs?page=12&per-page=76",
"REQUEST_METHOD": "GET",
"CONTENT_TYPE": ""
}
}
},
{
"id": 230,
"level": 4,
"category": "api//behaviors//RequestLogBehavior::afterRequest",
"log_time": 1529980273.5208,
"prefix": "[app-api][/v1/logs?page=76&per-page=3][]",
"message": {
"url": "/v1/logs?page=76&per-page=3",
"request_query_params": {
"page": "76",
"per-page": "3"
},
"request_body_params": {},
"user_id": "0",
"$_SERVER": {
"HTTP_ACCEPT_LANGUAGE": "zh-CN",
"HTTP_ACCEPT": "application/json; version=0.0",
"HTTP_HOST": "api.github-shuijingwan-yii2-app-advanced.localhost",
"REMOTE_ADDR": "127.0.0.1",
"REQUEST_URI": "/v1/logs?page=76&per-page=3",
"REQUEST_METHOD": "GET",
"CONTENT_TYPE": ""
}
}
}
],
"_links": {
"self": {
"href": "http://api.github-shuijingwan-yii2-app-advanced.localhost/v1/logs?page=77&per-page=3"
},
"first": {
"href": "http://api.github-shuijingwan-yii2-app-advanced.localhost/v1/logs?page=1&per-page=3"
},
"prev": {
"href": "http://api.github-shuijingwan-yii2-app-advanced.localhost/v1/logs?page=76&per-page=3"
}
},
"_meta": {
"totalCount": 230,
"pageCount": 77,
"currentPage": 77,
"perPage": 3
}
}
}
原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/tech/pnotes/250416.html
