HHeLiBeXの日記 正道編

日々の記憶の記録とメモ‥

Zend FrameworkからLaravelに移行する話(5)

はじめに

お仕事で、Zend Frameworkのバージョンアップをしなければならなくなった・・と思ったら、Zend Frameworkはもうなくて、Laminas Projectに移って新たなフレームワークとして公開されている。 しかし、Laminas MVCでは必要な要件を満たさないことが分かってとん挫していた。

そこで、次の候補としてLaravelを挙げて、必要な要件を満たせるかどうかを一歩ずつ調査していく。

要件(5)

5つ目の要件は、「セントラルDBに保存したテナントDB接続情報を使ってテナントDBへの接続および操作ができること」。

config/database.phpに接続情報を直接書いて接続するというのはLaravelのドキュメントなどを見て分かっているのだが、動的に接続情報を追加できるのかが分からなかったので、その検証を行う。

導入

こちらでセットアップした環境を(コピーして)使っていく。

hhelibex.hatenablog.jp

私は以下のようにコピーを作成。

cp -pr laravel-setup-4-forward-parameters laravel-setup-5-multiple-databases
cd laravel-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 UNIQUE,
    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への接続設定を以下のように.envファイルに記述する。

DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=central
DB_USERNAME=central_user
DB_PASSWORD=central_password

次に、以下のコマンドを実行してapp/Http/Controllers/TenantController.phpを作る。

php artisan make:controller '\App\Http\Controllers\TenantController'

このファイルを編集して、以下のようにする。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;

class TenantController extends Controller
{
    public function tenant(Request $request)
    {
        $tenantCode = $request->get('tenant_code');
        $result = DB::table('db_master_2')->where('tenant_code', $tenantCode)->first();
        return view('tenant.tenant', ['db_name' => $result->db_name]);
    }
}

次に、ビューファイルresources/views/tenant/tenant.tplを以下の内容で作成する。

DB name:{$db_name|escape:"html"}

最後に、routes/web.phpを編集して、以下の行を追加する。

Route::get('/tenant/tenant', [\App\Http\Controllers\TenantController::class, 'tenant']);

動作確認1

ここでいったん動作確認をする。

以下のURLにアクセスして、ページが表示されるかを確認する。

http://<your-ip-address>/laravel-setup-5-multiple-databases/public/tenant/tenant?tenant_code=0001

以下のようなテキストが表示されればOK。

DB name:tenant_0001 

設定・実装2

テナントDBの接続情報を動的に追加するためのミドルウェアを作成する。

php artisan make:middleware DatabaseConnection

ファイルapp/Http/Middleware/DatabaseConnection.phpを以下のように編集する。

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpFoundation\Response;

class DatabaseConnection
{
    /**
     * Handle an incoming request.
     *
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     */
    public function handle(Request $request, Closure $next): Response
    {
        $tenantDbs = DB::table('db_master_2')->orderBy('tenant_code')->get();
        foreach ($tenantDbs as $tenantDb) {
            Config::set('database.connections.tenant-' . $tenantDb->tenant_code,
                [
                    'driver' => 'pgsql',
                    'host' => $tenantDb->db_host,
                    'port' => $tenantDb->db_port,
                    'database' => $tenantDb->db_name,
                    'username' => $tenantDb->db_user,
                    'password' => $tenantDb->db_password,
                ]);
        }
        return $next($request);
    }
}

このミドルウェアが呼び出されるように、bootstrap/app.phpファイルを以下のように編集する。

<?php

use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware) {
        $middleware->append(
            [
                \App\Http\Middleware\DatabaseConnection::class,
            ]
        );
    })
    ->withExceptions(function (Exceptions $exceptions) {
        //
    })->create();

次に、テナントDBのuser_masterテーブルのデータを取得する処理を作成する。

まず、app/Http/Controllers/TenantController.phpを以下のように編集する。やることはusersメソッドの追加。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;

class TenantController extends Controller
{
    public function tenant(Request $request)
    {
        $tenantCode = $request->get('tenant_code');
        $result = DB::table('db_master_2')->where('tenant_code', $tenantCode)->first();
        return view('tenant.tenant', ['db_name' => $result->db_name]);
    }
    public function users(Request $request)
    {
        $tenantCode = $request->get('tenant_code');
        $result = DB::connection('tenant-' . $tenantCode)->table('user_master')->get();
        return view('tenant.users', ['users' => $result]);
    }
}

続いてビューテンプレートを作成する。パスはresources/views/tenant/users.tpl

<ul>
{foreach from=$users item='user'}
<li>{$user->id|escape:"html"}:{$user->user_name|escape:"html"}({$user->real_name|escape:"html"})</li>
{/foreach}
</ul>

最後に、routes/web.phpに以下の行を追記する。

Route::get('/tenant/users', [\App\Http\Controllers\TenantController::class, 'users']);

動作確認2

以下のURLにアクセスして、ページが表示されるかを確認する。

http://<your-ip-address>/laravel-setup-5-multiple-databases/public/tenant/users?tenant_code=0001

以下のようなテキストが表示されればOK。

・1:tenant0001(テナント1 太郎)
・2:tenant0002(テナント1 次郎)

まとめ

  • config/database.phpに動的に設定を追加することで、実現したいことはできた