#php #unit-testing #reflection #reference
#php #модульное тестирование #отражение #ссылка
Вопрос:
Мне нужно было проверить, может ли мой абстрактный класс корректно возвращать ссылку на внутренний массив из защищенного метода. Единственный способ протестировать защищенный метод — через отражение. Однако, похоже, невозможно получить ссылку, возвращаемую ReflectionMethod::invokeArgs()
. Или я делаю здесь что-то не так?
class A
{
protected $items;
protected function amp;_getItemStorage()
{
return $this->items;
}
}
function amp;reflectionInvokeByRef($object, $method, $args = array())
{
$reflection = new ReflectionMethod($object, $method);
$reflection->setAccessible(true);
$result = amp;$reflection->invokeArgs($object, $args);
return $result;
}
$a = new A();
$items = amp;reflectionInvokeByRef($a, '_getItemStorage');
Это приводит к следующей ошибке:
Only variables should be assigned by reference
On line: $result = amp;$reflection->invokeArgs($object, $args);
Эта ошибка обычно возникает при попытке присвоить возвращаемое значение функции по ссылке, тогда как функция не объявлена как возвращаемая по ссылке. Делает мою функциональность недоступной для тестирования с использованием любого известного метода.
Комментарии:
1. Я думаю, вам просто нужно вызвать $reflection без побитового, поскольку вы уже возвращаете ссылку в своих функциях. Можете ли вы вставить код для функции invokeArgs?
2. Я не могу, потому что функция находится на C. Но в начале вопроса есть ссылка на документы этой функции. В этих документах также указано, что вам необходимо включить
amp;
, который, кстати, не является побитовым оператором, а означает «ссылка». В этом случае это указывает на то, что я присваиваю ссылку, а не значение ссылки.
Ответ №1:
tl; dr
Используется ReflectionMethod::getClosure()
для извлечения замыкания, представляющего метод. Если это замыкание вызывается напрямую, оно правильно вернет ссылку, если это было поведение исходного метода.
Это возможно только для PHP версии 5.4 или более поздней.
Об этом нет упоминания в руководстве по PHP, но ReflectionMethod::invokeArgs()
он не способен возвращать ссылку.
Единственный способ получить возвращаемую ссылку с помощью отражения — использовать ReflectionMethod::getClosure()
, который возвращает Closure
экземпляр, представляющий исходный метод. При прямом вызове это замыкание правильно вернет ссылку, если это было поведение исходного метода.
Я говорю «если вызывается напрямую», поскольку call_user_func_array()
функция также не способна возвращать ссылки, поэтому вы не можете использовать эту функцию для вызова закрытия с массивом аргументов.
Обратите внимание, что этот ReflectionMethod::getClosure()
метод доступен только для PHP версии 5.4 или более поздней.
PHP 5.6
Вы можете передать массив аргументов в закрытие, просто используя распаковку аргументов:
<?php
function amp;reflectionInvokeByRef($object, $method, $args = array())
{
$reflection = new ReflectionMethod($object, $method);
$reflection->setAccessible(true);
$closure = $reflection->getClosure($object);
$result = amp;$closure(...$args);
return $result;
}
$a = new A();
$items = amp;reflectionInvokeByRef($a, '_getItemStorage');
$items[] = 'yolo';
var_dump($a);
Это приведет к
object(A)#1 (1) {
["items":protected]=> amp;array(1) {
[0]=> string(4) "yolo"
}
}
PHP 5.4 — 5.5
Распаковка аргументов недоступна, поэтому передача массива аргументов должна выполняться по-другому.
Используя тот факт, что функциям и методам PHP может быть предоставлено больше аргументов, чем ожидается (т. Е. Больше, Чем количество объявленных параметров), мы можем увеличить массив аргументов до заданного максимума (например, 10) и передать аргументы традиционным способом.
<?php
function amp;reflectionInvokeByRef($object, $method, $args = array())
{
$reflection = new ReflectionMethod($object, $method);
$reflection->setAccessible(true);
$args = array_pad($args, 10, null);
$closure = $reflection->getClosure($object);
$result = amp;$closure($args[0], $args[1], $args[2], $args[3], $args[4], $args[5],
$args[6], $args[7], $args[8], $args[9]);
return $result;
}
$a = new A();
$items = amp;reflectionInvokeByRef($a, '_getItemStorage', array('some', 'args'));
$items[] = 'yolo';
var_dump($a);
Это приведет к
object(A)#1 (1) {
["items":protected]=> amp;array(1) {
[0]=> string(4) "yolo"
}
}
Признаю, это некрасиво. Это грязное решение, и я бы не стал публиковать его здесь, если бы был какой-либо другой способ (который я мог придумать). Вам также нужно будет выбрать максимальное количество аргументов для заполнения массива arguments, но я думаю, что 10 — это безопасный максимум.
Комментарии:
1. Я думал об этом раньше. Однако я предположил, что это не сработает, поскольку функция должна быть определена как возвращаемая по ссылке, и закрытие from
#getClosure()
, скорее всего, не будет определено таким образом. Но если мое предположение неверно, действительно ли приведенный выше код работает? Что произойдет, если вы добавите$items = array('apple'); var_dump($a);
ниже?2. Я бы подумал, что оператор присваивания изменит переменную, чтобы указать на новый литеральный массив, но после тестирования кажется, что экземпляр
A
фактически модифицировал свой внутренний$items
массив, чтобы он был равенarray (size=1) 0 => string 'apples'
. Честно говоря, я ожидал не совсем такого поведения.3. Тем не менее, именно то, что я ожидаю и в чем нуждаюсь. Спасибо, я скоро протестирую это.
4. Кроме того, может случиться так, что замыкание, возвращаемое из
$reflection
экземпляра, является не фактическимClosure
, которое обтекает метод, а фактической ссылкой на сам метод. В любом случае класс отражения имеетreturnsReference()
метод, который позволяет ему узнать природу возвращаемого значения. Таким образом, даже если закрытие является оболочкой, оно, вероятно, построено с учетом возврата ссылки.5. Вероятно, это объяснение. Похоже, я не должен был сдаваться так скоро. Еще раз спасибо.