在 Yii 2 Starter Kit 中数据库迁移的多租户实现

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

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

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

图1

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

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

图2

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

运行命令:./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

在目录 /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

运行命令:./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

删除数据库中所有表,运行命令:./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

运行命令:./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

删除数据库中所有表,运行命令:./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(),
        ]);
编辑 /common/migrations/db/m150725_192740_seed_data.php,/common/models/Page 替换为 /common/logics/Page

图10

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

删除数据库中所有表,运行命令:./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'
        ],
通过应用组件配置客户端,编辑 /common/config/web.php,将 tenantHttp 组件移至 /common/config/base.php

图12

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

删除数据库中所有表,运行命令:./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

继续运行命令:./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

继续运行命令:./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

打开网址: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

运行命令:./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

运行命令:./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

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

图19

 

原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/180973.html

(0)
上一篇 2021年10月31日
下一篇 2021年10月31日

相关推荐

发表回复

登录后才能评论