もがき系プログラマの日常

もがき系エンジニアの勉強したこと、日常のこと、気になっている技術、備忘録などを紹介するブログです。

cakephp3とphp-mysql-engineの組み合わせはうまく行かない

はじめに

こんばんは。

php-mysql-engineの話を聞いて、ワクワクしながら試してみました。

結果、cakephp3だとうまく行かないことがわかりました。

検証した環境は、 3.9.8 でした。

本題

結果的にうまく行かなかったのですが、ハマった箇所は色んな場所でした。

最初は ドライバを用意して、FakePDOに書き換えるだけでイケルと思いましたが、甘かったです。

最初に用意したドライバは、以下です。

<?php
namespace App\Test\Driver;

use Cake\Database\Driver\Mysql;
use Vimeo\MysqlEngine\Php7\FakePdo;

class TestMysqlDriver extends Mysql
{
    /**
     * Establishes a connection to the database server
     *
     * @param  string $dsn    A Driver-specific PDO-DSN
     * @param  array  $config configuration to be used for creating connection
     * @return bool   true on success
     */
    protected function _connect($dsn, array $config)
    {
        $connection = new FakePdo(
            $dsn,
            $config['username'],
            $config['password'],
            $config['flags']
        );
        $this->setConnection($connection);

        return true;
    }
}

無理やりクリアしたエラー

1. Exception: The SQL code SET foreign_key_checks = 0 could not be parsed: Unexpected SET in [/path/to/vendor/vimeo/php-mysql-engine/src/FakePdoStatementTrait.php, line 133]

これは FixtureManager.phpload() を実行したした際に内部で実行される _runOperation() の中の以下の

disableConstraints() が実行しているSQLです。

    /**
     * {@inheritDoc}
     *
     * ### Example:
     *
     * $connection->disableConstraints(function ($connection) {
     *   $connection->newQuery()->delete('users')->execute();
     * });
     */
    public function disableConstraints(callable $operation)
    {
        return $this->getDisconnectRetry()->run(function () use ($operation) {
            $this->disableForeignKeys();

            try {
                $result = $operation($this);
            } catch (Exception $e) {
                $this->enableForeignKeys();
                throw $e;
            }

            $this->enableForeignKeys();

            return $result;
        });
    }

外部キー制約のチェックを外すというSQLです。

幸い実行しているのが MysqlDriverが use している MysqlDialectTrait というTraitクラスだったので、 MysqlDriverをextendsしている今回の独自ドライバで上書き可能でした。

なので一旦、用意したドライバで、通りそうなSQLを用意してお茶を濁しました。

    /**
     * {@inheritDoc}
     */
    public function disableForeignKeySQL()
    {
        return 'SELECT "1"';
    }

2. Exception: The SQL code SET foreign_key_checks = 1 could not be parsed: Unexpected SET in [/path/to/vendor/vimeo/php-mysql-engine/src/FakePdoStatementTrait.php, line 133]

これも同じく、 disableConstraints() が内部で実行しているSQLです。

さっきの逆ですね。

これも同じようななんでもないSQLをドライバで用意しました。

    /**
     * {@inheritDoc}
     */
    public function enableForeignKeySQL()
    {
        return 'SELECT "1"';
    }

3. Exception: The SQL code SHOW TABLES FROM example_database could not be parsed: Parser error: expected SHOW TABLES LIKE in [/path/to/vendor/vimeo/php-mysql-engine/src/FakePdoStatementTrait.php, line 133]

php-mysql-engineでは SWOW TABLESで全テーブル取得を許可していないようです。 必ずLIKEで絞ることが条件になっていました。

しかし、 FixtureManager.phpload() で呼ばれている listTables() が SHOW TABLES を実行していました...

これは、 実際には vendor/cakephp/cakephp/src/Database/Schema/Collection.php が実行しています。

そしてMysqlの場合は MysqlSchema が該当のSQLを持っています。

    /**
     * {@inheritDoc}
     */
    public function listTablesSql($config)
    {
        return ['SHOW TABLES FROM ' . $this->_driver->quoteIdentifier($config['database']), []];
    }

MysqlSchemaMysqlDialectTraitschemaDialect() というメソッドで _schemaDialect というプロパティが何もセットされてない場合、初期値としてセットされ、以降それが使用されます。

なので、作成した新たなドライバのコンストラクタで 事前に _schemaDialect プロパティに独自のSchemaクラスをセットすることで回避できました。

    /**
     * Constructor
     *
     * @param  array                     $config The configuration for the driver.
     * @throws \InvalidArgumentException
     */
    public function __construct($config = [])
    {
        parent::__construct($config);
        $this->_schemaDialect = new TestMysqlSchema($this);
    }
<?php
namespace App\Test\Driver;

use Cake\Database\Schema\MysqlSchema;
use Cake\Database\Schema\TableSchema;

class TestMysqlSchema extends MysqlSchema
{
    /**
     * {@inheritDoc}
     */
    public function listTablesSql($config)
    {
        return ['SHOW TABLES LIKE "%%"', []];
    }
}

4. Warning Error: PDO::getAttribute(): SQLSTATE[00000]: No error: PDO constructor was not called in [/path/to/vendor/cakephp/cakephp/src/Database/Driver/Mysql.php, line 174]

PDO::ATTR_SERVER_VERSION を実行する部分で、エラーが警告が起きていました。

        if ($this->_version === null) {
            $this->_version = $this->_connection->getAttribute(PDO::ATTR_SERVER_VERSION);
        }

ここも versionを設定しておけばとりあえず回避できると思い、 先ほど作成した コンストラクタで $this->_version = 5.7 という感じで決め打ちで指定しました。

5. Warning Error: Fixture creation for "users" failed "No default collation or character set given" in [path/to/vendor/cakephp/cakephp/src/TestSuite/Fixture/TestFixture.php, line 312]

collationやcharacter setの指定が create table文にないことで警告が出ており、その後のテーブル作成が失敗していました。

charsetのオプションがfixtureクラスにあれば、このエラーは出ないと思いますが、試したプロジェクトの場合は全fixtureになかったので、全部のテストで同じエラーがでました。

今回の場合は、とりあえず utf8 に設定できればいいので、TestMysqlSchemaで createTableSql() をオーバライドして対応しました。

    /**
     * {@inheritDoc}
     */
    public function createTableSql(TableSchema $schema, $columns, $constraints, $indexes)
    {
        $contents = parent::createTableSql($schema, $columns, $constraints, $indexes);
        $contents[0] .= ' DEFAULT CHARACTER SET = utf8';

        return $contents;
    }

無理矢理は無理だったけど、なんとかなりそうなエラー

1. Warning Error: Fixture creation for "users" failed "Column type INTEGER not recognized" in [/path/to/vendor/cakephp/cakephp/src/TestSuite/Fixture/TestFixture.php, line 312]

create table文でint型のカラムがあるばあい、intは通るけどintegerは通らないという感じでした。

これに関しては、PRだしてみました。

github.com

もし通らなかったら、先程オーバライドした createTableSql 内で置換する感じで対応はできそうです。

どうにもならなかったエラー

1. UnexpectedValueException: The SQL code SHOW FULL COLUMNS FROM users could not be parsed: Parser error: expected SHOW TABLES

cakephpでは、find()やselect()を実行したときに、 内部でgetSchemaが呼ばれて、上記の SHOW FULL COLUMNSが実行されます。

php-mysql-engineは SHOW FULL COLUMNS も SHOW COLUMNSも非対応なので、これはどうにもなりませんでした...

終わりに

ワクワクしながら頑張ったのですが、結局ダメでした。。。

参考サイトのスライドでは cakephp4.2.4を使っているとのことなので、4系ではうまくいくのかもしれません 。

laravelとかはどうなんだろう?試してみようかな。

現場からは以上です。