1、前文:http://www.shuijingwanwq.com/2018/01/18/2328/

2、参考第11步骤,db 组件移至开发环境,以方便于 Gii 的使用,\common\config\base.php,如图1

图1

3、配置为生产环境,以便于测试数据库迁移的多租户实现,不依赖于 db 组件,如图2

图2

4、运行命令:.\yii app/setup,报错:Exception ‘yii\base\InvalidConfigException’ with message ‘Failed to instantiate component or class “db”.’,如图3

图3

5、基于 Trait 实现,将 getTenantDb() 方法放入 Trait 中,新建 \common\traits\TenantDb.php,通过 [[yii\di\ServiceLocator::setComponents()]] 方法注册数据库连接组件、RBAC组件

<?php
/**
 * Created by PhpStorm.
 * User: WangQiang
 * Date: 2018/01/26
 * Time: 16:25
 */
namespace common\traits;

use Yii;
use common\logics\http\tenant\Env;
use yii\web\ServerErrorHttpException;

/**
 * 获取租户模块环境配置信息,存储至Redis,注册数据库连接组件
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since 1.0
 */
trait TenantDb
{
    /**
     * 数据库连接组件ID(基于多租户)
     *
     * @return string
     */    public static function getTenantDb()
    {
        $env = new Env();
        $tenantEnv = $env->getTenantEnv();

        if ($tenantEnv === false) {
            if ($env->hasErrors()) {
                foreach ($env->getFirstErrors() as $message) {
                    $firstErrors = $message;
                }
                throw new ServerErrorHttpException(Yii::t('error', Yii::t('error', Yii::t('error', '20006'), ['firstErrors' => $firstErrors])), 20006);
            } elseif (!$env->hasErrors()) {
                throw new ServerErrorHttpException('Multi-tenant HTTP requests fail for unknown reasons.');
            }
        }

        $tenantDb = $tenantEnv['data']['tenantid'] . 'Db';

        // 检查数据库连接组件、RBAC组件是否被注册
        if (!(Yii::$app->has($tenantDb) && Yii::$app->has('authManager'))) {
            // 注册数据库连接组件、RBAC组件
            Yii::$app->setComponents([
                $tenantDb => [
                    'class' => 'yii\db\Connection',
                    'dsn' => 'mysql:host=' . $tenantEnv['data']['db_info']['host'] . ';port=3306;dbname=' . $tenantEnv['data']['db_info']['database'] . '',
                    'username' => $tenantEnv['data']['db_info']['login'],
                    'password' => $tenantEnv['data']['db_info']['password'],
                    'tablePrefix' => $tenantEnv['data']['db_info']['prefix'],
                    'charset' => 'utf8',
                    'enableSchemaCache' => YII_ENV_PROD,
                    'schemaCache' => 'redisCache',
                ],
                'authManager' => [
                    'class' => 'yii\rbac\DbManager',
                    'db' => $tenantDb,
                    'itemTable' => '{{%rbac_auth_item}}',
                    'itemChildTable' => '{{%rbac_auth_item_child}}',
                    'assignmentTable' => '{{%rbac_auth_assignment}}',
                    'ruleTable' => '{{%rbac_auth_rule}}'
                ],
            ]);
        }

        return $tenantDb;
    }
}

6、编辑 \common\components\db\ActiveRecord.php,导入 common\traits\TenantDb

<?php
/**
 * Created by PhpStorm.
 * User: Administrator
 * Date: 2018/01/16
 * Time: 10:31
 */
namespace common\components\db;

use Yii;
use common\traits\TenantDb;

/**
 * 导入 TenantDb,注册数据库连接组件
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since 1.0
 */
class ActiveRecord extends \yii\db\ActiveRecord
{
    use TenantDb;

    /**
     * Returns the database connection used by this AR class.
     * By default, the "db" application component is used as the database connection.
     * You may override this method if you want to use a different database connection.
     * @return Connection the database connection used by this AR class.
     */    public static function getDb()
    {
        $tenantDb = self::getTenantDb();
        return Yii::$app->$tenantDb;
    }
}

7、基于 Trait 实现,导入 common\traits\TenantDb,将 init() 方法放入 Trait 中,新建 \common\traits\TenantMigration.php,将 $tenantDb 设置为数据库连接组件

<?php
/**
 * Created by PhpStorm.
 * User: WangQiang
 * Date: 2018/01/26
 * Time: 16:25
 */
namespace common\traits;

use Yii;
use common\traits\TenantDb;

/**
 * 导入 TenantDb,将 $tenantDb 设置为数据库连接组件
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since 1.0
 */
trait TenantMigration
{
    use TenantDb;

    /**
     * Initializes the migration.
     * This method will set [[db]] to be the 'db' application component, if it is `null`.
     */    public function init()
    {
        $tenantDb = self::getTenantDb();
        $this->db = $tenantDb;
        parent::init();
    }
}

8、新建 \common\components\db\Migration.php,导入 common\traits\TenantMigration

<?php
/**
 * Created by PhpStorm.
 * User: Administrator
 * Date: 2018/01/26
 * Time: 13:48
 */
namespace common\components\db;

use common\traits\TenantMigration;

/**
 * 导入 TenantMigration,将 $tenantDb 设置为数据库连接组件
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since 1.0
 */
class Migration extends \yii\db\Migration
{
    use TenantMigration;
}

9、在目录 \common\migrations 中查找:yii\db\Migration,批量替换为:common\components\db\Migration,如图4

图4

10、编辑 \common\migrations\db\m140703_123055_log.php,导入 common\traits\TenantMigration

<?php
require(Yii::getAlias('@yii/log/migrations/m141106_185632_log_init.php'));

use common\traits\TenantMigration;

class m140703_123055_log extends m141106_185632_log_init
{
    use TenantMigration;
}

11、编辑 \common\migrations\db\m140703_123813_rbac.php,导入 common\traits\TenantMigration

<?php
require(Yii::getAlias('@yii/rbac/migrations/m140506_102106_rbac_init.php'));

use common\traits\TenantMigration;

class m140703_123813_rbac extends m140506_102106_rbac_init
{
    use TenantMigration;
}

12、新建数据库迁移类,\console\controllers\MigrateController.php

<?php
/**
 * Created by PhpStorm.
 * User: Administrator
 * Date: 2018/01/26
 * Time: 17:35
 */
namespace console\controllers;

use common\traits\TenantMigration;

/**
 * 导入 TenantMigration,将 $tenantDb 设置为数据库连接组件
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since 1.0
 */
class MigrateController extends \yii\console\controllers\MigrateController
{
    use TenantMigration;
}

13、调整数据库迁移配置,编辑 \console\config\console.php,如图5

        'migrate' => [
            'class' => 'console\controllers\MigrateController',
            'migrationPath' => '@common/migrations/db',
            'migrationTable' => '{{%system_db_migration}}'
        ],

14、运行命令:.\yii app/setup,报错:Exception ‘yii\base\UnknownMethodException’ with message ‘Calling unknown method: yii\console\Request::get()’,如图6

图6

15、获取请求参数时,判断当前请求是否通过命令行进行,编辑 \common\logics\http\tenant\Env.php

<?php
/**
 * Created by PhpStorm.
 * User: Administrator
 * Date: 2018/01/17
 * Time: 15:20
 */
namespace common\logics\http\tenant;

use Yii;
use yii\base\Model;
use yii\web\BadRequestHttpException;
use yii\web\ServerErrorHttpException;

/**
 * 多租户的模块环境配置
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since 1.0
 */class Env extends Model
{
    public $app_name;
    public $secret;
    public $tenant_id;

    public function attributeLabels()
    {
        return [
            'app_name' => \Yii::t('model/http/tenant/env', 'App Name'),
            'secret' => \Yii::t('model/http/tenant/env', 'Secret'),
            'tenant_id' => \Yii::t('model/http/tenant/env', 'Tenant ID'),
        ];
    }

    /**
     * 返回租户模块环境配置信息
     *
     * @return array|false
     *
     * 格式如下:
     *
     * 租户模块环境配置信息
     * [
     *     'message' => '', //说明
     *     'data' => [], //数据
     * ]
     *
     * 失败(将错误保存在 [[yii\base\Model::errors]] 属性中)
     * false
     *
     * @throws ServerErrorHttpException 如果响应状态码不等于20x
     */    public function getTenantEnv()
    {
        /* 获取请求参数 */        $request = Yii::$app->request;

        // 判断当前请求是否通过命令行进行
        if ($request->isConsoleRequest) {
            $get = $request->getParams();

            /* 判断请求参数中租户ID是否存在 */            if (empty($get[1])) {
                // throw new BadRequestHttpException(Yii::t('error', '20007'), 20007);
                $get[1] = env('TENANT_DEFAULT_ID');
            }
            $this->tenant_id = $get[1];
        } else {
            $get = $request->get();

            /* 判断请求参数中租户ID是否存在 */            if (empty($get['tenantid'])) {
                // throw new BadRequestHttpException(Yii::t('error', '20007'), 20007);
                $get['tenantid'] = env('TENANT_DEFAULT_ID');
            }
            $this->tenant_id = $get['tenantid'];
        }

        // 设置多租户数据的缓存键
        $redisCache = Yii::$app->redisCache;
        $tenantKey = 'tenant:' . $this->tenant_id;

        // 从缓存中取回多租户数据
        $tenantData = $redisCache[$tenantKey];

        if ($tenantData === false) {
            $this->app_name = env('TENANT_APP_NAME');
            $this->secret = env('TENANT_SECRET');

            $response = Yii::$app->tenantHttp->createRequest()
                ->setMethod('get')
                ->setUrl('getTenantEnv')
                ->setData([
                    'appname' => $this->app_name,
                    'secret' => $this->secret,
                    'tenantid' => $this->tenant_id,
                ])
                ->send();
            // 检查响应状态码是否等于20x
            if ($response->isOk) {
                // 检查业务逻辑是否成功
                if ($response->data['returnCode'] === 0) {
                    $tenantData = ['message' => $response->data['returnDesc'], 'data' => $response->data['returnData']];
                    // 将多租户数据存放到缓存供下次使用
                    $redisCache[$tenantKey] = $tenantData;
                    return $tenantData;
                } else {
                    $this->addError('tenant_id', $response->data['returnDesc']);
                    return false;
                }
            } else {
                throw new ServerErrorHttpException(Yii::t('error', Yii::t('error', Yii::t('error', '20005'), ['statusCode' => $response->getStatusCode()])), 20005);
            }
        } else {
            return $tenantData;
        }

    }

}

16、删除数据库中所有表,运行命令:.\yii app/setup,报错:Exception ‘yii\db\Exception’ with message ‘SQLSTATE[42S02]: Base table or view not found: 1146 Table ‘cmcp-api.ca_system
_db_migration’ doesn’t exist’,如图7

图7

17、运行命令:.\yii app/setup,报错:Exception ‘yii\base\InvalidConfigException’ with message ‘You should configure “log” component to use one or more databa
se targets before executing this migration.’,如图8

图8

18、删除 E:\wwwroot\cmcp-api\common\migrations\db\m140703_123055_log.php,因为日志组件已经配置为基于文件存储了的。

19、删除数据库中所有表,运行命令:.\yii app/setup,报错:Exception: Undefined class constant ‘STATUS_PUBLISHED’ (E:\wwwroot\cmcp-api\common\migrations\db\m150725_192740_seed_dat
a.php:63),如图9

图9

20、编辑 \common\migrations\db\m150725_192740_seed_data.php,\common\models\Page 替换为 \common\logics\Page,如图10

        $this->insert('{{%page}}', [
            'slug' => 'about',
            'title' => 'About',
            'body' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
            'status' => \common\logics\Page::STATUS_PUBLISHED,
            'created_at' => time(),
            'updated_at' => time(),
        ]);

图10

21、删除数据库中所有表,运行命令:.\yii app/setup,报错:Exception ‘yii\base\UnknownPropertyException’ with message ‘Getting unknown property: yii\console\Application::tenantHtt
p’,如图11

图11

22、通过应用组件配置客户端,编辑 \common\config\web.php,将 tenantHttp 组件移至 \common\config\base.php,如图12

        'tenantHttp' => [
            'class' => 'yii\httpclient\Client',
            'baseUrl' => Yii::getAlias('@tenantUrl'),
            'transport' => 'yii\httpclient\CurlTransport'
        ],

图12

23、删除数据库中所有表,运行命令:.\yii app/setup default,Migrated up successfully.如图13

图13

24、删除数据库中所有表,运行命令:.\yii migrate default,Migrated up successfully.

25、继续运行命令:.\yii rbac-migrate default,报错:Exception ‘yii\base\InvalidConfigException’ with message ‘Failed to instantiate component or class “db”.’,如图14

图14

26、编辑 RBAC 数据库迁移类,\console\controllers\RbacMigrateController.php,

<?php

namespace console\controllers;

use yii\console\controllers\MigrateController;
use common\traits\TenantMigration;

/**
 * @author Eugene Terentev <eugene@terentev.net>
 */class RbacMigrateController extends MigrateController
{
    use TenantMigration;

    /**
     * Creates a new migration instance.
     * @param string $class the migration class name
     * @return \common\rbac\Migration the migration instance
     */    protected function createMigration($class)
    {
        $file = $this->migrationPath . DIRECTORY_SEPARATOR . $class . '.php';
        require_once($file);

        return new $class();
    }
}

27、继续运行命令:.\yii rbac-migrate default,Migrated up successfully.如图15

图15

28、打开网址:http://backend.cmcp-api.localhost/ ,报错:SQLSTATE[42S02]: Base table or view not found: 1146 Table ‘cmcp-api.ca_system_log’ doesn’t exist
The SQL being executed was: SELECT COUNT(*) FROM `ca_system_log`,如图16

图16

29、编辑 \backend\views\layouts\common.php,涉及到 SystemLog:: 的相关代码,需要删除掉,因为在实现数据库迁移时,系统日志表文件已经删除。

30、运行命令:.\yii migrate/create create_news_table,报错:Exception ‘yii\web\ServerErrorHttpException’ with message ‘多租户HTTP请求失败:模块信息未配置’,如图17

图17

31、决定将租户ID从参数形式转换为选项,如–tenantid=default,编辑 \common\logics\http\tenant\Env.php

        // 判断当前请求是否通过命令行进行
        if ($request->isConsoleRequest) {
            $get = $request->getParams();

            foreach ($get as $value) {
                $option = explode('=', $value);
                if ($option[0] == '--tenantid') {
                    $optionValue = $option[1];
                    break;
                }
            }

            /* 判断请求参数中租户ID是否存在 */            if (empty($optionValue)) {
                // throw new BadRequestHttpException(Yii::t('error', '20007'), 20007);
                $optionValue = env('TENANT_DEFAULT_ID');
            }
            $this->tenant_id = $optionValue;
        } else {
            $get = $request->get();

            /* 判断请求参数中租户ID是否存在 */            if (empty($get['tenantid'])) {
                // throw new BadRequestHttpException(Yii::t('error', '20007'), 20007);
                $get['tenantid'] = env('TENANT_DEFAULT_ID');
            }
            $this->tenant_id = $get['tenantid'];
        }

32、通过覆盖在 [[yii\console\Controller::options()]] 中的方法, 以指定可用于控制台命令(controller/actionID)选项。编辑 \console\controllers\AppController.php

<?php

namespace console\controllers;

use Yii;
use yii\console\Controller;
use yii\helpers\Console;

/**
 * @author Eugene Terentev <eugene@terentev.net>
 */class AppController extends Controller
{
    public $writablePaths = [
        '@common/runtime',
        '@frontend/runtime',
        '@frontend/web/assets',
        '@backend/runtime',
        '@backend/web/assets',
        '@api/runtime',
        '@api/web/assets',
        '@storage/cache',
        '@storage/web/source'
    ];

    public $executablePaths = [
        '@backend/yii',
        '@api/yii',
        '@frontend/yii',
        '@console/yii',
    ];

    public $generateKeysPaths = [
        '@base/.env'
    ];

    public $tenantid;

    public function options($actionID)
    {
        return ['color', 'interactive', 'help', 'tenantid'];
    }

    public function actionSetup()
    {
        $this->runAction('set-writable', ['interactive' => $this->interactive]);
        $this->runAction('set-executable', ['interactive' => $this->interactive]);
        $this->runAction('set-keys', ['interactive' => $this->interactive]);
        \Yii::$app->runAction('migrate/up', ['interactive' => $this->interactive]);
        \Yii::$app->runAction('rbac-migrate/up', ['interactive' => $this->interactive]);
    }

    public function actionSetWritable()
    {
        $this->setWritable($this->writablePaths);
    }

    public function actionSetExecutable()
    {
        $this->setExecutable($this->executablePaths);
    }

    public function actionSetKeys()
    {
        $this->setKeys($this->generateKeysPaths);
    }

    public function setWritable($paths)
    {
        foreach ($paths as $writable) {
            $writable = Yii::getAlias($writable);
            Console::output("Setting writable: {$writable}");
            @chmod($writable, 0777);
        }
    }

    public function setExecutable($paths)
    {
        foreach ($paths as $executable) {
            $executable = Yii::getAlias($executable);
            Console::output("Setting executable: {$executable}");
            @chmod($executable, 0755);
        }
    }

    public function setKeys($paths)
    {
        foreach ($paths as $file) {
            $file = Yii::getAlias($file);
            Console::output("Generating keys in {$file}");
            $content = file_get_contents($file);
            $content = preg_replace_callback('/<generated_key>/', function () {
                $length = 32;
                $bytes = openssl_random_pseudo_bytes(32, $cryptoStrong);
                return strtr(substr(base64_encode($bytes), 0, $length), '+/', '_-');
            }, $content);
            file_put_contents($file, $content);
        }
    }
}

33、通过覆盖在 [[yii\console\Controller::options()]] 中的方法, 以指定可用于控制台命令(controller/actionID)选项。编辑 \console\controllers\MigrateController.php、\console\controllers\RbacMigrateController.php
\console\controllers\MigrateController.php

<?php
/**
 * Created by PhpStorm.
 * User: Administrator
 * Date: 2018/01/26
 * Time: 17:35
 */
namespace console\controllers;

use common\traits\TenantMigration;

/**
 * 导入 TenantMigration,将 $tenantDb 设置为数据库连接组件
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since 1.0
 */
class MigrateController extends \yii\console\controllers\MigrateController
{
    use TenantMigration;

    public $tenantid;

    public function options($actionID)
    {
        return ['color', 'interactive', 'help', 'tenantid'];
    }
}

\console\controllers\RbacMigrateController.php

<?php

namespace console\controllers;

use yii\console\controllers\MigrateController;
use common\traits\TenantMigration;

/**
 * @author Eugene Terentev <eugene@terentev.net>
 */class RbacMigrateController extends MigrateController
{
    use TenantMigration;

    public $tenantid;

    public function options($actionID)
    {
        return ['color', 'interactive', 'help', 'tenantid'];
    }

    /**
     * Creates a new migration instance.
     * @param string $class the migration class name
     * @return \common\rbac\Migration the migration instance
     */    protected function createMigration($class)
    {
        $file = $this->migrationPath . DIRECTORY_SEPARATOR . $class . '.php';
        require_once($file);

        return new $class();
    }
}

34、删除数据库中所有表,调整第24、25、26步骤的命令,.\yii app/setup –tenantid=default、.\yii migrate –tenantid=default、.\yii rbac-migrate –tenantid=default,成功运行

35、运行命令:.\yii migrate/create create_news_table、.\yii migrate/create create_news_table –tenantid=default,成功运行,如图18

图18

36、由于已经在控制器中定义了数据库 application component 的 ID,将第8(删除 \common\components\db\Migration.php)、9、11步骤还原,在目录 \common\migrations 中查找:common\components\db\Migration,批量替换为:yii\db\Migration,如图19

图19

 

永夜