はじめに
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だ。
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のコードを見てみる。以下のリポジトリである。
実際利用しているFluentPDOのバージョンは1系なのだが、2系でもnamespaceやディレクトリ構造が変わっただけで、コード自体はそんなに変わっていない。 src/Queries/Common.php
を見てみると、
public function __call($name, $parameters = [])
こういった定義になっている。
ここで試しに vendor ディレクトリ以下をいじって $parameters = []
の部分を単に $parameters
、つまりデフォルト引数なしにしてみると、エラーは出なくなりテストが通った。ここの定義の際があかんらしい。
どうするか
FluentPDO最新バージョンでもこの実装になっているように、ここは治す気は無いようだ。実装的にはMockeryの思想が正しい気もするが、非常に微妙なラインである。
__call()
をテストしたい場面もあるはずなので何か対策はあるはず!ということで次回へ続く。