entry-header-eye-catch.html
entry-title-container.html

entry-header-author-info.html
Article by

ピクシブ百科事典のテストにphp-mysql-engineを導入しました

はじめましての方ははじめまして、アルバイトとして働いているJavakkyです。

本記事では、データベースを利用するアプリケーションのテストに利用できるphp-mysql-engineというライブラリの導入方法と使用感について紹介します。

github.com

php-mysql-engineとは

php-mysql-engineは動画共有サイトを運営するVimeo社が開発しています。このライブラリの目論見はVimeo Engineering Blogで説明されています。

medium.com

このライブラリはVimeo社のMatt Brownさんが二年前に開発していたhacktophpを用いて、Slack社がHackで実装したslackhq/hack-sql-fakeをPHPコードに変換したものを下敷きにしています。

開発者のMatt Brownさんは、静的解析でVimeo社のコードを改善するためにPsalmを開発したことでも知られています。

medium.com

php-mysql-engineが提供するのはMartinFowler.comで紹介されているbliki: InMemoryTestDatabaseに相当する機能ですが、あくまで本物のMySQLではないので、このライブラリに依存するかどうかはプロジェクトごとに慎重に判断する必要があります。php-mysql-engineの説明では以下の記事にも言及されています。

phauer.com

対象環境

今回の対象はピクシブ百科事典のコードベースです。このプロダクトは独自フレームワークで構築されており、PDOを独自にラップしたクラスによってMySQLに接続する機能を提供しています。

ピクシブ百科事典の開発ではPHPUnitとPHPStanによるCIは既に稼動しています。

inside.pixiv.blog

なお、ピクシブ百科事典ではなくpixiv.netのテストではPHPUnitからユーザー権限でMySQL Serverを複数起動して並列処理することで高速化を図っています。

php-mysql-engine の追加

php-mysql-engineはPackagistで公開されているので、Composerで簡単にインストールできます。

composer require --dev vimeo/php-mysql-engine

FakePdoを使ってみる

php-mysql-engineは、FakePdoというPDO(PHP Data Objects)を継承したクラスを提供することでデータベースのエミュレーションを実現しています。ひとまず、テスト環境から呼び出すPDOをこちらに差し替えていきましょう。

元々の実装

ピクシブ百科事典ではDbManagerというクラスで接続先の系統ごとにPDOのインスタンスを管理しています。このクラスでは独自のPDOクラスのインスタンス生成の処理がハードコードされていたので、今回の変更に際してPdoFactoryInterfaceというクラスに分割することにしました。

<?php

final class FakePdoFactory implements PdoFactoryInterface
{
   /**
    * @return FakePdo
    */
    public function createPdo(string $dsn, string $username = '', string $passwd = '', array $options = []): PDO
    {
        return new FakePdo($dsn, $username, $passwd, $options);
    }
}

テストからの呼び出し

まず、テストの実行前処理(PHPUnitでいうsetUpBeforeなど)でDbManagerPdoFactoryを登録します。その後、利用するテーブルそれぞれについて CREATE TABLEを実行しておきます。(ここではファイルと同じディレクトリのcreate_tables.sqlにデータベースの CREATE TABLE 文が列挙して記述されているものとします)

<?php

//Factoryの登録
DbManager::setPdoFactory(new FakePdoFactory());

// データベースの定義SQLを分割して
$str = str_replace("\n", "", file_get_contents(__DIR__ . '/create_tables.sql'));
foreach (explode(";", $str) as $query) {
    if ($query === '') { continue; }
    // CREATE TABLE を実行する
    self::$dao->prepare($query.';')->execute();
}

あとはDbManager::getInstance()でPDOを取得するなり、DbManagerを利用したDBアプリケーションのコードを呼び出すなりして本番環境のデータベースを用いずにテストを行うことができます。

発生した問題

と、このような手順で実行しようとしたのですが、いくつか注意点があったので説明していきます。

VARBINARY型が実装されていなかった

VARBINARY型が未実装状態でペンディングされていたので、tadsanが実装したPull Requestがマージされました。

github.com

現在Packagistからインストールできるバージョンでは問題なく利用できます。

PDO生成時のDSN構文が異なる

今回はMySQLを利用しましたが、どうにも構文が違うようで、以下のようなエラーが発生しました。

Nyholm\Dsn\Exception\SyntaxException: The provided DSN is not valid. Maybe you need to url-encode the user/password? (host=localhost; dbname=dbname; port=3307) in nyholm/dsn/src/DsnParser.php:132

PDOに渡すDSNの構文はドライバによって異なり、MySQLでは以下のような形式になっています。

mysql:host=localhost;port=3307;dbname=testdb;

www.php.net

そして、標準のPDOでは以下のように、; (セミコロン)の後に空白が入っていても正常に動作します。

mysql:host=localhost; port=3307; dbname=testdb;

対して、php-mysql-engineで依存しているnyholm/dsnでは空白が入っていると構文がvalidではないとエラーが出てしまいました。ピクシブ百科事典のリリース当時からのDSN組み立て実装がたまたまそうしていたというだけで特に意味はなかったので、不要な空白を削除することで解決しました。

collation-serveの設定ができない

MySQLでは、それぞれのテーブルに照合順序を設定することができます。が、もちろん照合順序にはデフォルト値が存在するため、省略することができます。

今回用意した create_tables.sql ではこれを省略したSQLを記述していたため、以下のようなエラーが発生しました。

UnexpectedValueException: No default collation or character set given in vimeo/php-mysql-engine/src/Processor/CreateProcessor.php:76

CREATE TABLEDEFAULT COLLATEを省略したときの値はmy.cnfあるいはMySQL Serverの起動オプションのcollation-serveで設定できますが、現在のphp-mysql-engineには未実装です。

今回はCREATE TABLE文の文字列をFakePdoに渡す前にCOLLATEを挿入することで回避しました。

PDO::quote()がない

pixivでは社内でPxvSqlというSQL用のクエリビルダー(というよりテンプレートエンジンのようなもの)を開発しており、PDO::quote()メソッドに依存しているためエラーが起きました。

PDO::quote(): SQLSTATE[00000]: No error: PDO constructor was not called

実はphp-mysql-engineのFakePdoPDOクラスを継承してはいますが、本来のPDOのコンストラクタを呼ばず未初期化の状態で必要なメソッドを全て上書き実装することでデータベースへの接続をエミュレートしています。

FakePdo::quote()が実装されていないことが問題なので、FakePdoを継承したクラスで quote() メソッドを独自実装することで問題を回避しました。

<?php

use Vimeo\MysqlEngine\Php7\FakePdo as BaseFakePdo;

/**
 * PHP MySQL Engine
 *
 * @see https://github.com/vimeo/php-mysql-engine
 */
final class FakePdo extends BaseFakePdo
{
    /**
     * Quotes a string for use in a query
     *
     * @see https://www.php.net/manual/ja/pdo.quote.php
     * @param string $string
     * @param int $parameter_type
     * @return string
     */
    public function quote($string, $parameter_type = PDO::PARAM_STR)
   {
        // https://github.com/php/php-src/blob/php-8.0.1/ext/mysqlnd/mysqlnd_charset.c#L860-L878
        $quoted_string = strtr($string, [
            "\0" => '\0',
            "\n" => '\n',
            "\r" => '\r',
            "\\" => '\\\\',
            "\'" => '\\\'',
            "\"" => '\\"',
            "\032" => '\Z',
       ]);

       return "'{$quoted_string}'";
   }
}

これについては、tadsanが提出したPull Requestがマージされたので、次回リリースに含まれると思われます。

github.com

既知の問題

ピクシブ百科事典で利用しているクエリの全てをphp-mysql-engineで実装するには未実装のMySQL関数がいくつかあることがわかっているため、必要な関数は実装してPull Requestを継続的に提出することで貢献する予定です。

おわりに

以上の手順で、DB接続をphp-mysql-engineの提供するFakePdoに差し替えるだけで、データベースを利用したアプリケーションのテストを行うことができるようになりました。

まだまだ開発途上のライブラリで、戸惑う部分もあるかと思いますが、とても便利なので皆さんで改善させながら使っていきましょう。

icon
javakky
決済周りの改善を中心に働いている2021年入社エンジニア。その名の通りJavaが好きなことで有名(?)で、最近はScalaを使える部署へ入ったらしい。