PHP
отражение
Поиск…
Доступ к частным и защищенным переменным-членам
Отражение часто используется как часть тестирования программного обеспечения, например, для создания / создания экземпляров макета. Это также отлично подходит для проверки состояния объекта в любой момент времени. Ниже приведен пример использования Reflection в модульном тесте для проверки того, что защищенный член класса содержит ожидаемое значение.
Ниже представлен очень простой класс для автомобиля. Он имеет защищенную переменную-член, которая будет содержать значение, представляющее цвет автомобиля. Поскольку переменная-член защищена, мы не можем получить к ней доступ напрямую и должны использовать метод getter и setter для извлечения и установки его значения соответственно.
class Car
{
protected $color
public function setColor($color)
{
$this->color = $color;
}
public function getColor($color)
{
return $this->color;
}
}
Чтобы проверить это, многие разработчики создадут объект «Автомобиль», установите цвет автомобиля с помощью Car::setColor()
, извлеките цвет с помощью Car::getColor()
и сравните это значение с установленным цветом:
/**
* @test
* @covers \Car::setColor
*/
public function testSetColor()
{
$color = 'Red';
$car = new \Car();
$car->setColor($color);
$getColor = $car->getColor();
$this->assertEquals($color, $reflectionColor);
}
На первый взгляд это выглядит хорошо. В конце концов, все Car::getColor()
возвращает значение защищенной переменной элемента Car::$color
. Но этот тест испорчен двумя способами:
- Он использует
Car::getColor()
который выходит за рамки этого теста - Это зависит от
Car::getColor()
который может иметь ошибку, которая может сделать тест ложным положительным или отрицательным
Давайте посмотрим, почему мы не должны использовать Car::getColor()
в нашем модульном тесте и вместо этого должны использовать Reflection. Предположим, разработчику назначена задача добавить «Металлик» в каждый цвет автомобиля. Поэтому они пытаются модифицировать Car::getColor()
чтобы добавить «Metallic» к цвету автомобиля:
class Car
{
protected $color
public function setColor($color)
{
$this->color = $color;
}
public function getColor($color)
{
return "Metallic "; $this->color;
}
}
Вы видите ошибку? Разработчик использовал вместо запятой оператор с запятой, пытаясь добавить цвет «Металлик» к цвету автомобиля. В результате, когда вызывается Car::getColor()
, «Metallic» будет возвращен независимо от фактического цвета автомобиля. В результате наш Car::setColor()
тест Car::setColor()
завершится неудачно, даже если Car::setColor()
работает отлично и не повлиял на это изменение .
Итак, как мы можем подтвердить, что Car::$color
содержит значение, которое мы устанавливаем через Car::setColor()
? Мы можем использовать Refelection для непосредственного контроля защищенной переменной-члена. Так как же нам делать? Мы можем использовать Refelection, чтобы сделать защищенную переменную-член доступной для нашего кода, чтобы она могла получить значение.
Сначала посмотрим на код, а затем сломаем его:
/**
* @test
* @covers \Car::setColor
*/
public function testSetColor()
{
$color = 'Red';
$car = new \Car();
$car->setColor($color);
$reflectionOfCar = new \ReflectionObject($car);
$protectedColor = $reflectionOfForm->getProperty('color');
$protectedColor->setAccessible(true);
$reflectionColor = $protectedColor->getValue($car);
$this->assertEquals($color, $reflectionColor);
}
Вот как мы используем Reflection для получения значения Car::$color
в приведенном выше коде:
- Мы создаем новый объект ReflectionObject, представляющий наш объект Car
- Мы получаем ReflectionProperty для
Car::$color
(это «представляет» переменнуюCar::$color
) - Мы делаем доступным доступный
Car::$color
- Мы получаем значение
Car::$color
Как вы можете видеть, используя Reflection, мы можем получить значение Car::$color
без необходимости вызова Car::getColor()
или любой другой функции доступа, которая может привести к недействительным результатам теста. Теперь наш блок-тест для Car::setColor()
является безопасным и точным.
Определение функций классов или объектов
Функция обнаружения классов может частично выполняться с функциями property_exists
и method_exists
.
class MyClass {
public $public_field;
protected $protected_field;
private $private_field;
static $static_field;
const CONSTANT = 0;
public function public_function() {}
protected function protected_function() {}
private function private_function() {}
static function static_function() {}
}
// check properties
$check = property_exists('MyClass', 'public_field'); // true
$check = property_exists('MyClass', 'protected_field'); // true
$check = property_exists('MyClass', 'private_field'); // true, as of PHP 5.3.0
$check = property_exists('MyClass', 'static_field'); // true
$check = property_exists('MyClass', 'other_field'); // false
// check methods
$check = method_exists('MyClass', 'public_function'); // true
$check = method_exists('MyClass', 'protected_function'); // true
$check = method_exists('MyClass', 'private_function'); // true
$check = method_exists('MyClass', 'static_function'); // true
// however...
$check = property_exists('MyClass', 'CONSTANT'); // false
$check = property_exists($object, 'CONSTANT'); // false
С ReflectionClass
также могут быть обнаружены константы:
$r = new ReflectionClass('MyClass');
$check = $r->hasProperty('public_field'); // true
$check = $r->hasMethod('public_function'); // true
$check = $r->hasConstant('CONSTANT'); // true
// also works for protected, private and/or static members.
Примечание: для property_exists
и method_exists
, также является объект класса интереса может быть предоставлен вместо имени класса. С помощью отражения, то ReflectionObject
класс следует использовать вместо ReflectionClass
.
Тестирование частных / защищенных методов
Иногда полезно тестировать частные и защищенные методы, а также публичные.
class Car
{
/**
* @param mixed $argument
*
* @return mixed
*/
protected function drive($argument)
{
return $argument;
}
/**
* @return bool
*/
private static function stop()
{
return true;
}
}
Самый простой способ тестирования метода диска - использовать рефлексию
class DriveTest
{
/**
* @test
*/
public function testDrive()
{
// prepare
$argument = 1;
$expected = $argument;
$car = new \Car();
$reflection = new ReflectionClass(\Car::class);
$method = $reflection->getMethod('drive');
$method->setAccessible(true);
// invoke logic
$result = $method->invokeArgs($car, [$argument]);
// test
$this->assertEquals($expected, $result);
}
}
Если метод является статическим, вы передаете null вместо экземпляра класса
class StopTest
{
/**
* @test
*/
public function testStop()
{
// prepare
$expected = true;
$reflection = new ReflectionClass(\Car::class);
$method = $reflection->getMethod('stop');
$method->setAccessible(true);
// invoke logic
$result = $method->invoke(null);
// test
$this->assertEquals($expected, $result);
}
}