1. Set the default collation of the database to: utf8mb4_unicode_ci, as shown in Figure 1

2. Change the default character set used for database connections to: utf8mb4. Edit the configuration file for the development environment, \environments\dev\common\config\main-local.php, and edit the configuration file for the production environment, \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. Execute the initialization command, as shown in Figure 2

.\init
4. Edit the database configuration, \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. Empty the database and execute the database migration command, as shown in Figure 3

.\yii migrate
6. The database schema for logs can be initialized via application migration. Execute the following command, as shown in Figure 4

.\yii migrate --migrationPath=@yii/log/migrations/
7. Browse the database tables. The collation of the user and log tables is: utf8_unicode_ci, as shown in Figure 5

8. Create a new database migration file and adjust the collation to: utf8mb4_unicode_ci. Execute the command, as shown in Figure 6

.\yii migrate/create update_table_options_to_log
9. Modify the default character set of the tables (user, log) and the character set of all character columns. Edit \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. A [[yii\log\DbTarget|database target]] exports filtered log messages to a database table. Set the log target to DbTarget
11. Configuration file for the console application, \console\config\main.php, code
'components' => [
'log' => [
'targets' => [
[
'class' => 'yii\log\FileTarget',
'levels' => ['error', 'warning'],
],
],
],
],
12. Configuration file for the console application, \console\config\main.php. Set the log target to DbTarget and edit the code
'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. Configuration file for the API application, \api\config\main.php, code
<?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. Configuration file for the API application, \api\config\main.php. Set the log target to DbTarget and edit the code
'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. Delete all tables in the database and re-execute the command, as shown in Figure 7

.\yii migrate --migrationPath=@yii/log/migrations/
.\yii migrate
16. Browse the database tables. The collation of the user and log tables is: utf8mb4_unicode_ci, and the collation of the table columns is also: utf8mb4_unicode_ci, as shown in Figure 8

17. Define the request log behavior. When the [[yii\base\Application::EVENT_BEFORE_REQUEST|EVENT_BEFORE_REQUEST]] event is triggered, write the log to the database. Create \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. Attach the behavior to the application subject based on the configuration. Edit \api\config\main.php
'components' => [
],
'as requestLog' => [
'class' => api\behaviors\RequestLogBehavior::class,
],
19. View the log table. The execution of the console application did not write to the log because all categories are set as a blacklist via the [[yii\log\Target::except|except]] property, as shown in Figure 9
![View the log table. The execution of the console application did not write to the log because all categories are set as a blacklist via the [[yii\log\Target::except|except]] property](https://media.shuijingwanwq.com/2018/06/9.png)
20. Run the API application and execute a non-existent API request in Postman. The response is as follows
{
"name": "Not Found",
"message": "页面未找到。",
"code": 0,
"status": 404,
"type": "yii\\web\\NotFoundHttpException"
}
21. Run the API application and execute 3 existing API requests in Postman. The responses are as follows in sequence
{
"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. View the log table. The execution of the API application has been written to the log because the api\behaviors\RequestLogBehavior::beforeRequest category is set as a whitelist via the [[yii\log\Target::categories|categories]] property. Therefore, only logs under the api\behaviors\RequestLogBehavior::beforeRequest category are written. Since it was executed 4 times in total, 4 log entries were written, as shown in Figure 10
![View the log table. The execution of the API application has been written to the log because the api\behaviors\RequestLogBehavior::beforeRequest category is set as a whitelist via the [[yii\log\Target::categories|categories]] property. Therefore, only logs under the api\behaviors\RequestLogBehavior::beforeRequest category are written. Since it was executed 4 times in total, 4 log entries were written](https://media.shuijingwanwq.com/2018/06/10.png)
23. Implement the corresponding interfaces for the log functionality. Open the URL: http://www.github-shuijingwan-yii2-app-advanced.localhost/gii/model , set the options with the namespace as common\models. At this point, internationalization needs to be supported. Generate \common\models\Log.php, as shown in Figure 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. Create \common\logics\Log.php. The MySQL model files in the common/logics directory are business logic related and inherit from the \common\models\Log data layer
<?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. Create \common\messages\en-US\model\log.php to support message translation when the target language is US English
<?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. Create \common\messages\zh-CN\model\log.php to support message translation when the target language is Simplified Chinese
<?php
/**
* Created by PhpStorm.
* User: WangQiang
* Date: 2018/06/21
* Time: 15:44
*/
return [
'ID' => 'ID',
'Level' => '等级',
'Category' => '分类',
'Log Time' => '日志时间',
'Prefix' => '前缀',
'Message' => '消息',
];
27. Create \api\models\Log.php. The MySQL model files in the api/models directory are business logic related (only related to the api) and inherit from the \common\logics\Log logic layer
<?php
/**
* Created by PhpStorm.
* User: WangQiang
* Date: 2018/06/21
* Time: 15:50
*/
namespace api\models;
class Log extends \common\logics\Log
{
}
28. Create \api\modules\v1\models\Log.php, inheriting from \api\models\Log.php
Note: \api\modules\v1\models\Log (only for the v1 module) > \api\models\Log (only for the api application) > \common\logics\Log.php (can be used in multiple applications such as api, frontend, etc.) > \common\models\Log.php (limited to Gii generation) > \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. Implement the corresponding interfaces for the log functionality. Edit \api\config\urlManager.php to support only list and detail
<?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. Create the controller class \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. Create \api\modules\v1\controllers\LogController.php. By specifying [[yii\rest\ActiveController::modelClass|modelClass]] as api\modules\v1\models\Log, the controller knows which model to use to fetch and process data
Note: \api\modules\v1\controllers\LogController.php (only for the v1 module) > \api\controllers\LogController.php (only for the api application) > \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. Copy Action.php, IndexAction.php, ViewAction.php, and Serializer.php from the \api\rests\user directory to the \api\rests\log directory
33. Edit \api\rests\log\IndexAction.php to adjust the namespace, inheritance relationship, query conditions, etc.
<?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. Edit \api\rests\log\Serializer.php to adjust the namespace, inheritance relationship, response structure (successful response: “code”: 10000,”message”,”data”; failed response: “code”: a number other than 10000,”message”), etc.
<?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. Edit the language pack file: \api\messages\zh-CN\success.php (Simplified Chinese, successful response)
<?php
return [
10000 => 'success',
10001 => '获取用户列表成功',
10002 => '获取用户详情成功',
10003 => '创建用户成功',
10004 => '更新用户成功',
10005 => '删除用户成功',
10801 => '获取日志列表成功',
10802 => '获取日志详情成功',
];36. Edit the language pack file: \api\messages\zh-CN\error.php (Simplified Chinese, failed response)
<?php
return [
20000 => 'error',
20001 => '用户列表为空',
20002 => '用户ID:{id},不存在',
20003 => '用户ID:{id},的状态为已删除',
20004 => '数据验证失败:{firstErrors}',
20801 => '日志列表为空',
20802 => '日志ID:{id},不存在',
20803 => '数据过滤器验证失败:{firstErrors}',
];37. Edit the language pack file: \api\messages\en-US\success.php (US English, successful response)
<?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. Edit the language pack file: \api\messages\en-US\error.php (US English, failed response)
<?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. In Postman, GET http://api.github-shuijingwan-yii2-app-advanced.localhost/v1/logs , 200 response. The format of requestQueryParams and requestBodyParams is sometimes an array (when empty) and sometimes an object, as shown in Figure 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. In Postman, GET http://api.github-shuijingwan-yii2-app-advanced.localhost/v1/logs?filter[level]=a , 200 response, as shown in Figure 13
![In Postman, GET http://api.github-shuijingwan-yii2-app-advanced.localhost/v1/logs?filter[level]=a , 200 response](https://media.shuijingwanwq.com/2018/06/13.png)
{
"code": 20803,
"message": "数据过滤器验证失败:Level必须是整数。"
}
41. In 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 , test the data filter, 200 response, as shown in Figure 14
![In 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 , test the data filter, 200 response](https://media.shuijingwanwq.com/2018/06/14-1.png)
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. Define a search model. This search model should declare all available search attributes and their validation rules. Create \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. Create \api\models\LogSearch.php. The MySQL model files in the api/models directory are business logic related (only related to the api) and inherit from the \common\logics\LogSearch logic layer
<?php
/**
* Created by PhpStorm.
* User: WangQiang
* Date: 2018/06/22
* Time: 13:43
*/
namespace api\models;
class LogSearch extends \common\logics\LogSearch
{
}
44. Create \api\modules\v1\models\LogSearch.php, inheriting from \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. Edit \api\rests\log\IndexAction.php. Stop using the yii\base\DynamicModel instance as $searchModel. Set up the data filter to enable filter processing and generate the SQL statement, as shown in Figure 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: Returns the details of log 1. Edit \api\rests\log\Action.php to adjust the namespace, inheritance relationship, response structure, etc.
<?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. Edit \api\rests\log\ViewAction.php to adjust the namespace, inheritance relationship, response structure, etc. ContentNegotiator supports response content format processing and language processing. It determines the response content format and language by checking the GET parameters and the Accept HTTP header. Configure ContentNegotiator to support US English and Simplified Chinese. Configure the response component to pass encoding options to yii\helpers\Json::encode(): JSON_FORCE_OBJECT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE. JSON_FORCE_OBJECT: Makes a non-associative array output an object rather than an array. This is particularly useful when the array is empty but an object is expected by the receiver. It avoids manually handling the conversion of empty arrays.
<?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. In Postman, GET http://api.github-shuijingwan-yii2-app-advanced.localhost/v1/logs/3, as shown in Figure 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. Define the request log behavior. When the [[yii\base\Application::EVENT_BEFORE_REQUEST|EVENT_BEFORE_REQUEST]] event is triggered, write the log to the database. Edit \api\behaviors\RequestLogBehavior.php, replacing null with ” to keep the field format consistent.
50. Configuration file for the API application, \api\config\main.php. Set the log target to DbTarget and set the whitelist category to api\behaviors\RequestLogBehavior::afterRequest. Edit the code to avoid the situation where message[‘userId’] cannot obtain a value. Note: This will cause 404 and other requests to not be written to the log
'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. Define the request log behavior. When the [[yii\base\Application::EVENT_AFTER_REQUEST|EVENT_AFTER_REQUEST]] event is triggered, write the log to the database. Edit \api\behaviors\RequestLogBehavior.php, adjust the fields to lowercase, and synchronously update the corresponding usages of these fields in other files
<?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. In Postman, GET http://api.github-shuijingwan-yii2-app-advanced.localhost/v1/logs?page=77&per-page=3 , 200 response
{
"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
}
}
}

