HHeLiBeXの日記 正道編

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

Zend FrameworkからLaminasに移行する話(6)

はじめに

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

いちから勉強しないといけないじゃん、ということで、必要な要件を満たせるかどうかを一歩ずつ調査していく。

要件(6)

6つ目の要件は、「DBのテーブルに対してJOINクエリが発行できること」。

Zend Frameworkでは、クエリビルダーがあったので、それでJOINクエリを書いていたが、Laminasでも同じようにできるのか、はたまたSQL文をベタで書く羽目になるのか、その辺りを調査していく。

導入

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

hhelibex.hatenablog.jp

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

cp -pr laminas-setup-5-multiple-databases laminas-setup-6-join-queries
cd laminas-setup-6-join-queries

DBへの追加

今回、テナントDBの方がメインになるので、テナントDBの方でJOINクエリを試していく。 以下のようにテナントDBに新たにテーブルを追加する。

\c tenant_0001 tenant_0001_user

CREATE TABLE messages(
    id BIGSERIAL PRIMARY KEY,
    user_id BIGINT NOT NULL,
    title VARCHAR(128) NOT NULL,
    content TEXT NOT NULL);

INSERT INTO messages(user_id, title, content)
 VALUES(1, 'テナント1ー1さんへのメッセージ1', 'メッセージ1の内容');
INSERT INTO messages(user_id, title, content)
 VALUES(2, 'テナント1ー2さんへのメッセージ1', 'メッセージ1の内容');

INSERT INTO user_master(user_name, real_name)
 VALUES('tenant0002', 'テナント1 次郎');
\c tenant_0002 tenant_0002_user

CREATE TABLE messages(
    id BIGSERIAL PRIMARY KEY,
    user_id BIGINT NOT NULL,
    title VARCHAR(128) NOT NULL,
    content TEXT NOT NULL);

INSERT INTO messages(user_id, title, content)
 VALUES(1, 'テナント2ー1さんへのメッセージ1', 'メッセージ1の内容');
INSERT INTO messages(user_id, title, content)
 VALUES(2, 'テナント2ー2さんへのメッセージ1', 'メッセージ1の内容');

INSERT INTO user_master(user_name, real_name)
 VALUES('tenant0001', 'テナント2 太郎');

想定するクエリ

先に追加したテーブルを使って、「実名に"太郎"を含むユーザーのメッセージ一覧」を取得するクエリを書いてみる。 SQL文で書くと以下のような感じになる。

SELECT m.id,
        m.user_id,
        m.title,
        m.content
    FROM messages m
    JOIN user_master u
    ON m.user_id = u.id
    WHERE u.real_name LIKE '%太郎%'
    ORDER BY m.id
    LIMIT 10 OFFSET 0

設定・実装

まず、messagesテーブル用のModelクラスmodule/Application/src/Model/Tenant/Message.phpを作る。

<?php

declare(strict_types=1);

namespace Application\Model\Tenant;

class Message
{
    public int $id;
    public int $userId;
    public string $title;
    public string $content;

    public function exchangeArray(array $data)
    {
        $this->id = isset($data['id']) ? $data['id'] : null;
        $this->userId = isset($data['user_id']) ? $data['user_id'] : null;
        $this->title = isset($data['title']) ? $data['title'] : null;
        $this->content = isset($data['content']) ? $data['content'] : null;
    }
}

次に、Tableクラスを作るのだが、以前のようにTableGatewayを介するのではなく、Adapterを直接扱う形になるので、その点だけ注意。 ファイルmodule/Application/src/Model/Tenant/MessagesTable.phpを以下のように作成する。

<?php

declare(strict_types=1);

namespace Application\Model\Tenant;

use RuntimeException;
use Laminas\Db\Adapter\Adapter;
use Laminas\Db\Sql\Sql;
use Laminas\Db\Sql\Predicate\Like;

use Application\Model\Tenant\Message;

class MessagesTable
{
    private $adapter;

    public function __construct(Adapter $adapter)
    {
        $this->adapter = $adapter;
    }
    public function fetchAll()
    {
        $sql = new Sql($this->adapter);
        $select = $sql->select();
        $select->from('messages');
        $select->order('id');
        $select->limit(10);
        $select->offset(0);

        $statement = $sql->prepareStatementForSqlObject($select);
        $results = $statement->execute();
        return $this->buildMessages($results);
    }
    public function getMessages($realNameCond)
    {
        if (is_null($realNameCond)) {
            throw new RuntimeException('Real name condition is required.');
        }

        $sql = new Sql($this->adapter);
        $select = $sql->select();
        $select->columns(['id', 'user_id', 'title', 'content']);
        $select->from(['m' => 'messages']);
        $select->join(['u' => 'user_master'], 'm.user_id = u.id', [], $select::JOIN_INNER);
        $like = new Like('real_name', "%{$realNameCond}%");
        $select->where($like);
        $select->order('m.id');
        $select->limit(10);
        $select->offset(0);

        $statement = $sql->prepareStatementForSqlObject($select);
        $results = $statement->execute();
        return $this->buildMessages($results);
    }
    private function buildMessages($results)
    {
        $messages = [];
        foreach ($results as $result) {
            $message = new Message();
            $message->exchangeArray($result);
            $messages[] = $message;
        }
        return $messages;
    }
}

続いて、サービスマネージャの設定を編集する。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\MessagesTable::class => function($container) {
                    $dbAdapter = $container->get(Model\Tenant\Adapter::class);
                    return new Model\Tenant\MessagesTable($dbAdapter);
                },
                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),
                        $container->get(Model\Tenant\MessagesTable::class)
                    );
                },
            ],
        ];
    }
}

編集内容は、Model\Tenant\MessagesTable::classのファクトリ定義の追加と、Controller\TenantController::classクラスのコンストラクタに渡すテーブルオブジェクトの追加。

続いて、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;
use Application\Model\Tenant\MessagesTable;

class TenantController extends AbstractActionController
{
    private $dbMaster2Table;
    private $userMasterTable;
    private $messagesTable;

    public function __construct(DbMaster2Table $dbMaster2Table, UserMasterTable $userMasterTable, MessagesTable $messagesTable)
    {
        $this->dbMaster2Table = $dbMaster2Table;
        $this->userMasterTable = $userMasterTable;
        $this->messagesTable = $messagesTable;
    }

    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,
        ]);
    }
    public function messagesAction()
    {
        $allResult = $this->messagesTable->fetchAll();
        $userResult = $this->messagesTable->getMessages('太郎');
        return new ViewModel([
            'all_messages' => $allResult,
            'user_messages' => $userResult,
        ]);
    }
}

コンストラクタへの引数の追加と、messagesAction()メソッドの追加。

最後にビューテンプレートの作成。ファイルパスはmodule/Application/view/application/tenant/messages.tpl

全メッセージ
<ul>
{foreach from=$all_messages item='message'}
<li>{$message->id|escape:"html"}:{$message->userId|escape:"html"}({$message->title|escape:"html"})</li>
{/foreach}
</ul>
ユーザーメッセージ
<ul>
{foreach from=$user_messages item='message'}
<li>{$message->id|escape:"html"}:{$message->userId|escape:"html"}({$message->title|escape:"html"})</li>
{/foreach}
</ul>

動作確認

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

http://192.168.56.xxx/laminas-setup-6-join-queries/public/tenant/messages?tenant_code=0001

以下のような画面が表示されればOK。

laminas-setup-6-join-queries-1

まとめ

  • クエリビルダーのメソッドが充実しているので、たいていのクエリはそれで書けそう
  • WHERE条件も、述語クラスが一通り用意されており、困ることはなさそう