Отражение обычно определяется как способность программы проверять себя и изменять свою логику во время выполнения. В менее технических терминах рефлексия просит объект рассказать вам о его свойствах и методах, и изменяет эти элементы (даже частные). В этом уроке мы разберемся, как это достигается и когда это может оказаться полезным.
Маленькая история
На заре эры программирования появился язык ассемблера. Программа, написанная на ассемблере, находится на физических регистрах внутри компьютера. Его состав, методы и значения могут быть проверены в любое время путем чтения регистров. Более того, вы можете изменить программу во время ее работы, просто изменив эти регистры. Это потребовало некоторых глубоких знаний о работающей программе, но это было по сути своей рефлексивным.
Как с любой классной игрушкой, используйте отражение, но не злоупотребляйте им.
По мере появления языков программирования более высокого уровня (таких как C) эта отражательная способность исчезала и исчезала. Позднее он был вновь представлен объектно-ориентированным программированием.
Сегодня большинство языков программирования могут использовать рефлексию. Языки со статической типизацией, такие как Java, практически не имеют проблем с отражением. Однако мне кажется интересным то, что любой язык с динамической типизацией (например, PHP или Ruby) в значительной степени основан на рефлексии. Без концепции рефлексии, скорее всего, невозможно будет ввести тип утки. Когда вы отправляете один объект другому (например, параметр), принимающий объект не имеет возможности узнать структуру и тип этого объекта. Все, что он может сделать — это использовать рефлексию для определения методов, которые можно и нельзя вызывать для полученного объекта.
Простой пример
Отражение распространено в PHP. На самом деле, есть несколько ситуаций, когда вы можете использовать его, даже не подозревая об этом. Например:
01
02
03
04
05
06
07
08
09
10
11
12
13
|
// Nettuts.php
require_once ‘Editor.php’;
class Nettuts {
function publishNextArticle() {
$editor = new Editor(‘John Doe’);
$editor->setNextArticle(‘135523’);
$editor->publish();
}
}
|
И:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
// Editor.php
class Editor {
private $name;
public $articleId;
function __construct($name) {
$this->name = $name;
}
public function setNextArticle($articleId) {
$this->articleId = $articleId;
}
public function publish() {
// publish logic goes here
return true;
}
}
|
В этом коде у нас есть прямой вызов локальной инициализированной переменной с известным типом. Создание редактора в publishNextArticle()
делает очевидным, что переменная $editor
имеет тип Editor
. Здесь не нужно никаких размышлений, но давайте представим новый класс с именем Manager
:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
// Manager.php
require_once ‘./Editor.php’;
require_once ‘./Nettuts.php’;
class Manager {
function doJobFor(DateTime $date) {
if ((new DateTime())->getTimestamp() > $date->getTimestamp()) {
$editor = new Editor(‘John Doe’);
$nettuts = new Nettuts();
$nettuts->publishNextArticle($editor);
}
}
}
|
Затем измените Nettuts
следующим образом:
01
02
03
04
05
06
07
08
09
10
|
// Nettuts.php
class Nettuts {
function publishNextArticle($editor) {
$editor->setNextArticle(‘135523’);
$editor->publish();
}
}
|
Теперь Nettuts
имеет абсолютно никакого отношения к классу Editor
. Он не включает свой файл, он не инициализирует свой класс и даже не знает о его существовании. Я мог бы передать объект любого типа в метод publishNextArticle()
и код будет работать.
Как видно из этой диаграммы классов, Nettuts
имеет прямое отношение только к Manager
. Manager
создает его, и, следовательно, Manager
зависит от Nettuts
. Но Nettuts
больше не имеет никакого отношения к классу Editor
, а Editor
связан только с Manager
.
Во время выполнения Nettuts
использует объект Editor
, то есть << использует >> и знак вопроса. Во время выполнения PHP проверяет полученный объект и проверяет, что он реализует setNextArticle()
и publish()
.
Информация об участнике объекта
Мы можем заставить PHP отображать детали объекта. Давайте создадим тест PHPUnit, который поможет нам легко использовать наш код:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
// ReflectionTest.php
require_once ‘../Editor.php’;
require_once ‘../Nettuts.php’;
class ReflectionTest extends PHPUnit_Framework_TestCase {
function testItCanReflect() {
$editor = new Editor(‘John Doe’);
$tuts = new Nettuts();
$tuts->publishNextArticle($editor);
}
}
|
Теперь добавьте var_dump()
в Nettuts
:
01
02
03
04
05
06
07
08
09
10
11
|
// Nettuts.php
class NetTuts {
function publishNextArticle($editor) {
$editor->setNextArticle(‘135523’);
$editor->publish();
var_dump(new ReflectionClass($editor));
}
}
|
Запустите тест и посмотрите, как волшебство происходит в выводе:
01
02
03
04
05
06
07
08
09
10
11
|
PHPUnit 3.6.11 by Sebastian Bergmann.
.object(ReflectionClass)#197 (1) {
[«name»]=>
string(6) «Editor»
}
Time: 0 seconds, Memory: 2.25Mb
OK (1 test, 0 assertions)
|
Наш класс отражения имеет свойство name
установленное в исходный тип переменной $editor
: Editor
, но это не так много информации. А как насчет методов Editor
?
01
02
03
04
05
06
07
08
09
10
11
12
13
|
// Nettuts.php
class Nettuts {
function publishNextArticle($editor) {
$editor->setNextArticle(‘135523’);
$editor->publish();
$reflector = new ReflectionClass($editor);
var_dump($reflector->getMethods());
}
}
|
В этом коде мы присваиваем экземпляр класса отражения переменной $reflector
чтобы мы могли теперь запускать его методы. ReflectionClass
предоставляет большой набор методов, которые можно использовать для получения информации об объекте. Одним из этих методов является getMethods()
, который возвращает массив, содержащий информацию о каждом методе.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
PHPUnit 3.6.11 by Sebastian Bergmann.
.array(3) {
[0]=>
&object(ReflectionMethod)#196 (2) {
[«name»]=>
string(11) «__construct»
[«class»]=>
string(6) «Editor»
}
[1]=>
&object(ReflectionMethod)#195 (2) {
[«name»]=>
string(14) «setNextArticle»
[«class»]=>
string(6) «Editor»
}
[2]=>
&object(ReflectionMethod)#194 (2) {
[«name»]=>
string(7) «publish»
[«class»]=>
string(6) «Editor»
}
}
Time: 0 seconds, Memory: 2.25Mb
OK (1 test, 0 assertions)
|
Другой метод, getProperties()
, извлекает свойства (даже частные свойства!) Объекта:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
|
PHPUnit 3.6.11 by Sebastian Bergmann.
.array(2) {
[0]=>
&object(ReflectionProperty)#196 (2) {
[«name»]=>
string(4) «name»
[«class»]=>
string(6) «Editor»
}
[1]=>
&object(ReflectionProperty)#195 (2) {
[«name»]=>
string(9) «articleId»
[«class»]=>
string(6) «Editor»
}
}
Time: 0 seconds, Memory: 2.25Mb
OK (1 test, 0 assertions)
|
Элементы в массивах, возвращаемые из getMethod()
и getProperties()
имеют тип ReflectionMethod
и ReflectionProperty
, соответственно; эти объекты довольно полезны:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
// Nettuts.php
class Nettuts {
function publishNextArticle($editor) {
$editor->setNextArticle(‘135523’);
$editor->publish();
$reflector = new ReflectionClass($editor);
$publishMethod = $reflector->getMethod(‘publish’);
$publishMethod->invoke($editor);
}
}
|
Здесь мы используем getMethod()
для извлечения одного метода с именем «publish»; Результатом которого является объект ReflectionMethod
. Затем мы вызываем метод invoke()
, передавая ему объект $editor
, для повторного выполнения метода publish()
редактора.
В нашем случае этот процесс был прост, потому что у нас уже был объект Editor
для передачи в invoke()
. В некоторых случаях у нас может быть несколько объектов Editor
, что позволяет нам выбирать, какой объект использовать. В других обстоятельствах у нас может не быть объектов для работы, и в этом случае нам потребуется получить объект из ReflectionClass
.
Давайте изменим метод publish()
Editor
чтобы продемонстрировать двойной вызов:
01
02
03
04
05
06
07
08
09
10
11
12
13
|
// Editor.php
class Editor {
[ … ]
public function publish() {
// publish logic goes here
echo («HERE\n»);
return true;
}
}
|
И новый вывод:
1
2
3
4
5
6
7
8
|
PHPUnit 3.6.11 by Sebastian Bergmann.
.HERE
HERE
Time: 0 seconds, Memory: 2.25Mb
OK (1 test, 0 assertions)
|
Управление данными экземпляра
Мы также можем изменить код во время выполнения. Как насчет изменения закрытой переменной, у которой нет открытого сеттера? Давайте добавим метод в Editor
который получает имя редактора:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
// Editor.php
class Editor {
private $name;
public $articleId;
function __construct($name) {
$this->name = $name;
}
[ … ]
function getEditorName() {
return $this->name;
}
}
|
Этот новый метод вызывается getEditorName()
и просто возвращает значение из закрытой переменной $name
. Переменная $name
устанавливается во время создания, и у нас нет открытых методов, позволяющих нам ее изменить. Но мы можем получить доступ к этой переменной, используя отражение. Вы могли бы сначала попробовать более очевидный подход:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
// Nettuts.php
class Nettuts {
function publishNextArticle($editor) {
var_dump($editor->getEditorName());
$reflector = new ReflectionClass($editor);
$editorName = $reflector->getProperty(‘name’);
$editorName->getValue($editor);
}
}
|
Несмотря на то, что это выводит значение в var_dump()
, оно выдает ошибку при попытке получить значение с отражением:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
PHPUnit 3.6.11 by Sebastian Bergmann.
Estring(8) «John Doe»
Time: 0 seconds, Memory: 2.50Mb
There was 1 error:
1) ReflectionTest::testItCanReflect
ReflectionException: Cannot access non-public member Editor::name
[…]/Reflection in PHP/Source/NetTuts.php:13
[…]/Reflection in PHP/Source/Tests/ReflectionTest.php:13
/usr/bin/phpunit:46
FAILURES!
Tests: 1, Assertions: 0, Errors: 1.
|
Чтобы решить эту проблему, нам нужно попросить объект ReflectionProperty
предоставить нам доступ к закрытым переменным и методам:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
// Nettuts.php
class Nettuts {
function publishNextArticle($editor) {
var_dump($editor->getEditorName());
$reflector = new ReflectionClass($editor);
$editorName = $reflector->getProperty(‘name’);
$editorName->setAccessible(true);
var_dump($editorName->getValue($editor));
}
}
|
Вызов setAccessible()
и передача true
делает свое дело:
1
2
3
4
5
6
7
8
9
|
PHPUnit 3.6.11 by Sebastian Bergmann.
.string(8) «John Doe»
string(8) «John Doe»
Time: 0 seconds, Memory: 2.25Mb
OK (1 test, 0 assertions)
|
Как видите, нам удалось прочитать приватную переменную. Первая строка выводится из собственного getEditorName()
объекта, а вторая — из отражения. Но как насчет изменения значения частной переменной? Используйте метод setValue()
:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
// Nettuts.php
class Nettuts {
function publishNextArticle($editor) {
var_dump($editor->getEditorName());
$reflector = new ReflectionClass($editor);
$editorName = $reflector->getProperty(‘name’);
$editorName->setAccessible(true);
$editorName->setValue($editor, ‘Mark Twain’);
var_dump($editorName->getValue($editor));
}
}
|
Вот и все. Этот код меняет «Джон Доу» на «Марк Твен».
1
2
3
4
5
6
7
8
9
|
PHPUnit 3.6.11 by Sebastian Bergmann.
.string(8) «John Doe»
string(10) «Mark Twain»
Time: 0 seconds, Memory: 2.25Mb
OK (1 test, 0 assertions)
|
Использование косвенного отражения
В некоторых встроенных функциях PHP косвенно используется отражение — одним из них является call_user_func()
.
Обратный звонок
Функция call_user_func()
принимает массив: первый элемент указывает на объект, а второй — на имя метода. Вы можете указать необязательный параметр, который затем передается вызываемому методу. Например:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
// Nettuts.php
class Nettuts {
function publishNextArticle($editor) {
var_dump($editor->getEditorName());
$reflector = new ReflectionClass($editor);
$editorName = $reflector->getProperty(‘name’);
$editorName->setAccessible(true);
$editorName->setValue($editor, ‘Mark Twain’);
var_dump($editorName->getValue($editor));
var_dump(call_user_func(array($editor, ‘getEditorName’)));
}
}
|
Следующий вывод демонстрирует, что код получает правильное значение:
01
02
03
04
05
06
07
08
09
10
|
PHPUnit 3.6.11 by Sebastian Bergmann.
.string(8) «John Doe»
string(10) «Mark Twain»
string(10) «Mark Twain»
Time: 0 seconds, Memory: 2.25Mb
OK (1 test, 0 assertions)
|
Использование значения переменной
Другим примером косвенного отражения является вызов метода по значению, содержащемуся в переменной, а не прямой вызов этого метода. Например:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
// Nettuts.php
class Nettuts {
function publishNextArticle($editor) {
var_dump($editor->getEditorName());
$reflector = new ReflectionClass($editor);
$editorName = $reflector->getProperty(‘name’);
$editorName->setAccessible(true);
$editorName->setValue($editor, ‘Mark Twain’);
var_dump($editorName->getValue($editor));
$methodName = ‘getEditorName’;
var_dump($editor->$methodName());
}
}
|
Этот код выдает тот же вывод, что и в предыдущем примере. PHP просто заменяет переменную на строку, которую она представляет, и вызывает метод. Это даже работает, когда вы хотите создать объекты, используя переменные для имен классов.
Когда мы должны использовать отражение?
Теперь, когда мы оставили технические детали позади, когда мы должны использовать рефлексию? Вот несколько сценариев:
- Динамическая типизация , вероятно, невозможна без рефлексии.
- Аспектно-ориентированное программирование слушает вызовы методов и размещает код вокруг методов, и все это выполняется с помощью отражения.
- PHPUnit сильно зависит от рефлексии, как и другие фреймворки.
- Веб-фреймворки обычно используют отражение для разных целей. Некоторые используют его для инициализации моделей, конструирования объектов для представлений и многого другого. Laravel активно использует рефлексию для внедрения зависимостей.
- Метапрограммирование , как и в нашем последнем примере, является скрытым отражением.
- Фреймворки анализа кода используют рефлексию для понимания вашего кода.
Последние мысли
Как с любой классной игрушкой, используйте отражение, но не злоупотребляйте им. Отражение обходится дорого, когда вы осматриваете много объектов, и оно может усложнить архитектуру и дизайн вашего проекта. Я рекомендую вам использовать его только тогда, когда это действительно дает вам преимущество, или когда у вас нет другого жизнеспособного варианта.
Лично я использовал отражение только в нескольких случаях, чаще всего при использовании сторонних модулей, в которых отсутствует документация. Я часто использую код, похожий на последний пример. Правильный метод легко вызвать, когда ваш MVC отвечает переменной, содержащей значения «add» или «remove».
Спасибо за прочтение!