1. Previous article: https://www.shuijingwanwq.com/2018/01/18/2328/
2. Referring to step 11, move the db component to the development environment to facilitate the use of Gii, \common\config\base.php, as shown in Figure 1

3. Configure for the production environment to test the multi-tenant implementation of database migration without relying on the db component, as shown in Figure 2

4. Run the command: .\yii app/setup, error: Exception ‘yii\base\InvalidConfigException’ with message ‘Failed to instantiate component or class “db”.’, as shown in Figure 3

5. Implement based on a Trait, place the getTenantDb() method into the Trait, create \common\traits\TenantDb.php, and register the database connection component and RBAC component via the [[yii\di\ServiceLocator::setComponents()]] method
<?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. Edit \common\components\db\ActiveRecord.php, import 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. Implement based on a Trait, import common\traits\TenantDb, place the init() method into the Trait, create \common\traits\TenantMigration.php, and set $tenantDb as the database connection component
<?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. Create \common\components\db\Migration.php, import 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. In the directory \common\migrations, search for: yii\db\Migration, and batch replace with: common\components\db\Migration, as shown in Figure 4

10. Edit \common\migrations\db\m140703_123055_log.php, import 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. Edit \common\migrations\db\m140703_123813_rbac.php, import 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. Create the database migration class, \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. Adjust the database migration configuration, edit \console\config\console.php, as shown in Figure 5
'migrate' => [
'class' => 'console\controllers\MigrateController',
'migrationPath' => '@common/migrations/db',
'migrationTable' => '{{%system_db_migration}}'
],
14. Run the command: .\yii app/setup, error: Exception ‘yii\base\UnknownMethodException’ with message ‘Calling unknown method: yii\console\Request::get()’, as shown in Figure 6

15. When getting request parameters, determine whether the current request is made via the command line, edit \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. Delete all tables in the database, run the command: .\yii app/setup, error: 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’, as shown in Figure 7
![Delete all tables in the database, run the command: .\yii app/setup, error: 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'](https://media.shuijingwanwq.com/2018/01/7-3.png)
17. Run the command: .\yii app/setup, error: Exception ‘yii\base\InvalidConfigException’ with message ‘You should configure “log” component to use one or more databa
se targets before executing this migration.’, as shown in Figure 8

18. Delete E:\wwwroot\cmcp-api\common\migrations\db\m140703_123055_log.php, because the log component has already been configured to be file-based.
19. Delete all tables in the database, run the command: .\yii app/setup, error: Exception: Undefined class constant ‘STATUS_PUBLISHED’ (E:\wwwroot\cmcp-api\common\migrations\db\m150725_192740_seed_dat
a.php:63), as shown in Figure 9

20. Edit \common\migrations\db\m150725_192740_seed_data.php, replace \common\models\Page with \common\logics\Page, as shown in Figure 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(),
]);

21. Delete all tables in the database, run the command: .\yii app/setup, error: Exception ‘yii\base\UnknownPropertyException’ with message ‘Getting unknown property: yii\console\Application::tenantHtt
p’, as shown in Figure 11

22. Configure the client via application components, edit \common\config\web.php, and move the tenantHttp component to \common\config\base.php, as shown in Figure 12
'tenantHttp' => [
'class' => 'yii\httpclient\Client',
'baseUrl' => Yii::getAlias('@tenantUrl'),
'transport' => 'yii\httpclient\CurlTransport'
],

23. Delete all tables in the database, run the command: .\yii app/setup default, Migrated up successfully. As shown in Figure 13

24. Delete all tables in the database, run the command: .\yii migrate default, Migrated up successfully.
25. Continue to run the command: .\yii rbac-migrate default, error: Exception ‘yii\base\InvalidConfigException’ with message ‘Failed to instantiate component or class “db”.’, as shown in Figure 14

26. Edit the RBAC database migration class, \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. Continue to run the command: .\yii rbac-migrate default, Migrated up successfully. As shown in Figure 15

28. Open the URL: http://backend.cmcp-api.localhost/ , error: 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`, as shown in Figure 16
![Open the URL: http://backend.cmcp-api.localhost/ , error: 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`](https://media.shuijingwanwq.com/2018/01/16-2.png)
29. Edit \backend\views\layouts\common.php, the related code involving SystemLog:: needs to be deleted, because the system log table file was already deleted when implementing the database migration.
30. Run the command: .\yii migrate/create create_news_table, error: Exception ‘yii\web\ServerErrorHttpException’ with message ‘Multi-tenant HTTP request failed: Module information not configured’, as shown in Figure 17

31. Decide to convert the tenant ID from a parameter format to an option, such as –tenantid=default, edit \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. By overriding the method in [[yii\console\Controller::options()]], specify the options available for the console command (controller/actionID). Edit \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. By overriding the method in [[yii\console\Controller::options()]], specify the options available for the console command (controller/actionID). Edit \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. Delete all tables in the database, adjust the commands in steps 24, 25, and 26 to .\yii app/setup –tenantid=default, .\yii migrate –tenantid=default, and .\yii rbac-migrate –tenantid=default, run successfully
35. Run the commands: .\yii migrate/create create_news_table, .\yii migrate/create create_news_table –tenantid=default, run successfully, as shown in Figure 18

36. Since the ID of the database application component has already been defined in the controller, revert steps 8 (delete \common\components\db\Migration.php), 9, and 11. In the directory \common\migrations, search for: common\components\db\Migration, and batch replace with: yii\db\Migration, as shown in Figure 19

