はじめに
お仕事で、Zend Frameworkのバージョンアップをしなければならなくなった・・と思ったら、Zend Frameworkはもうなくて、Laminas Projectに移って新たなフレームワークとして公開されている。
いちから勉強しないといけないじゃん、ということで、必要な要件を満たせるかどうかを一歩ずつ調査していく。
要件(5)
5つ目の要件は、「セントラルDBに保存したテナントDB接続情報を使ってテナントDBへの接続および操作ができること」。
Laminasの設定ファイルにDB接続情報を書いて接続するっていうのができることは、チュートリアルなどを見て確認しているのだけど、独自にモデルを作れるところまで確認できなかったので、その検証を行う。
導入
こちらでセットアップした環境を(コピーして)使っていく。
私は以下のようにコピーを作成。
cp -pr laminas-setup-4-multiple-modules laminas-setup-5-multiple-databases cd laminas-setup-5-multiple-databases
DBの作成
今回、PostgreSQLを使用する。
まず、「セントラルDB」と「テナントDB」を作っていく。
セントラルDB
DDLは以下のような感じ。
CREATE ROLE central_user WITH CREATEDB LOGIN PASSWORD 'central_password'; CREATE DATABASE central WITH OWNER central_user TEMPLATE = 'template0' ENCODING = 'UTF8' LC_COLLATE = 'ja_JP.UTF-8' LC_CTYPE = 'ja_JP.UTF-8'; \c central central_user; CREATE TABLE db_master_2( id BIGSERIAL PRIMARY KEY, tenant_code VARCHAR(64) NOT NULL, db_host VARCHAR(256) NOT NULL, db_port INTEGER NOT NULL, db_user VARCHAR(256) NOT NULL, db_password VARCHAR(256) NOT NULL, db_name VARCHAR(256) NOT NULL); INSERT INTO db_master_2(tenant_code, db_host, db_port, db_user, db_password, db_name) VALUES('0001', 'localhost', 5432, 'tenant_0001_user', 'tenant_0001_password', 'tenant_0001'); INSERT INTO db_master_2(tenant_code, db_host, db_port, db_user, db_password, db_name) VALUES('0002', 'localhost', 5432, 'tenant_0002_user', 'tenant_0002_password', 'tenant_0002');
テナントDB
DDLは以下のような感じ。
CREATE ROLE tenant_0001_user WITH CREATEDB LOGIN PASSWORD 'tenant_0001_password'; CREATE DATABASE tenant_0001 WITH OWNER tenant_0001_user TEMPLATE = 'template0' ENCODING = 'UTF8' LC_COLLATE = 'ja_JP.UTF-8' LC_CTYPE = 'ja_JP.UTF-8'; \c tenant_0001 tenant_0001_user; CREATE TABLE user_master(id BIGSERIAL PRIMARY KEY, user_name VARCHAR(256) NOT NULL, real_name VARCHAR(256) NOT NULL); INSERT INTO user_master(user_name, real_name) VALUES('tenant0001', 'テナント1 太郎');
CREATE ROLE tenant_0002_user WITH CREATEDB LOGIN PASSWORD 'tenant_0002_password'; CREATE DATABASE tenant_0002 WITH OWNER tenant_0002_user TEMPLATE = 'template0' ENCODING = 'UTF8' LC_COLLATE = 'ja_JP.UTF-8' LC_CTYPE = 'ja_JP.UTF-8'; \c tenant_0002 tenant_0002_user; CREATE TABLE user_master(id BIGSERIAL PRIMARY KEY, user_name VARCHAR(256) NOT NULL, real_name VARCHAR(256) NOT NULL); INSERT INTO user_master(user_name, real_name) VALUES('tenant0002', 'テナント2 次郎');
設定・実装1
まず、セントラルDBに接続して、データが取得できるようにする。
以下のコマンドを実行して、DB接続のためのコンポーネントをインストールする。
composer require laminas/laminas-db
途中で以下の質問が出てくるが、「1」を選択する。
Please select which config file you wish to inject 'Laminas\Db' into: [0] Do not inject [1] config/modules.config.php [2] config/development.config.php.dist Make your selection (default is 1):1
完了すると、config/modules.config.php
にLaminas\Db
が追加される。
続いて、セントラルDBへの接続設定を行う。
config/autoload/global.php
に以下のように記述を行う。
<?php /** * Global Configuration Override * * You can use this file for overriding configuration values from modules, etc. * You would place values in here that are agnostic to the environment and not * sensitive to security. * * NOTE: In practice, this file will typically be INCLUDED in your source * control, so do not include passwords or other sensitive information in this * file. */ return [ 'db' => [ 'driver' => 'Pdo', 'dsn' => 'pgsql:host=localhost;port=5432;dbname=central;user=central_user;password=central_password', ], ];
続いて、セントラルDBのdb_master_2
テーブルのデータを取得するためのModelを作成する。ファイルパスはmodule/Application/src/Model/DbMaster2.php
。
<?php declare(strict_types=1); namespace Application\Model; class DbMaster2 { public int $id; public string $tenantCode; public string $dbHost; public int $dbPort; public string $dbName; public string $dbUser; public string $dbPassword; public function exchangeArray(array $data) { $this->id = isset($data['id']) ? $data['id'] : null; $this->tenantCode = isset($data['tenant_code']) ? $data['tenant_code'] : null; $this->dbHost = isset($data['db_host']) ? $data['db_host'] : null; $this->dbPort = isset($data['db_port']) ? $data['db_port'] : null; $this->dbUser = isset($data['db_user']) ? $data['db_user'] : null; $this->dbPassword = isset($data['db_password']) ? $data['db_password'] : null; $this->dbName = isset($data['db_name']) ? $data['db_name'] : null; } }
続いて、オブジェクトマッパーを作っていく。ファイルパスはmodule/Application/src/Model/DbMaster2Table.php
。
<?php declare(strict_types=1); namespace Application\Model; use RuntimeException; use Laminas\Db\TableGateway\TableGatewayInterface; class DbMaster2Table { private $tableGateway; public function __construct(TableGatewayInterface $tableGateway) { $this->tableGateway = $tableGateway; } public function fetchAll() { return $this->tableGateway->select(); } public function getDb($tenantCode) { if (is_null($tenantCode)) { throw new RuntimeException('Tenant code is required.'); } $rowset = $this->tableGateway->select(['tenant_code' => $tenantCode]); $row = $rowset->current(); if (!$row) { throw new RuntimeException(sprintf( 'Could not find row with tenant code %s', $tenantCode )); } return $row; } }
続いて、ServiceManagerを書いていく。module/Application/src/Module.php
を以下のように編集する。
<?php declare(strict_types=1); namespace Application; use Laminas\Db\Adapter\AdapterInterface; use Laminas\Db\ResultSet\ResultSet; use Laminas\Db\TableGateway\TableGateway; use Laminas\ModuleManager\Feature\ConfigProviderInterface; class Module implements ConfigProviderInterface { public function getConfig(): array { /** @var array $config */ $config = include __DIR__ . '/../config/module.config.php'; return $config; } public function getServiceConfig(): array { return [ 'factories' => [ Model\DbMaster2Table::class => function($container) { $tableGateway = $container->get(Model\DbMaster2TableGateway::class); return new Model\DbMaster2Table($tableGateway); }, Model\DbMaster2TableGateway::class => function($container) { $dbAdapter = $container->get(AdapterInterface::class); $resultSetPrototype = new ResultSet(); $resultSetPrototype->setArrayObjectPrototype(new Model\DbMaster2()); return new TableGateway('db_master_2', $dbAdapter, null, $resultSetPrototype); }, ], ]; } public function getControllerConfig() { return [ 'factories' => [ Controller\TenantController::class => function($container) { return new Controller\TenantController( $container->get(Model\DbMaster2Table::class) ); }, ], ]; } }
getControllerConfig
に書いてあるController\TenantController
はこの次に作っていく。
module/Application/src/Controller/TenantController.php
を以下の内容で作成する。
<?php declare(strict_types=1); namespace Application\Controller; use Laminas\Mvc\Controller\AbstractActionController; use Laminas\View\Model\ViewModel; use Application\Model\DbMaster2Table; class TenantController extends AbstractActionController { private $dbMaster2Table; public function __construct(DbMaster2Table $dbMaster2Table) { $this->dbMaster2Table = $dbMaster2Table; } public function tenantAction() { $result = $this->dbMaster2Table->getDb($_GET['tenant_code']); return new ViewModel([ 'db_name' => $result->dbName, ]); } }
module/Application/config/module.config.php
にroutesを追加する。
'tenant' => [ 'type' => Segment::class, 'options' => [ 'route' => '/tenant[/:action]', 'defaults' => [ 'controller' => Controller\TenantController::class, 'action' => 'index', ], ], ],
最後に、ビューテンプレートを作成する。ファイルパスはmodule/Application/view/application/tenant/tenant.tpl
。
DB name:{$db_name|escape:"html"}
動作確認1
ここでいったん動作確認をする。
以下のURLにアクセスして、ページが表示されるかを確認する。
http://192.168.56.xxx/laminas-setup-5-multiple-databases/public/tenant/tenant?tenant_code=0001
以下のような画面が出ればOK。
設定・実装2
テナントDBのuser_master
テーブルのデータを取得するためのModelクラスを作成する。ファイルパスはmodule/Application/src/Model/Tenant/UserMaster.php
。
<?php declare(strict_types=1); namespace Application\Model\Tenant; class UserMaster { public int $id; public string $userName; public string $realName; public function exchangeArray(array $data) { $this->id = isset($data['id']) ? $data['id'] : null; $this->userName = isset($data['user_name']) ? $data['user_name'] : null; $this->realName = isset($data['real_name']) ? $data['real_name'] : null; } }
続いて、オブジェクトマッパーを作っていく。ファイルパスはmodule/Application/src/Model/Tenant/UserMasterTable.php
。
<?php declare(strict_types=1); namespace Application\Model\Tenant; use RuntimeException; use Laminas\Db\TableGateway\TableGatewayInterface; class UserMasterTable { private $tableGateway; public function __construct(TableGatewayInterface $tableGateway) { $this->tableGateway = $tableGateway; } public function fetchAll() { return $this->tableGateway->select(); } public function getUser($id) { if (is_null($id) || !is_int($id)) { throw new RuntimeException('ID is required.'); } $id = (int)$id; $rowset = $this->tableGateway->select(['id' => $id]); $row = $rowset->current(); if (!$row) { throw new RuntimeException(sprintf( 'Could not find row with ID %d', $id )); } return $row; } }
続いて、ServiceManagerを書いていく。module/Application/src/Module.php
を以下のように編集する。
<?php declare(strict_types=1); namespace Application; use Laminas\Db\Adapter\Adapter; use Laminas\Db\Adapter\AdapterInterface; use Laminas\Db\ResultSet\ResultSet; use Laminas\Db\TableGateway\TableGateway; use Laminas\ModuleManager\Feature\ConfigProviderInterface; class Module implements ConfigProviderInterface { public function getConfig(): array { /** @var array $config */ $config = include __DIR__ . '/../config/module.config.php'; return $config; } public function getServiceConfig(): array { return [ 'factories' => [ Model\DbMaster2Table::class => function($container) { $tableGateway = $container->get(Model\DbMaster2TableGateway::class); return new Model\DbMaster2Table($tableGateway); }, Model\DbMaster2TableGateway::class => function($container) { $dbAdapter = $container->get(AdapterInterface::class); $resultSetPrototype = new ResultSet(); $resultSetPrototype->setArrayObjectPrototype(new Model\DbMaster2()); return new TableGateway('db_master_2', $dbAdapter, null, $resultSetPrototype); }, Model\Tenant\UserMasterTable::class => function($container) { $tableGateway = $container->get(Model\Tenant\UserMasterTableGateway::class); return new Model\Tenant\UserMasterTable($tableGateway); }, Model\Tenant\UserMasterTableGateway::class => function($container) { $dbAdapter = $container->get(Model\Tenant\Adapter::class); $resultSetPrototype = new ResultSet(); $resultSetPrototype->setArrayObjectPrototype(new Model\Tenant\UserMaster()); return new TableGateway('user_master', $dbAdapter, null, $resultSetPrototype); }, Model\Tenant\Adapter::class => function($container) { $tenantCode = $_GET['tenant_code']; $dbMaster2Table = $container->get(Model\DbMaster2Table::class); $result = $dbMaster2Table->getDb($tenantCode); $config = [ 'driver' => 'pdo_pgsql', 'database' => $result->dbName, 'username' => $result->dbUser, 'password' => $result->dbPassword, 'hostname' => $result->dbHost, 'port' => $result->dbPort, 'charset' => 'UTF-8', ]; $dbAdapter = new Adapter($config); return $dbAdapter; }, ], ]; } public function getControllerConfig() { return [ 'factories' => [ Controller\TenantController::class => function($container) { return new Controller\TenantController( $container->get(Model\DbMaster2Table::class), $container->get(Model\Tenant\UserMasterTable::class) ); }, ], ]; } }
修正すべき個所は、getServiceConfig()
メソッド内へのファクトリ定義の追加と、getControllerConfig()
メソッド内でのController\TenantController
のコンストラクタのパラメータの修正。
続いて、module/Application/src/Controller/TenantController.php
を以下の内容に変更する。
<?php declare(strict_types=1); namespace Application\Controller; use Laminas\Mvc\Controller\AbstractActionController; use Laminas\View\Model\ViewModel; use Application\Model\DbMaster2Table; use Application\Model\Tenant\UserMasterTable; class TenantController extends AbstractActionController { private $dbMaster2Table; private $userMasterTable; public function __construct(DbMaster2Table $dbMaster2Table, UserMasterTable $userMasterTable) { $this->dbMaster2Table = $dbMaster2Table; $this->userMasterTable = $userMasterTable; } public function tenantAction() { $result = $this->dbMaster2Table->getDb($_GET['tenant_code']); return new ViewModel([ 'db_name' => $result->dbName, ]); } public function usersAction() { $result = $this->userMasterTable->fetchAll(); return new ViewModel([ 'users' => $result, ]); } }
最後に、ビューテンプレートmodule/Application/view/application/tenant/users.tpl
を以下の内容で作成する。
<ul> {foreach from=$users item='user'} <li>{$user->id|escape:"html"}:{$user->userName|escape:"html"}({$user->realName|escape:"html"})</li> {/foreach} </ul>
動作確認2
以下のURLにアクセスして、ページが表示されるかを確認する。
http://192.168.56.xxx/laminas-setup-5-multiple-databases/public/tenant/users?tenant_code=0001
以下のような画面が出ればOK。
まとめ
- 独自でDB接続をするには、\Laminas\Db\Adapter\Adapter のインスタンスを作ってあげればいい
- PDO経由にしてあげないと動かないので注意
driver
をpgsql
にしていて、int
型のプロパティへの代入ができないと言われてしばらくハマった- 型変換を独自でやるならPDO経由でなくてもいいけど、無駄な労力が発生する