1円でも得したいWebエンジニアの日常

クーポンだったりクレジットカードのポイントだったりを利用して1円でも得しつつ生活を便利にしていきたいWebエンジニアによるブログ。技術的な記事から商品レビューなど日常的なことまで。

Mockeryでモックしようとしたときに Mockery_XXX::__call($method, $args) should be compatible with ... と言われる

f:id:yoshiki_utakata:20181004212834j:plain

はじめに

PHPのモックフレームワーク「Mockery」を利用してクラスをモックしようとした際に以下のエラーが出た。

Declaration of Mockery_3_SelectQuery::__call($method, $args) should be compatible with CommonQuery::__call($name, $parameters = Array)

FluentPDOというライブラリのバージョン1系を利用しているのだが、その中のCommonQueryというclassに__callというマジックメソッドが定義されていた。MockeryでCommonQueryクラスをモックしようとしたときに、Mockertyが__callメソッドを上書きしようとして失敗しているようだ。どうにか回避できないとテストで困る。

MockeryとFluentPDOの実装を追ってみる。

Mockery側の実装を見てみる。エラーが出ている箇所はこういうコードだ

\Mockery::mock(CommonQuery::class);

Mockeryのリポジトリは以下。利用しているMockeryのバージョンは1.2.0だ。

github.com

mockメソッドはlibrary/Mockery.phpに定義されていて、Static shortcut to \Mockery\Container::mock(). とコメントがある。非常に親切なコメントで見習いたい。

ということで、library/Mockery/Container.php を見ていく。エラーのスタックトレースを見ると、 Container.php:224 224行目でエラーが出ている。

224: $this->getLoader()->load($def);

getLoader() で帰ってくるLoaderの中身だが、Containerのコンストラクタで $this->_loader = $loader ?: \Mockery::getDefaultLoader(); としている。中身は return new EvalLoader() となっている。

エラーのスタックトレースを見ると、 EvalLoader.php:34 34行目でエラーが出ているらしい。library/Mockery/Loader/EvalLoader.php を見てみる。

eval("?>" . $definition->getCode());

ここでクラスを定義しようとして死んでいるっぽい。Containerの$def = $this->getGenerator()->generate($config); ここのほうが重要っぽいのでGeneratorを見てみる。generatorは

new CachingGenerator(StringManipulationGenerator::withDefaultPasses());

これが入っているようだ。library/Mockery/Generator/CachingGenerator.php これである。中身を見ると結局 StringManipulationGenerator::withDefaultPasses() が重要に見える。また、generatorに渡されている $config も怪しい。

   /**
     * Creates a new StringManipulationGenerator with the default passes
     *
     * @return StringManipulationGenerator
     */
    public static function withDefaultPasses()
    {
        return new static([
            new CallTypeHintPass(),
            new MagicMethodTypeHintsPass(),
            new ClassPass(),
            new TraitPass(),
            new ClassNamePass(),
            new InstanceMockPass(),
            new InterfacePass(),
            new MethodDefinitionPass(),
            new RemoveUnserializeForInternalSerializableClassesPass(),
            new RemoveBuiltinMethodsThatAreFinalPass(),
            new RemoveDestructorPass(),
            new ConstantsPass(),
        ]);
    }

ここで __call で検索してみると、 tests/Mockery/Generator/StringManipulation/Pass/CallTypeHintPassTest.php というのがヒットして、いかにもっぽいテストをしている。このCallTypeHintPassをここで利用しているのだ。

    const CODE = ' public function __call($method, array $args) {}
                   public static function __callStatic($method, array $args) {}

これを見ると、 __call($method, array $args) こういうメソッド定義を想定している。FluentPDOのコードを見てみる。以下のリポジトリである。

github.com

実際利用しているFluentPDOのバージョンは1系なのだが、2系でもnamespaceやディレクトリ構造が変わっただけで、コード自体はそんなに変わっていない。 src/Queries/Common.php を見てみると、

public function __call($name, $parameters = [])

こういった定義になっている。

ここで試しに vendor ディレクトリ以下をいじって $parameters = [] の部分を単に $parameters 、つまりデフォルト引数なしにしてみると、エラーは出なくなりテストが通った。ここの定義の際があかんらしい。

どうするか

FluentPDO最新バージョンでもこの実装になっているように、ここは治す気は無いようだ。実装的にはMockeryの思想が正しい気もするが、非常に微妙なラインである。

__call() をテストしたい場面もあるはずなので何か対策はあるはず!ということで次回へ続く。