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/250416.html