基于 yiisoft/yii2-app-advanced,在 GitHub 上新建仓库 yii2-app-advanced,新建接口应用(实现 RESTful 风格的 Web Service 服务的 API),调整默认字符集为:utf8mb4,接口响应格式的调整,空数组自动转换为空对象,在接口应用中收集请求日志消息(1个请求对应1条日志消息)至数据库,且实现日志功能的相应接口:日志列表(设置数据过滤器以启用筛选器处理)、日志详情 (五) (1)
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 } } }
近期评论