はじめましての方ははじめまして、アルバイトとして働いているJavakkyです。
本記事では、データベースを利用するアプリケーションのテストに利用できるphp-mysql-engineというライブラリの導入方法と使用感について紹介します。
php-mysql-engineとは
php-mysql-engineは動画共有サイトを運営するVimeo社が開発しています。このライブラリの目論見はVimeo Engineering Blogで説明されています。
このライブラリはVimeo社のMatt Brownさんが二年前に開発していたhacktophpを用いて、Slack社がHackで実装したslackhq/hack-sql-fakeをPHPコードに変換したものを下敷きにしています。
開発者のMatt Brownさんは、静的解析でVimeo社のコードを改善するためにPsalmを開発したことでも知られています。
php-mysql-engineが提供するのはMartinFowler.comで紹介されているbliki: InMemoryTestDatabaseに相当する機能ですが、あくまで本物のMySQLではないので、このライブラリに依存するかどうかはプロジェクトごとに慎重に判断する必要があります。php-mysql-engineの説明では以下の記事にも言及されています。
対象環境
今回の対象はピクシブ百科事典のコードベースです。このプロダクトは独自フレームワークで構築されており、PDOを独自にラップしたクラスによってMySQLに接続する機能を提供しています。
ピクシブ百科事典の開発ではPHPUnitとPHPStanによるCIは既に稼動しています。
なお、ピクシブ百科事典ではなく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
など)でDbManager
にPdoFactory
を登録します。その後、利用するテーブルそれぞれについて 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がマージされました。
現在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;
そして、標準の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 TABLE
にDEFAULT 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のFakePdo
はPDO
クラスを継承してはいますが、本来の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がマージされたので、次回リリースに含まれると思われます。
既知の問題
ピクシブ百科事典で利用しているクエリの全てをphp-mysql-engineで実装するには未実装のMySQL関数がいくつかあることがわかっているため、必要な関数は実装してPull Requestを継続的に提出することで貢献する予定です。
おわりに
以上の手順で、DB接続をphp-mysql-engineの提供するFakePdo
に差し替えるだけで、データベースを利用したアプリケーションのテストを行うことができるようになりました。
まだまだ開発途上のライブラリで、戸惑う部分もあるかと思いますが、とても便利なので皆さんで改善させながら使っていきましょう。