Open14

Mockeryのバグ調査してみる(Mockeryのバグではなく、PHPの関数の仕様説)

wadakatuwadakatu

失敗するテストケース

<?php
declare(strict_types=1);

namespace Tests\Unit;

use Mockery;
use Tests\TestCase;

class Foo
{
    public function bar($a, ...$b)
    {
        return [$a, $b];
    }
}

class mockeryBugTest extends TestCase
{
    /** @test */
    public function a_test()
    {
        $mock = Mockery::mock(Foo::class)->makePartial();

        $res = $mock->bar('abc', ...['def' => 'hjk']);

        self::assertEquals('abc', $res[0]);
        self::assertEquals(['def' => 'hjk'], $res[1]);
    }
}

本来であれば、['def' => 'hjk']が返るはずなのに、なぜか空配列が入ってくる

self::assertEquals(['def' => 'hjk'], $res[1]);
wadakatuwadakatu

makePartial()のメソッドは以下の通り

library/Mockery/Mock.php
public function makePartial()
    {
        $this->_mockery_deferMissing = true;
        return $this;
    }

_mockery_deferMissingのプロパティをtrueに変更している
そして、Mockインスタンスを返却している

wadakatuwadakatu

そもそも_mockery_deferMissingプロパティとはなんなのか

   /**
     * Flag to indicate whether we can defer method calls missing from our
     * expectations
     *
     * @var bool
     */
    protected $_mockery_deferMissing = false;

PHPDocのコメントを日本語訳すると。。
期待から外れたメソッドコールを延期できるかどうかを示すフラグ

期待から外れたメソッドというのは、shouldReceive()などで振る舞いが指定されていないメソッドのこと
延期できるかというのは、振る舞いが指定されていないメソッドをそのまま動作させるかどうかだと思う

class Foo
{
    public function foo($a)
    {
        return $a;
    }

    public function bar($a, ...$b)
    {
        return [$a, $b];
    }
}

$foo = Mockery::mock($foo)->makePartial();

$foo->shouldReceive('foo')->andReturn('Hello World.')

//期待されているメソッドコール(振る舞いが指定されている)
//異なる引数で何回呼び出しても、`Hello World.`の文字列が返却される
$foo->foo('aaa');
//期待から外れたメソッドコール(振る舞いが指定されていない)
//引数が異なれば、返却される値も変わる
$foo->bar('aa' ['bb' => 'cc']);
wadakatuwadakatu

_mockery_deferMissingがtrueの時に影響を受ける箇所を洗い出す
該当箇所は2箇所

library/Mockery/Mock.php 901
 } elseif ($this->_mockery_deferMissing && is_callable(get_parent_class($this) . '::' . $method)
            && (!$this->hasMethodOverloadingInParentClass() || (get_parent_class($this) && method_exists(get_parent_class($this), $method)))) {
            return call_user_func_array(get_parent_class($this) . '::' . $method, $args);
        } 
library/Mockery/Mock.php 904
 } elseif ($this->_mockery_deferMissing && get_parent_class($this) && method_exists(get_parent_class($this), '__call')) {
            return call_user_func(get_parent_class($this) . '::__call', $method, $args);
wadakatuwadakatu
library/Mockery/Mock.php 901
 } elseif ($this->_mockery_deferMissing && is_callable("parent::$method")
            && (!$this->hasMethodOverloadingInParentClass() || (get_parent_class($this) && method_exists(get_parent_class($this), $method)))) {
            return call_user_func_array("parent::$method", $args);
        }

この中に処理が入ってきてそう。

wadakatuwadakatu

この中で、dd($method, $args)をしてみる。
本来であれば、以下のような結果が返ってくるはず。

"bar" // $method
array:1 [ //$args
  0 => "abc" 
  1 => array:1 [
    "def" => "hjk"
  ]
]

実際の結果

"bar" // $method
array:1 [ //$args
  0 => "abc" 
]

ふむ。
この時点で、第二引数が消し去られている。
てことは、makePartial()関係ない説・・・?

wadakatuwadakatu

さっきのelseifが記述されているのは、以下のメソッド

protected function _mockery_handleMethodCall($method, array $args)

このメソッドに入ってきた時点で、第二引数がなくなっている。

wadakatuwadakatu

_mockery_handleMethodCallは、Mockery側で作成されたbarメソッドの中で呼ばれる

public function bar($a, ...$b){
    $argc = func_num_args(); // 関数に渡された関数の数
    $argv = func_get_args(); //関数に渡された関数の中身
    $ret = $this->_mockery_handleMethodCall(__FUNCTION__, $argv); // 関数実行
    return $ret;
}

func_num_argsfunc_get_argsの結果を見てみると、以下のようになった。

1 // func_num_args()
array:1 [ // func_get_args()
  0 => "abc"
]

引数には2個渡している('abc', ['def' => 'hjk'])のに、1個になっている。
何かがおかしい・・・

wadakatuwadakatu

Mockeryなしでfunc_num_argsfunc_get_argsの結果を見てみると、Mockeryありの場合と同じになった。

1 // func_num_args()
array:1 [ // func_get_args()
  0 => "abc"
]
``