没有不值得去解决的问题,也没有不值得去学习的技术!

基于 yiisoft/yii2-app-advanced,在 GitHub 上新建仓库 yii2-app-advanced,新建接口应用(实现 RESTful 风格的 Web Service 服务的 API),实现模型分层:数据层、逻辑层,明确公共目录、应用、模块的继承、引用关系

Yii2 Advanced 实战系列:RESTful API + RPC 服务 + 国际化 + 日志与测试

新建 api 目录、配置和环境、测试、Vagrant 等的支持。

(1) 基于 yiisoft/yii2-app-advanced,在 GitHub 上新建仓库 yii2-app-advanced,新建接口应用(实现 RESTful 风格的 Web Service 服务的 API),新建api目录、配置和环境、测试、Vagrant等的支持

(2) 基于 yiisoft/yii2-app-advanced,在 GitHub 上新建仓库 yii2-app-advanced,新建接口应用(实现 RESTful 风格的 Web Service 服务的 API),实现模型分层:数据层、逻辑层,明确公共目录、应用、模块的继承、引用关系

在 Postman 中,GET http://api.github-shuijingwan-yii2-app-advanced.localhost/v1/users ,200响应,message为简体中文

(3) 基于 yiisoft/yii2-app-advanced,在 GitHub 上新建仓库 yii2-app-advanced,新建接口应用(实现 RESTful 风格的 Web Service 服务的 API),实现 RESTful Web 服务,支持国际化(动态地设置目标语言,默认为简体中文)

运行测试,获取详细的输出,可看到一步一步的行为报告,符合预期

(4) 基于 yiisoft/yii2-app-advanced,在 GitHub 上新建仓库 yii2-app-advanced,新建接口应用(实现 RESTful 风格的 Web Service 服务的 API),在 api 的 tests 目录中准备用户相关操作的一些自动化测试的样例(API 测试),确保应用程序在改变或增加新的功能时不会影响现有的功能

在 Postman 中,GET http://api.github-shuijingwan-yii2-app-advanced.localhost/v1/logs?filter[level]=4&filter[category][like]=api\behaviors\RequestLogBehavior&filter[prefix][like]=app-api&filter[log_time][gte]=1528090828&filter[log_time][lte]=1529564924.6648 ,200响应,空数组已经被自动转换为空对象,以保证字段格式的统一,GET http://api.github-shuijingwan-yii2-app-advanced.localhost/v1/logs/3

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

在开发环境中,执行数据库迁移命令失败:1071 Specified key was too long; max key length is 767 bytes

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

浏览 user 表数据,status、updated_at 已经更新

(7) 基于 yiisoft/yii2-app-advanced,在 GitHub 上新建仓库 yii2-app-advanced,新建接口应用(实现 RESTful 风格的 Web Service 服务的 API),实现 ActiveRecord 的软删除,生成 ActiveQuery,自定义查询类

执行数据库迁移命令,查看数据库表 user 结构,符合预期

(8) 基于 yiisoft/yii2-app-advanced,在 GitHub 上新建仓库 yii2-app-advanced,新建接口应用(实现 RESTful 风格的 Web Service 服务的 API),实现 ActiveRecord 的软删除,生成 ActiveQuery,自定义查询类(二)

在 Postman 中 POST 请求,http://api.github-shuijingwan-yii2-app-advanced.localhost/v1/users ,输入格式:application/json

(9) 基于 yiisoft/yii2-app-advanced,在 GitHub 上新建仓库 yii2-app-advanced,新建接口应用(实现 RESTful 风格的 Web Service 服务的 API),application/x-www-form-urlencoded 和 multipart/form-data 输入格式,默认支持,新增 application/json 输入格式的支持

打开 Windows PowerShell,执行 init 命令并选择 dev 作为环境,api应用所需环境配置文件自动生成

(10) 基于 yiisoft/yii2-app-advanced,在 GitHub 上新建仓库 yii2-app-advanced,新建远程过程调用应用(实现基于 Hprose 2.0 for PHP 的 RPC 服务端),新建 rpc 目录、配置和环境、测试、Vagrant等的支持

出现以上原因是因为 Windows 下 nginx php 环境,不支持并发的原因,当同时访问多个域名,并且同时指向你本地服务的时候,就不支持并发了。nginx.conf 里面 对不同 server 修改 fastcgi_pass 的端口号,启动多个 php-cgi。

(11) 基于 yiisoft/yii2-app-advanced,在 GitHub 上新建仓库 yii2-app-advanced,新建远程过程调用应用(实现基于 Hprose 2.0 for PHP 的 RPC 服务端),在 rpc 目录中实现 页面 的相应 RPC 服务,且在 api 目录中的 API 实现 RPC 客户端

执行数据库迁移,查看表结构,基于 Gii 重新生成模型文件

(12) 基于 yiisoft/yii2-app-advanced,在 GitHub 上新建仓库 yii2-app-advanced,UUID 的实现

由于日志消息是存储于 MySQL 的 log 表中,在生产环境中,累积了大量的数据,log 表的数据长度已经达到 数百 GB ,因此,决定基于控制台命令行实现日志消息的定时删除,开发环境的 log 表的数据长度为 14.64 GB

(13) 基于 yiisoft/yii2-app-advanced,在 GitHub 上新建仓库 yii2-app-advanced,日志消息的自动定时删除

1、定义与规范:
定义:
(1)数据层:models 用于定义数据相关的自动验证和自动完成和数据存取接口;
(2)逻辑层:logics 用于定义数据相关的业务逻辑;

规范:
(1)/common/models 目录中的模型类文件仅允许Gii工具所生成,为公共的模型数据层;
(2)/common/logics 目录中的模型类文件为业务逻辑相关,继承至 /common/models 数据层,为公共的模型逻辑层;
(3)/common 目录中需要引用模型类文件,仅引用 /common/logics 中的模型类文件,例:


public $modelClass = 'common\logics\User';


(4)/api/models、/backend/models、/frontend/models 目录中的模型类文件为业务逻辑相关(仅与各应用相关),继承至 /common/logics 公共逻辑层;
(5)接口、前端、后端应用目录中需要引用模型类文件,仅引用各自目录下的对应模型类文件,例:


use api\models\User;


(6)/api/modules/v1/models 目录中的模型类文件为业务逻辑相关(仅与模块相关),继承至 /api/models 应用逻辑层;
(7)模块目录中需要引用模型类文件,仅引用各自模块下的对应模型类文件,例:


public $modelClass = 'api\modules\v1\models\User';


2、在common目录中新建logics目录,用于MySQL模型的逻辑层所在目录,如图1

在common目录中新建logics目录,用于MySQL模型的逻辑层所在目录
图1

3、复制 \common\models\LoginForm.php、\common\models\User.php 至 \common\logics,如图2

复制 \common\models\LoginForm.php、\common\models\User.php 至 \common\logics
图2

4、在common/models目录中的MySQL模型文件仅为Gii工具所生成,删除 \common\models\LoginForm.php,如图3

在common/models目录中的MySQL模型文件仅为Gii工具所生成,删除 \common\models\LoginForm.php
图3

5、配置路由,使用美观的 URL ,编辑 \frontend\config\main.php


        'urlManager' => [
            'enablePrettyUrl' => true,
            'showScriptName' => false,
            'rules' => [
            ],
        ],


6、国际化的支持,新建目录 \common\messages\en-US\model、\common\messages\zh-CN\model,分别用于模型的数据层的美国英语、中文简体;新建 \common\messages\{language}\app.php、\common\messages\{language}\error.php、\common\messages\{language}\success.php,分别用于应用全局消息、应用错误消息、应用成功消息,如图4

国际化的支持,新建目录 \common\messages\en-US\model、\common\messages\zh-CN\model,分别用于模型的数据层的美国英语、中文简体;新建 \common\messages\{language}\app.php、\common\messages\{language}\error.php、\common\messages\{language}\success.php,分别用于应用全局消息、应用错误消息、应用成功消息
图4

app.php

<?php
return [
];

error.php

<?php
return [
    20000 => 'error',
];

success.php

<?php
return [
    10000 => 'success',
];

7、配置应用程序语言,源语言为美国英语,目标语言为简体中文,当源语言和目标语言相同时,是否强制进行消息翻译,默认为假,设置为真,编辑 \common\config\main.php


    'sourceLanguage' => 'en-US',
    'language' => 'zh-CN',
    'components' => [
        'i18n' => [
            'translations' => [
                '*' => [
                    'class' => 'yii\i18n\PhpMessageSource',
                    'forceTranslation' => true,
                    'basePath'=>'@common/messages',
                    'fileMap' => [
                        'app' => 'app.php',
                        'error' => 'error.php',
                        'success' => 'success.php',
                    ],
                ],
            ],
        ],
        'cache' => [
            'class' => 'yii\caching\FileCache',
        ],
    ],


8、打开网址:http://www.github-shuijingwan-yii2-app-advanced.localhost/gii/model ,选项,命名空间为common\models,此时需支持国际化,覆盖\common\models\User.php,如图5

打开网址:http://www.github-shuijingwan-yii2-app-advanced.localhost/gii/model ,选项,命名空间为common\models,此时需支持国际化,覆盖\common\models\User.php
图5
<?php
 
namespace common\models;
 
use Yii;
 
/**
 * This is the model class for table "{{%user}}".
 *
 * @property int $id
 * @property string $username
 * @property string $auth_key
 * @property string $password_hash
 * @property string $password_reset_token
 * @property string $email
 * @property int $status
 * @property int $created_at
 * @property int $updated_at
 */
class User extends \yii\db\ActiveRecord
{
    /**
     * @inheritdoc
     */
    public static function tableName()
    {
        return '{{%user}}';
    }
 
    /**
     * @inheritdoc
     */
    public function rules()
    {
        return [
            [['username', 'auth_key', 'password_hash', 'email', 'created_at', 'updated_at'], 'required'],
            [['status', 'created_at', 'updated_at'], 'integer'],
            [['username', 'password_hash', 'password_reset_token', 'email'], 'string', 'max' => 255],
            [['auth_key'], 'string', 'max' => 32],
            [['username'], 'unique'],
            [['email'], 'unique'],
            [['password_reset_token'], 'unique'],
        ];
    }
 
    /**
     * @inheritdoc
     */
    public function attributeLabels()
    {
        return [
            'id' => Yii::t('model/user', 'ID'),
            'username' => Yii::t('model/user', 'Username'),
            'auth_key' => Yii::t('model/user', 'Auth Key'),
            'password_hash' => Yii::t('model/user', 'Password Hash'),
            'password_reset_token' => Yii::t('model/user', 'Password Reset Token'),
            'email' => Yii::t('model/user', 'Email'),
            'status' => Yii::t('model/user', 'Status'),
            'created_at' => Yii::t('model/user', 'Created At'),
            'updated_at' => Yii::t('model/user', 'Updated At'),
        ];
    }
}
 
 

9、在common/logics目录中的MySQL模型文件为业务逻辑相关,继承至 \common\models 数据层,打开网址:http://www.github-shuijingwan-yii2-app-advanced.localhost/gii/model ,选项,如图6

在common/logics目录中的MySQL模型文件为业务逻辑相关,继承至 \common\models 数据层,打开网址:http://www.github-shuijingwan-yii2-app-advanced.localhost/gii/model ,选项
图6

10、基于 diff ,编辑 \common\logics\User.php,如图7

11、在common/logics目录中的MySQL模型文件为业务逻辑相关,继承至 \common\models\User 数据层

<?php
 
namespace common\logics;
 
use Yii;
use yii\base\NotSupportedException;
use yii\behaviors\TimestampBehavior;
use yii\web\IdentityInterface;
 
/**
 * This is the model class for table "{{%user}}".
 *
 * @property int $id
 * @property string $username
 * @property string $auth_key
 * @property string $password_hash
 * @property string $password_reset_token
 * @property string $email
 * @property int $status
 * @property int $created_at
 * @property int $updated_at
 * @property string $password write-only password
 */
class User extends \common\models\User implements IdentityInterface
{
    const STATUS_DELETED = 0;
    const STATUS_ACTIVE = 10;
 
    /**
     * @inheritdoc
     */
    public function behaviors()
    {
        return [
            TimestampBehavior::className(),
        ];
    }
 
    /**
     * @inheritdoc
     */
    public function rules()
    {
        return [
            [['username', 'auth_key', 'password_hash', 'email'], 'required'],
            [['status', 'created_at', 'updated_at'], 'integer'],
            [['username', 'password_hash', 'password_reset_token', 'email'], 'string', 'max' => 255],
            [['auth_key'], 'string', 'max' => 32],
            [['username'], 'unique'],
            [['email'], 'unique'],
            [['password_reset_token'], 'unique'],
            ['status', 'default', 'value' => self::STATUS_ACTIVE],
            ['status', 'in', 'range' => [self::STATUS_ACTIVE, self::STATUS_DELETED]],
        ];
    }
 
    /**
     * @inheritdoc
     */
    public static function findIdentity($id)
    {
        return static::findOne(['id' => $id, 'status' => self::STATUS_ACTIVE]);
    }
 
    /**
     * @inheritdoc
     */
    public static function findIdentityByAccessToken($token, $type = null)
    {
        throw new NotSupportedException('"findIdentityByAccessToken" is not implemented.');
    }
 
    /**
     * Finds user by username
     *
     * @param string $username
     * @return static|null
     */
    public static function findByUsername($username)
    {
        return static::findOne(['username' => $username, 'status' => self::STATUS_ACTIVE]);
    }
 
    /**
     * Finds user by password reset token
     *
     * @param string $token password reset token
     * @return static|null
     */
    public static function findByPasswordResetToken($token)
    {
        if (!static::isPasswordResetTokenValid($token)) {
            return null;
        }
 
        return static::findOne([
            'password_reset_token' => $token,
            'status' => self::STATUS_ACTIVE,
        ]);
    }
 
    /**
     * Finds out if password reset token is valid
     *
     * @param string $token password reset token
     * @return bool
     */
    public static function isPasswordResetTokenValid($token)
    {
        if (empty($token)) {
            return false;
        }
 
        $timestamp = (int) substr($token, strrpos($token, '_') + 1);
        $expire = Yii::$app->params['user.passwordResetTokenExpire'];
        return $timestamp + $expire >= time();
    }
 
    /**
     * @inheritdoc
     */
    public function getId()
    {
        return $this->getPrimaryKey();
    }
 
    /**
     * @inheritdoc
     */
    public function getAuthKey()
    {
        return $this->auth_key;
    }
 
    /**
     * @inheritdoc
     */
    public function validateAuthKey($authKey)
    {
        return $this->getAuthKey() === $authKey;
    }
 
    /**
     * Validates password
     *
     * @param string $password password to validate
     * @return bool if password provided is valid for current user
     */
    public function validatePassword($password)
    {
        return Yii::$app->security->validatePassword($password, $this->password_hash);
    }
 
    /**
     * Generates password hash from password and sets it to the model
     *
     * @param string $password
     */
    public function setPassword($password)
    {
        $this->password_hash = Yii::$app->security->generatePasswordHash($password);
    }
 
    /**
     * Generates "remember me" authentication key
     */
    public function generateAuthKey()
    {
        $this->auth_key = Yii::$app->security->generateRandomString();
    }
 
    /**
     * Generates new password reset token
     */
    public function generatePasswordResetToken()
    {
        $this->password_reset_token = Yii::$app->security->generateRandomString() . '_' . time();
    }
 
    /**
     * Removes password reset token
     */
    public function removePasswordResetToken()
    {
        $this->password_reset_token = null;
    }
}
 

12、新建 \common\messages\en-US\model\user.php,支持目标语言为英语美国时的消息翻译

<?php
/**
 * Created by PhpStorm.
 * User: WangQiang
 * Date: 2018/04/04
 * Time: 10:34
 */
 
return [
    'ID' => 'ID',
    'Username' => 'Username',
    'Auth Key' => 'Auth Key',
    'Password Hash' => 'Password Hash',
    'Password Reset Token' => 'Password Reset Token',
    'Email' => 'Email',
    'Status' => 'Status',
    'Created At' => 'Created At',
    'Updated At' => 'Updated At',
];

13、新建 \common\messages\zh-CN\model\user.php,支持目标语言为简体中文时的消息翻译

<?php
/**
 * Created by PhpStorm.
 * User: WangQiang
 * Date: 2018/04/04
 * Time: 10:39
 */
 
return [
    'ID' => 'ID',
    'Username' => '用户名',
    'Auth Key' => '认证密钥',
    'Password Hash' => '密码哈希',
    'Password Reset Token' => '密码重置令牌',
    'Email' => '邮箱',
    'Status' => '状态',
    'Created At' => '创建时间',
    'Updated At' => '更新时间',
];

14、新建 \api\models\User.php,在api/models目录中的MySQL模型文件为业务逻辑相关(仅与api相关),继承至 \common\logics\User 逻辑层

<?php
/**
 * Created by PhpStorm.
 * User: WangQiang
 * Date: 2018/04/04
 * Time: 10:44
 */
 
namespace api\models;
 
class User extends \common\logics\User
{
 
}

15、复制\api\models\User.php 至 \frontend\models\User.php、\backend\models\User.php,调整为各自的命名空间
\frontend\models\User.php

<?php
/**
 * Created by PhpStorm.
 * User: WangQiang
 * Date: 2018/04/04
 * Time: 10:44
 */
 
namespace frontend\models;
 
class User extends \common\logics\User
{
 
}

\backend\models\User.php

<?php
/**
 * Created by PhpStorm.
 * User: WangQiang
 * Date: 2018/04/04
 * Time: 10:44
 */
 
namespace backend\models;
 
class User extends \common\logics\User
{
 
}

16、在 api 应用中搜索 use common\models\User;,替换为:use api\models\User;,在前台、后台应用中同样类似处理,如图8

在 api 应用中搜索 use common\models\User;,替换为:use api\models\User;,在前台、后台应用中同样类似处理
图8

17、在 api 应用中搜索 common\models\User,替换为:api\models\User,在前台、后台应用中同样类似处理,如图9

在 api 应用中搜索 common\models\User,替换为:api\models\User,在前台、后台应用中同样类似处理
图9

18、编辑 \common\logics\LoginForm.php,调整命名空间

<?php
namespace common\logics;
 
use Yii;
use yii\base\Model;
 
/**
 * Login form
 */
class LoginForm extends Model
{
    public $username;
    public $password;
    public $rememberMe = true;
 
    private $_user;
 
 
    /**
     * {@inheritdoc}
     */
    public function rules()
    {
        return [
            // username and password are both required
            [['username', 'password'], 'required'],
            // rememberMe must be a boolean value
            ['rememberMe', 'boolean'],
            // password is validated by validatePassword()
            ['password', 'validatePassword'],
        ];
    }
 
    /**
     * Validates the password.
     * This method serves as the inline validation for password.
     *
     * @param string $attribute the attribute currently being validated
     * @param array $params the additional name-value pairs given in the rule
     */
    public function validatePassword($attribute, $params)
    {
        if (!$this->hasErrors()) {
            $user = $this->getUser();
            if (!$user || !$user->validatePassword($this->password)) {
                $this->addError($attribute, 'Incorrect username or password.');
            }
        }
    }
 
    /**
     * Logs in a user using the provided username and password.
     *
     * @return bool whether the user is logged in successfully
     */
    public function login()
    {
        if ($this->validate()) {
            return Yii::$app->user->login($this->getUser(), $this->rememberMe ? 3600 * 24 * 30 : 0);
        }
         
        return false;
    }
 
    /**
     * Finds user by [[username]]
     *
     * @return User|null
     */
    protected function getUser()
    {
        if ($this->_user === null) {
            $this->_user = User::findByUsername($this->username);
        }
 
        return $this->_user;
    }
}
 
 

19、新建 \api\models\LoginForm.php,在api/models目录中的MySQL模型文件为业务逻辑相关(仅与api相关),继承至 \common\logics\LoginForm 逻辑层

<?php
/**
 * Created by PhpStorm.
 * User: WangQiang
 * Date: 2018/04/04
 * Time: 11:10
 */
 
namespace api\models;
 
 
class LoginForm extends \common\logics\LoginForm
{
 
}

20、复制\api\models\LoginForm.php 至 \frontend\models\LoginForm.php、\backend\models\LoginForm.php,调整为各自的命名空间
\frontend\models\LoginForm.php

<?php
/**
 * Created by PhpStorm.
 * User: WangQiang
 * Date: 2018/04/04
 * Time: 11:10
 */
 
namespace frontend\models;
 
 
class LoginForm extends \common\logics\LoginForm
{
 
}

\backend\models\LoginForm.php

<?php
/**
 * Created by PhpStorm.
 * User: WangQiang
 * Date: 2018/04/04
 * Time: 11:10
 */
 
namespace backend\models;
 
 
class LoginForm extends \common\logics\LoginForm
{
 
}

21、在 api 应用中搜索 use common\models\LoginForm;,替换为:use api\models\LoginForm;,在前台、后台应用中同样类似处理,如图10

在 api 应用中搜索 use common\models\LoginForm;,替换为:use api\models\LoginForm;,在前台、后台应用中同样类似处理
图10

22、在 api 应用中搜索 common\models\LoginForm,替换为:api\models\LoginForm,在前台、后台应用中同样类似处理,如图11

在 api 应用中搜索 common\models\LoginForm,替换为:api\models\LoginForm,在前台、后台应用中同样类似处理
图11

23、在 common 目录中搜索 common\models\User,替换为:common\logics\User,除了 \common\logics\User.php 例外,如图12

在 common 目录中搜索 common\models\User,替换为:common\logics\User,除了 \common\logics\User.php 例外
图12

24、在 common 目录中搜索 common\models\LoginForm,替换为:common\logics\LoginForm,如图13

在 common 目录中搜索 common\models\LoginForm,替换为:common\logics\LoginForm
图13

25、测试注册功能,打开网址:http://www.github-shuijingwan-yii2-app-advanced.localhost/site/signup ,符合预期,如图14

测试注册功能,打开网址:http://www.github-shuijingwan-yii2-app-advanced.localhost/site/signup ,符合预期
图14

26、查看数据库,如图15

查看数据库
图15

 

基于 yiisoft/yii2-app-advanced,在 GitHub 上新建仓库 yii2-app-advanced,新建接口应用(实现 RESTful 风格的 Web Service 服务的 API),新建api目录、配置和环境、测试、Vagrant等的支持 基于 yiisoft/yii2-app-advanced,在 GitHub 上新建仓库 yii2-app-advanced,新建接口应用(实现 RESTful 风格的 Web Service 服务的 API),实现 RESTful Web 服务,支持国际化(动态地设置目标语言,默认为简体中文)

PHP / Yii2 老项目维护与长期技术支持

如果你的 Yii2 项目已经上线,但遇到原开发离职、Bug 长期无人修复、接口不稳定、性能下降、代码难以接手等问题,可以联系我做一次远程技术排查。

适合以下情况:
✅ Yii2 老项目无人维护
✅ 后台管理系统 Bug 修复
✅ RESTful API 接口排查
✅ 小功能迭代与历史代码接手
✅ MySQL / Redis / Nginx 性能问题
✅ 长期远程兼职维护

可先从一次小问题开始:
✅ 线上报错排查
✅ 接口异常分析
✅ 慢查询与性能瓶颈定位
✅ 代码结构初步评估
✅ 部署环境与日志检查

如需咨询,请联系我,并注明:Yii2 维护咨询

联系方式:
Telegram:@shuijingwan
微信:13980074657
邮箱:shuijingwanwq@gmail.com

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理