1、前文:http://www.shuijingwanwq.com/2018/01/18/2328/
2、参考第11步骤,db 组件移至开发环境,以方便于 Gii 的使用,/common/config/base.php,如图1
3、配置为生产环境,以便于测试数据库迁移的多租户实现,不依赖于 db 组件,如图2
4、运行命令:./yii app/setup,报错:Exception ‘yii/base/InvalidConfigException’ with message ‘Failed to instantiate component or class “db”.’,如图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
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
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
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
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
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(),
]);
21、删除数据库中所有表,运行命令:./yii app/setup,报错:Exception ‘yii/base/UnknownPropertyException’ with message ‘Getting unknown property: yii/console/Application::tenantHtt
p’,如图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'
],
23、删除数据库中所有表,运行命令:./yii app/setup default,Migrated up successfully.如图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
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
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
29、编辑 /backend/views/layouts/common.php,涉及到 SystemLog:: 的相关代码,需要删除掉,因为在实现数据库迁移时,系统日志表文件已经删除。
30、运行命令:./yii migrate/create create_news_table,报错:Exception ‘yii/web/ServerErrorHttpException’ with message ‘多租户HTTP请求失败:模块信息未配置’,如图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
36、由于已经在控制器中定义了数据库 application component 的 ID,将第8(删除 /common/components/db/Migration.php)、9、11步骤还原,在目录 /common/migrations 中查找:common/components/db/Migration,批量替换为:yii/db/Migration,如图19
原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/tech/pnotes/250392.html
