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

1、设置数据库的默认排序规则为:utf8mb4_unicode_ci,如图1

图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

图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

图3

.\yii migrate

6、日志的数据库模式可以通过应用迁移来初始化,执行如下命令,如图4

图4

.\yii migrate --migrationPath=@yii/log/migrations/

7、浏览数据库表,user、log表的排序规则为:utf8_unicode_ci,如图5

图5

8、新建一个数据库迁移文件,调整排序规则为:utf8mb4_unicode_ci,执行命令,如图6

图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

图7

.\yii migrate --migrationPath=@yii/log/migrations/
.\yii migrate

16、浏览数据库表,user、log表的排序规则为:utf8mb4_unicode_ci,且表列的排序规则也为:utf8mb4_unicode_ci,如图8

图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

图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

图10


23、实现日志功能的相应接口,打开网址:http://www.github-shuijingwan-yii2-app-advanced.localhost/gii/model ,选项,命名空间为common\models,此时需支持国际化,生成 \common\models\Log.php,如图11

图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

图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

图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

图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

图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

图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
        }
    }
}
永夜