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

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

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

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、RESTful Web 服务,建议基于一个单独的接口应用来实现,此时基于 api 应用来实现

2、新建目录:\api\rests,此目录将做为 RESTful Web 服务的操作方法类目录

3、新建控制器类 \api\controllers\UserController.php ,控制器类扩展自 [[yii\rest\ActiveController]]。数据序列化的实现,在响应主体内包含分页信息来简化客户端的开发工作, 如图1

新建控制器类 \api\controllers\UserController.php ,控制器类扩展自 [[yii\rest\ActiveController]]。数据序列化的实现,在响应主体内包含分页信息来简化客户端的开发工作
图1
<?php
/**
 * Created by PhpStorm.
 * User: WangQiang
 * Date: 2018/04/04
 * Time: 15:35
 */
 
namespace api\controllers;
 
use yii\rest\ActiveController;
 
class UserController extends ActiveController
{
    public $serializer = [
        'class' => 'api\rests\user\Serializer',
        'collectionEnvelope' => 'items',
    ];
 
    /**
     * @inheritdoc
     */
    public function actions()
    {
        return [
            'index' => [
                'class' => 'api\rests\user\IndexAction',
                'modelClass' => $this->modelClass,
                'checkAccess' => [$this, 'checkAccess'],
            ],
            'view' => [
                'class' => 'api\rests\user\ViewAction',
                'modelClass' => $this->modelClass,
                'checkAccess' => [$this, 'checkAccess'],
            ],
            'create' => [
                'class' => 'api\rests\user\CreateAction',
                'modelClass' => $this->modelClass,
                'checkAccess' => [$this, 'checkAccess'],
                'scenario' => $this->createScenario,
            ],
            'update' => [
                'class' => 'api\rests\user\UpdateAction',
                'modelClass' => $this->modelClass,
                'checkAccess' => [$this, 'checkAccess'],
                'scenario' => $this->updateScenario,
            ],
            'delete' => [
                'class' => 'api\rests\user\DeleteAction',
                'modelClass' => $this->modelClass,
                'checkAccess' => [$this, 'checkAccess'],
            ],
            'options' => [
                'class' => 'api\rests\user\OptionsAction',
                'modelClass' => $this->modelClass,
                'checkAccess' => [$this, 'checkAccess'],
            ],
        ];
    }
}
 
 

注:如果仅支持较少的行为,可以选择下面的方案,例


    public function actions()
    {
        $actions = parent::actions();
        
        $actions['view']['class'] = 'api\rests\user\ViewAction';

        return $actions;
    }


4、当配置的内容十分复杂,通用做法是将其存储在一或多个 PHP 文件中, 这些文件被称为配置文件。一个配置文件返回的是 PHP 数组。版本化的实现,配置URL规则,修改有关在应用程序配置的urlManager组件的配置,支持 v1 模块,支持所有行为,新建:\api\config\urlManager.php,如图2

当配置的内容十分复杂,通用做法是将其存储在一或多个 PHP 文件中, 这些文件被称为配置文件。一个配置文件返回的是 PHP 数组。版本化的实现,配置URL规则,修改有关在应用程序配置的urlManager组件的配置,支持 v1 模块,支持所有行为,新建:\api\config\urlManager.php
图2
<?php
return [
    'class' => yii\web\UrlManager::class,
    'enablePrettyUrl' => true,
    'enableStrictParsing' => true,
    'showScriptName' => false,
    'rules' => [
        [
            'class' => 'yii\rest\UrlRule',
            'controller' => ['v1/user'],
        ],
    ],
];

5、将 \api\config\urlManager.php 包含在 \api\config\main.php 里,编辑 \api\config\main.php


        'urlManager' => require __DIR__ . '/urlManager.php',


6、把每个主要版本的 API 实现在一个单独的模块 ID 的主版本号,基于 Gii 生成模块 v1,打开网址:http://www.github-shuijingwan-yii2-app-advanced.localhost/gii/module ,删除目录:\api\modules\v1\views,如图3

把每个主要版本的 API 实现在一个单独的模块 ID 的主版本号,基于 Gii 生成模块 v1,打开网址:http://www.github-shuijingwan-yii2-app-advanced.localhost/gii/module ,删除目录:\api\modules\v1\views
图3

7、新建 \api\modules\v1\models\User.php,继承至 \api\models\User.php,如图4
注:\api\modules\v1\models\User(仅用于 v1 模块) > \api\models\User(仅用于 api 应用) > \common\logics\User.php(可用于 api、frontend 等多个应用) > \common\models\User.php(仅限于 Gii 生成) > \yii\db\ActiveRecord

新建 \api\modules\v1\models\User.php,继承至 \api\models\User.php
图4
<?php
/**
 * Created by PhpStorm.
 * User: WangQiang
 * Date: 2018/04/04
 * Time: 16:04
 */
 
namespace api\modules\v1\models;
 
 
class User extends \api\models\User
{
 
}
 

8、\api\modules\v1\controllers\DefaultController.php 重命名为 \api\modules\v1\controllers\UserController.php,通过指定 [[yii\rest\ActiveController::modelClass|modelClass]] 作为 api\modules\v1\models\User, 控制器就能知道使用哪个模型去获取和处理数据。编辑代码,如图5
注:\api\modules\v1\controllers\UserController.php(仅用于 v1 模块) > \api\controllers\UserController.php(仅用于 api 应用) > \yii\rest\ActiveController

\api\modules\v1\controllers\DefaultController.php 重命名为 \api\modules\v1\controllers\UserController.php,通过指定 [[yii\rest\ActiveController::modelClass|modelClass]] 作为 api\modules\v1\models\User, 控制器就能知道使用哪个模型去获取和处理数据
图5
<?php
 
namespace api\modules\v1\controllers;
 
/**
 * User controller for the `v1` module
 */
class UserController extends \api\controllers\UserController
{
    public $modelClass = 'api\modules\v1\models\User';
}
 

9、要在应用中使用模块,只需要将模块加入到应用主体配置的[[yii\base\Application::modules|modules]]属性的列表中, 如下代码的应用主体配置 使用 v1 模块,编辑 \api\config\main.php,如图6

要在应用中使用模块,只需要将模块加入到应用主体配置的[[yii\base\Application::modules|modules]]属性的列表中, 如下代码的应用主体配置 使用 v1 模块,编辑 \api\config\main.php
图6

    'modules' => [
        'v1' => [
            'class' => api\modules\v1\Module::class,
        ],
    ],


10、在 Postman 中,GET http://api.github-shuijingwan-yii2-app-advanced.localhost/v1 ,404响应,格式为HMTL,如图7

<!DOCTYPE html>
<html lang="zh-CN">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <meta name="csrf-param" content="_csrf-api">
        <meta name="csrf-token" content="00ssponpmJwoWBZLFTExhai8kSgeTCkNzKenlyRbznyiAHzyz6Pc9XICYxpmWV7KzePdQnYZG2umkezQXg38Tw==">
        <title>Not Found (#404)</title>
        <link href="/assets/73866dfd/css/bootstrap.css" rel="stylesheet">
        <link href="/css/site.css" rel="stylesheet">
    </head>
    <body>
    </body>
</html>

11、对于404响应格式为HTML的解决,编辑 \api\config\main.php,设置默认的响应格式为JSON


        'response' => [
            'format' => yii\web\Response::FORMAT_JSON,
        ],


12、在 Postman 中,GET http://api.github-shuijingwan-yii2-app-advanced.localhost/v1 ,404响应,格式为JSON,如图8

在 Postman 中,GET http://api.github-shuijingwan-yii2-app-advanced.localhost/v1 ,404响应,格式为JSON
图8

{
    "name": "Not Found",
    "message": "用户未找到。",
    "code": 0,
    "status": 404,
    "type": "yii\\web\\NotFoundHttpException"
}


13、RESTful APIs 通常是无状态的,因此,配置 user 应用组件,编辑 \api\config\main.php
注:配置user 应用组件:
设置 [[yii\web\User::enableSession|enableSession]] 属性为 false.
设置 [[yii\web\User::loginUrl|loginUrl]] 属性为null 显示一个HTTP 403 错误而不是跳转到登录界面.


        'user' => [
            'identityClass' => 'api\models\User',
            'enableSession' => false,
            'loginUrl' => null,
            'enableAutoLogin' => false,
        ],


14、复制目录 \vendor\yiisoft\yii2\rest 下的 Action.php、IndexAction.php、OptionsAction.php、ViewAction.php、CreateAction.php、UpdateAction.php、DeleteAction.php、Serializer.php 至目录 \api\rests\user,如果为多个单词组合的目录,建议目录使用小写+下划线,参考网址:https://github.com/hfcorriez/fig-standards/blob/master/accepted/zh_CN/PSR-0.md

15、编辑 \api\rests\user\IndexAction.php,调整命名空间、继承关系、查询条件等

<?php
/**
 * @link http://www.yiiframework.com/
 * @copyright Copyright (c) 2008 Yii Software LLC
 * @license http://www.yiiframework.com/license/
 */
 
namespace api\rests\user;
 
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
{
    const STATUS_DELETED = 0; //状态:已删除
    const STATUS_ACTIVE = 10; //状态:活跃
 
    /**
     * 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) {
                    return $this->dataFilter;
                }
            }
        }
 
        if ($this->prepareDataProvider !== null) {
            return call_user_func($this->prepareDataProvider, $this, $filter);
        }
 
        /* @var $modelClass \yii\db\BaseActiveRecord */
        $modelClass = $this->modelClass;
 
        $query = $modelClass::find()->where(['status' => self::STATUS_ACTIVE]);
        if (!empty($filter)) {
            $query->andWhere($filter);
        }
 
        return Yii::createObject([
            'class' => ActiveDataProvider::className(),
            'query' => $query,
            'pagination' => [
                'params' => $requestParams,
            ],
            'sort' => [
                'params' => $requestParams,
            ],
        ]);
    }
}
 

16、编辑 \api\rests\user\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\user;
 
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' => 20001, 'message' => Yii::t('error', '20001')];
        }
 
        if ($pagination !== false) {
            return ['code' => 10000, 'message' => Yii::t('success', '10001'), 'data' => array_merge($result, $this->serializePagination($pagination))];
        }
 
        return ['code' => 10000, 'message' => Yii::t('success', '10001'), 'data' => $result];
    }
}
 

17、编辑 \api\config\main.php,配置接口应用的 i18n 应用组件


        'i18n' => [
            'translations' => [
                'model/*'=> [
                    'class' => 'yii\i18n\PhpMessageSource',
                    'forceTranslation' => true,
                    'basePath'=>'@common/messages',
                    'fileMap'=>[
                    ],
                ],
                '*'=> [
                    'class' => 'yii\i18n\PhpMessageSource',
                    'forceTranslation' => true,
                    'basePath'=>'@api/messages',
                    'fileMap'=>[
                    ],
                ],
            ],
        ],


18、新建语言包文件:\api\messages\zh-CN\success.php(简体中文、响应成功)

<?php
return [
    10000 => 'success',
    10001 => '获取用户列表成功',
    10002 => '获取用户详情成功',
    10003 => '创建用户成功',
    10004 => '更新用户成功',
    10005 => '删除用户成功',
];

19、新建语言包文件:\api\messages\zh-CN\error.php(简体中文、响应失败)

<?php
return [
    20000 => 'error',
    20001 => '用户列表为空',
    20002 => '用户ID:{id},不存在',
    20003 => '用户ID:{id},的状态为已删除',
    20004 => '数据验证失败:{firstErrors}',
];

20、新建语言包文件:\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',
];
 

21、新建语言包文件:\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}, the status is not active',
    20004 => 'Data validation failed: {firstErrors}',
];
 

22、ContentNegotiator支持响应内容格式处理和语言处理。 通过检查 GET 参数和 Accept HTTP头部来决定响应内容格式和语言。配置ContentNegotiator支持英语美国和简体中文。编辑 \common\config\main.php

注:如果请求中没有检测到语言, 使用 [[languages]] 第一个配置项。


    'bootstrap' => ['contentNegotiator'],
    'components' => [

        'contentNegotiator' => [
            'class' => 'yii\filters\ContentNegotiator',
            'languages' => [
                'zh-CN',
                'en-US',
            ],
        ],

    ],


删除


    'sourceLanguage' => 'en-US',
    'language' => 'zh-CN',


23、在 Postman 中,GET http://api.github-shuijingwan-yii2-app-advanced.localhost/v1/users ,200响应,message默认为简体中文,如图9
注:
Accept application/json; version=0.0

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

{
    "code": 10000,
    "message": "获取用户列表成功",
    "data": {
        "items": [
            {
                "id": 1,
                "username": "111111",
                "auth_key": "P66wcJVGQL2toEpYr-Kc3z_rk7jJHMip",
                "password_hash": "$2y$13$qWV4xv1YLBtyn5cVx106VO6kTUOUP/7kX3oaC6CVpczHobkUrc6AG",
                "password_reset_token": null,
                "email": "111111@163.com",
                "status": 10,
                "created_at": 1522821105,
                "updated_at": 1522821105
            },
            {
                "id": 2,
                "username": "222222",
                "auth_key": "MgGfnpw6mYtkzctYkTOscyJAY_xxu739",
                "password_hash": "$2y$13$Xe0I4yjXL1FLvue8Q1YEU.nX8wH9HZzBOHSdbx2FL49me15CqoSa2",
                "password_reset_token": null,
                "email": "222222@163.com",
                "status": 10,
                "created_at": 1522821319,
                "updated_at": 1522821319
            },
            {
                "id": 3,
                "username": "333333",
                "auth_key": "WPeS81Lh4EUg0dp8FQ7ely4MF1ucF-ph",
                "password_hash": "$2y$13$.2G63zy8q2wTH/dWf.gLVe7c8kXKw3YCT5gquVhNhpTS2Albro1Ua",
                "password_reset_token": null,
                "email": "333333@163.com",
                "status": 10,
                "created_at": 1522823787,
                "updated_at": 1522823787
            },
            {
                "id": 4,
                "username": "444444",
                "auth_key": "scn4skhTfkg9YvRH1q9JZHkkddSR9H6a",
                "password_hash": "$2y$13$0RNx33MZIwC.FlSzMB0fuOCJAKmb1TlZzWCAujaCY/ixiM11myQ0a",
                "password_reset_token": null,
                "email": "444444@163.com",
                "status": 10,
                "created_at": 1522823915,
                "updated_at": 1522823915
            }
        ],
        "_links": {
            "self": {
                "href": "http://api.github-shuijingwan-yii2-app-advanced.localhost/v1/users?user=1"
            }
        },
        "_meta": {
            "totalCount": 4,
            "userCount": 1,
            "currentuser": 1,
            "peruser": 20
        }
    }
}


24、在 Postman 中,GET http://api.github-shuijingwan-yii2-app-advanced.localhost/v1/users ,200响应,message为简体中文,如图10
注:
Accept application/json; version=0.0
Accept-Language zh-CN

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

{
    "code": 10000,
    "message": "获取用户列表成功",
    "data": {
        "items": [
            {
                "id": 1,
                "username": "111111",
                "auth_key": "P66wcJVGQL2toEpYr-Kc3z_rk7jJHMip",
                "password_hash": "$2y$13$qWV4xv1YLBtyn5cVx106VO6kTUOUP/7kX3oaC6CVpczHobkUrc6AG",
                "password_reset_token": null,
                "email": "111111@163.com",
                "status": 10,
                "created_at": 1522821105,
                "updated_at": 1522821105
            },
            {
                "id": 2,
                "username": "222222",
                "auth_key": "MgGfnpw6mYtkzctYkTOscyJAY_xxu739",
                "password_hash": "$2y$13$Xe0I4yjXL1FLvue8Q1YEU.nX8wH9HZzBOHSdbx2FL49me15CqoSa2",
                "password_reset_token": null,
                "email": "222222@163.com",
                "status": 10,
                "created_at": 1522821319,
                "updated_at": 1522821319
            },
            {
                "id": 3,
                "username": "333333",
                "auth_key": "WPeS81Lh4EUg0dp8FQ7ely4MF1ucF-ph",
                "password_hash": "$2y$13$.2G63zy8q2wTH/dWf.gLVe7c8kXKw3YCT5gquVhNhpTS2Albro1Ua",
                "password_reset_token": null,
                "email": "333333@163.com",
                "status": 10,
                "created_at": 1522823787,
                "updated_at": 1522823787
            },
            {
                "id": 4,
                "username": "444444",
                "auth_key": "scn4skhTfkg9YvRH1q9JZHkkddSR9H6a",
                "password_hash": "$2y$13$0RNx33MZIwC.FlSzMB0fuOCJAKmb1TlZzWCAujaCY/ixiM11myQ0a",
                "password_reset_token": null,
                "email": "444444@163.com",
                "status": 10,
                "created_at": 1522823915,
                "updated_at": 1522823915
            }
        ],
        "_links": {
            "self": {
                "href": "http://api.github-shuijingwan-yii2-app-advanced.localhost/v1/users?user=1"
            }
        },
        "_meta": {
            "totalCount": 4,
            "userCount": 1,
            "currentuser": 1,
            "peruser": 20
        }
    }
}


25、GET /users/1: 返回用户 1 的详细信息,编辑 \api\rests\user\Action.php,调整命名空间、继承关系、响应结构等

<?php
/**
 * @link http://www.yiiframework.com/
 * @copyright Copyright (c) 2008 Yii Software LLC
 * @license http://www.yiiframework.com/license/
 */
 
namespace api\rests\user;
 
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', '20002'), ['id' => $id])), 20002);
    }
}

26、编辑 \api\rests\user\ViewAction.php,调整命名空间、继承关系、响应结构等

<?php
/**
 * @link http://www.yiiframework.com/
 * @copyright Copyright (c) 2008 Yii Software LLC
 * @license http://www.yiiframework.com/license/
 */
 
namespace api\rests\user;
 
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
{
    const STATUS_DELETED = 0; //状态:已删除
    const STATUS_ACTIVE = 10; //状态:活跃
 
    /**
     * 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);
        }
 
        /* 判断状态,如果为已删除,则返回失败 */
        if ($model->status === self::STATUS_DELETED) {
            return ['code' => 20003, 'message' => Yii::t('error', Yii::t('error', Yii::t('error', '20003'), ['id' => $id]))];
        }
 
        return ['code' => 10000, 'message' => Yii::t('success', '10002'), 'data' => $model];
    }
}
 
 

27、在 Postman 中,GET http://api.github-shuijingwan-yii2-app-advanced.localhost/v1/users/1 ,200响应,其状态为活跃,如图11
注:
Accept application/json; version=0.0
Accept-Language zh-CN

在 Postman 中,GET http://api.github-shuijingwan-yii2-app-advanced.localhost/v1/users/1 ,200响应,其状态为活跃
图11

{
    "code": 10000,
    "message": "获取用户详情成功",
    "data": {
        "id": 1,
        "username": "111111",
        "auth_key": "P66wcJVGQL2toEpYr-Kc3z_rk7jJHMip",
        "password_hash": "$2y$13$qWV4xv1YLBtyn5cVx106VO6kTUOUP/7kX3oaC6CVpczHobkUrc6AG",
        "password_reset_token": null,
        "email": "111111@163.com",
        "status": 10,
        "created_at": 1522821105,
        "updated_at": 1522821105
    }
}


28、在 Postman 中,GET http://api.github-shuijingwan-yii2-app-advanced.localhost/v1/users/5 ,404响应,用户不存在,如图12
注:
Accept application/json; version=0.0
Accept-Language en-US

在 Postman 中,GET http://api.github-shuijingwan-yii2-app-advanced.localhost/v1/users/5 ,404响应,用户不存在
图12

{
    "name": "Not Found",
    "message": "User ID: 5, does not exist",
    "code": 20002,
    "status": 404,
    "type": "yii\\web\\NotFoundHttpException"
}


29、POST /users: 创建一个新用户,复制 \api\models\SignupForm.php 至 \api\models\Signup.php

<?php
namespace api\models;
 
use yii\base\Model;
 
/**
 * Signup
 */
class Signup extends Model
{
    public $username;
    public $email;
    public $password;
 
 
    /**
     * {@inheritdoc}
     */
    public function rules()
    {
        return [
            ['username', 'trim'],
            ['username', 'required'],
            ['username', 'unique', 'targetClass' => '\api\models\User'],
            ['username', 'string', 'min' => 2, 'max' => 255],
            ['email', 'trim'],
            ['email', 'required'],
            ['email', 'email'],
            ['email', 'string', 'max' => 255],
            ['email', 'unique', 'targetClass' => '\api\models\User'],
            ['password', 'required'],
            ['password', 'string', 'min' => 6],
        ];
    }
}
 

30、新建 \api\modules\v1\models\Signup.php,继承至 \api\models\Signup.php

<?php
/**
 * Created by PhpStorm.
 * User: WangQiang
 * Date: 2018/04/08
 * Time: 10:48
 */
 
namespace api\modules\v1\models;
 
 
class Signup extends \api\models\Signup
{
 
}

31、POST /users: 创建一个新用户,编辑 \api\rests\user\CreateAction.php

<?php
/**
 * @link http://www.yiiframework.com/
 * @copyright Copyright (c) 2008 Yii Software LLC
 * @license http://www.yiiframework.com/license/
 */
 
namespace api\rests\user;
 
use Yii;
use yii\base\Model;
use api\models\Signup;
use yii\helpers\Url;
use yii\web\ServerErrorHttpException;
 
/**
 * CreateAction implements the API endpoint for creating a new model from the given data.
 *
 * For more details and usage information on CreateAction, see the [guide article on rest controllers](guide:rest-controllers).
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since 1.0
 */
class CreateAction extends Action
{
    /**
     * @var string the scenario to be assigned to the new model before it is validated and saved.
     */
    public $scenario = Model::SCENARIO_DEFAULT;
    /**
     * @var string the name of the view action. This property is need to create the URL when the model is successfully created.
     */
    public $viewAction = 'view';
 
 
    /**
     * Creates a new model.
     * @return \yii\db\ActiveRecordInterface the model newly created
     * @throws ServerErrorHttpException if there is any error when creating the model
     */
    public function run()
    {
        if ($this->checkAccess) {
            call_user_func($this->checkAccess, $this->id);
        }
 
        $signup = new Signup();
        $signup->load(Yii::$app->getRequest()->getBodyParams(), '');
        if (!$signup->validate()) {
            if ($signup->hasErrors()) {
                $response = Yii::$app->getResponse();
                $response->setStatusCode(422, 'Data Validation Failed.');
                foreach ($signup->getFirstErrors() as $message) {
                    $firstErrors = $message;
                    break;
                }
                return ['code' => 20004, 'message' => Yii::t('error', Yii::t('error', Yii::t('error', '20004'), ['firstErrors' => $firstErrors]))];
            } elseif (!$signup->hasErrors()) {
                throw new ServerErrorHttpException('Failed to create the object for unknown reason.');
            }
        }
 
        /* @var $model \yii\db\ActiveRecord */
        $model = new $this->modelClass([
            'scenario' => $this->scenario,
        ]);
 
        $model->username = $signup->username;
        $model->email = $signup->email;
        $model->setPassword($signup->password);
        $model->generateAuthKey();
        if ($model->save()) {
            $response = Yii::$app->getResponse();
            $response->setStatusCode(201);
            $id = implode(',', array_values($model->getPrimaryKey(true)));
            $response->getHeaders()->set('Location', Url::toRoute([$this->viewAction, 'id' => $id], true));
        } elseif ($model->hasErrors()) {
            $response = Yii::$app->getResponse();
            $response->setStatusCode(422, 'Data Validation Failed.');
            foreach ($model->getFirstErrors() as $message) {
                $firstErrors = $message;
                break;
            }
            return ['code' => 20004, 'message' => Yii::t('error', Yii::t('error', Yii::t('error', '20004'), ['firstErrors' => $firstErrors]))];
        } elseif (!$model->hasErrors()) {
            throw new ServerErrorHttpException('Failed to create the object for unknown reason.');
        }
 
        return ['code' => 10000, 'message' => Yii::t('success', '10003'), 'data' => $model];
    }
}

32、在 Postman 中,POST http://api.github-shuijingwan-yii2-app-advanced.localhost/v1/users ,201响应,如图13
注:
Accept application/json; version=0.0
Accept-Language zh-CN
Accept-Encoding gzip, deflate, br
Content-Type application/x-www-form-urlencoded

在 Postman 中,POST http://api.github-shuijingwan-yii2-app-advanced.localhost/v1/users ,201响应
图13

{
    "code": 10000,
    "message": "创建用户成功",
    "data": {
        "username": "555555",
        "email": "555555@163.com",
        "password_hash": "$2y$13$JcBRxuLFtLc4FfACRGwKpeXRNfhuP8XRGR/0ORvpXc8ob3c3D2E/q",
        "auth_key": "0eAriaA02Qw9J3s8ngwLtGlW34tpZ98k",
        "status": 10,
        "created_at": 1523156052,
        "updated_at": 1523156052,
        "id": 5
    }
}


33、在 Postman 中,POST http://api.github-shuijingwan-yii2-app-advanced.localhost/v1/users ,参数保持原样,422响应,如图14
注:
Accept application/json; version=0.0
Accept-Language zh-CN
Accept-Encoding gzip, deflate, br
Content-Type application/x-www-form-urlencoded

在 Postman 中,POST http://api.github-shuijingwan-yii2-app-advanced.localhost/v1/users ,参数保持原样,422响应
图14

{
    "code": 20004,
    "message": "数据验证失败:Username的值\"555555\"已经被占用了。"
}


34、PUT /users/5: 更新一个用户,新建 \api\models\UserUpdate.php

<?php
namespace api\models;
 
use yii\base\Model;
 
/**
 * UserUpdate
 */
class UserUpdate extends Model
{
    public $id;
    public $email;
    public $password;
    public $status;
 
    const STATUS_DELETED = 0; //状态:已删除
    const STATUS_ACTIVE = 10; //状态:活跃
 
    /**
     * {@inheritdoc}
     */
    public function rules()
    {
        return [
            ['email', 'trim'],
            ['email', 'required'],
            ['email', 'email'],
            ['email', 'string', 'max' => 255],
            ['email', 'unique', 'targetClass' => '\api\models\User', 'filter' => ['!=', 'id', $this->id]],
            ['password', 'required'],
            ['password', 'string', 'min' => 6],
            [['status'], 'in', 'range' => [self::STATUS_DELETED, self::STATUS_ACTIVE]],
        ];
    }
}

35、新建 \api\modules\v1\models\UserUpdate.php,继承至 \api\models\UserUpdate.php

<?php
/**
 * Created by PhpStorm.
 * User: WangQiang
 * Date: 2018/04/08
 * Time: 11:19
 */
 
namespace api\modules\v1\models;
 
 
class UserUpdate extends \api\models\UserUpdate
{
 
}

36、PUT /users/4: 更新一个用户,编辑 \api\rests\user\UpdateAction.php

<?php
/**
 * @link http://www.yiiframework.com/
 * @copyright Copyright (c) 2008 Yii Software LLC
 * @license http://www.yiiframework.com/license/
 */
 
namespace api\rests\user;
 
use Yii;
use yii\base\Model;
use yii\db\ActiveRecord;
use api\models\UserUpdate;
use yii\web\ServerErrorHttpException;
 
/**
 * UpdateAction implements the API endpoint for updating a model.
 *
 * For more details and usage information on UpdateAction, see the [guide article on rest controllers](guide:rest-controllers).
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since 1.0
 */
class UpdateAction extends Action
{
    /**
     * @var string the scenario to be assigned to the model before it is validated and updated.
     */
    public $scenario = Model::SCENARIO_DEFAULT;
 
 
    /**
     * Updates an existing model.
     * @param string $id the primary key of the model.
     * @return \yii\db\ActiveRecordInterface the model being updated
     * @throws ServerErrorHttpException if there is any error when updating the model
     */
    public function run($id)
    {
        /* @var $model ActiveRecord */
        $model = $this->findModel($id);
 
        if ($this->checkAccess) {
            call_user_func($this->checkAccess, $this->id, $model);
        }
 
        $userUpdate = new UserUpdate();
        $userUpdate->id = $id;
        $userUpdate->load(Yii::$app->getRequest()->getBodyParams(), '');
        if (!$userUpdate->validate()) {
            if ($userUpdate->hasErrors()) {
                $response = Yii::$app->getResponse();
                $response->setStatusCode(422, 'Data Validation Failed.');
                foreach ($userUpdate->getFirstErrors() as $message) {
                    $firstErrors = $message;
                    break;
                }
                return ['code' => 20004, 'message' => Yii::t('error', Yii::t('error', Yii::t('error', '20004'), ['firstErrors' => $firstErrors]))];
            } elseif (!$userUpdate->hasErrors()) {
                throw new ServerErrorHttpException('Failed to create the object for unknown reason.');
            }
        }
 
        $model->scenario = $this->scenario;
        $model->email = $userUpdate->email;
        $model->status = $userUpdate->status;
        $model->setPassword($userUpdate->password);
        $model->generateAuthKey();
        if ($model->save() === false) {
            if ($model->hasErrors()) {
                $response = Yii::$app->getResponse();
                $response->setStatusCode(422, 'Data Validation Failed.');
                foreach ($model->getFirstErrors() as $message) {
                    $firstErrors = $message;
                    break;
                }
                return ['code' => 20004, 'message' => Yii::t('error', Yii::t('error', Yii::t('error', '20004'), ['firstErrors' => $firstErrors]))];
            } elseif (!$model->hasErrors()) {
                throw new ServerErrorHttpException('Failed to update the object for unknown reason.');
            }
        }
 
        return ['code' => 10000, 'message' => Yii::t('success', '10004'), 'data' => $model];
    }
}
 

37、在 Postman 中,PUT http://api.github-shuijingwan-yii2-app-advanced.localhost/v1/users/5 ,200响应,如图15

在 Postman 中,PUT http://api.github-shuijingwan-yii2-app-advanced.localhost/v1/users/5 ,200响应
图15

{
    "code": 10000,
    "message": "更新用户成功",
    "data": {
        "id": 5,
        "username": "555555",
        "auth_key": "nXryM-o_ky4TAdn758l47pQhLhsSnjSZ",
        "password_hash": "$2y$13$qB5Y3Zl7B8NyPDFMXc06ye8OAhybCpp7p6wToyhtLH2iDzhd06ts.",
        "password_reset_token": null,
        "email": "555555@qq.com",
        "status": "0",
        "created_at": 1523156052,
        "updated_at": 1523165956
    }
}


38、在 Postman 中,PUT http://api.github-shuijingwan-yii2-app-advanced.localhost/v1/users/4 ,422响应,如图16

注:
Accept application/json; version=0.0
Accept-Language en-US
Accept-Encoding gzip, deflate, br
Content-Type application/x-www-form-urlencoded

Body
email 的值已经被另一用户占用(如果值未发生变化,则可验证通过)

在 Postman 中,PUT http://api.github-shuijingwan-yii2-app-advanced.localhost/v1/users/4 ,422响应
图16

{
    "code": 20004,
    "message": "Data validation failed: Email \"444444@163.com\" has already been taken."
}


39、DELETE /users/5: 删除用户5,编辑 \api\rests\user\DeleteAction.php

<?php
/**
 * @link http://www.yiiframework.com/
 * @copyright Copyright (c) 2008 Yii Software LLC
 * @license http://www.yiiframework.com/license/
 */
 
namespace api\rests\user;
 
use Yii;
use yii\web\ServerErrorHttpException;
 
/**
 * DeleteAction implements the API endpoint for deleting a model.
 *
 * For more details and usage information on DeleteAction, see the [guide article on rest controllers](guide:rest-controllers).
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since 1.0
 */
class DeleteAction extends Action
{
    /**
     * Deletes a model.
     * @param mixed $id id of the model to be deleted.
     * @throws ServerErrorHttpException on failure.
     */
    public function run($id)
    {
        $model = $this->findModel($id);
 
        if ($this->checkAccess) {
            call_user_func($this->checkAccess, $this->id, $model);
        }
 
        if ($model->delete() === false) {
            throw new ServerErrorHttpException('Failed to delete the object for unknown reason.');
        }
 
        return ['code' => 10000, 'message' => Yii::t('success', '10005')];
    }
}
 
 

40、在 Postman 中,DELETE http://api.github-shuijingwan-yii2-app-advanced.localhost/v1/users/5 ,200响应,如图17
注:
Accept application/json; version=0.0
Accept-Language zh-CN
Accept-Encoding gzip, deflate, br

在 Postman 中,DELETE http://api.github-shuijingwan-yii2-app-advanced.localhost/v1/users/5 ,200响应
图17

{
    "code": 10000,
    "message": "删除用户成功"
}


41、编辑 \api\rests\user\OptionsAction.php

<?php
/**
 * @link http://www.yiiframework.com/
 * @copyright Copyright (c) 2008 Yii Software LLC
 * @license http://www.yiiframework.com/license/
 */
 
namespace api\rests\user;
 
use Yii;
 
/**
 * OptionsAction responds to the OPTIONS request by sending back an `Allow` header.
 *
 * For more details and usage information on OptionsAction, see the [guide article on rest controllers](guide:rest-controllers).
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @since 2.0
 */
class OptionsAction extends \yii\rest\Action
{
    /**
     * @var array the HTTP verbs that are supported by the collection URL
     */
    public $collectionOptions = ['GET', 'POST', 'HEAD', 'OPTIONS'];
    /**
     * @var array the HTTP verbs that are supported by the resource URL
     */
    public $resourceOptions = ['GET', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'];
 
 
    /**
     * Responds to the OPTIONS request.
     * @param string $id
     */
    public function run($id = null)
    {
        if (Yii::$app->getRequest()->getMethod() !== 'OPTIONS') {
            Yii::$app->getResponse()->setStatusCode(405);
        }
        $options = $id === null ? $this->collectionOptions : $this->resourceOptions;
        $headers = Yii::$app->getResponse()->getHeaders();
        $headers->set('Allow', implode(', ', $options));
        $headers->set('Access-Control-Allow-Methods', implode(', ', $options));
    }
}
 

42、OPTIONS /users: 显示关于末端 /users 支持的动词,在 Postman 中,OPTIONS http://api.github-shuijingwan-yii2-app-advanced.localhost/v1/users ,200响应,如图18

OPTIONS /users: 显示关于末端 /users 支持的动词,在 Postman 中,OPTIONS http://api.github-shuijingwan-yii2-app-advanced.localhost/v1/users ,200响应
图18

43、OPTIONS /users/4: 显示关于末端 /users/4 支持的动词,在 Postman 中,OPTIONS http://api.github-shuijingwan-yii2-app-advanced.localhost/v1/users/1 ,200响应,如图19

OPTIONS /users/4: 显示关于末端 /users/4 支持的动词,在 Postman 中,OPTIONS http://api.github-shuijingwan-yii2-app-advanced.localhost/v1/users/1 ,200响应
图19

44、由于[[yii\web\Response::format|response format]] 响应格式不为html, 因此,不会使用错误或异常视图来显示错误信息,可取消错误动作,编辑 \api\config\main.php

删除


        'errorHandler' => [
            'errorAction' => 'site/error',
        ],


45、总结:现在支持的行为:index、view、create、update、delete、options,皆是继承之后,再次覆写实现具体的需求。

 

基于 yiisoft/yii2-app-advanced,在 GitHub 上新建仓库 yii2-app-advanced,新建接口应用(实现 RESTful 风格的 Web Service 服务的 API),实现模型分层:数据层、逻辑层,明确公共目录、应用、模块的继承、引用关系 基于 yiisoft/yii2-app-advanced,在 GitHub 上新建仓库 yii2-app-advanced,新建接口应用(实现 RESTful 风格的 Web Service 服务的 API),在 api 的 tests 目录中准备用户相关操作的一些自动化测试的样例(API 测试),确保应用程序在改变或增加新的功能时不会影响现有的功能

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

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

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

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

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

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

评论

发表回复

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

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