Статьи

Отражение в PHP

Отражение обычно определяется как способность программы проверять себя и изменять свою логику во время выполнения. В менее технических терминах рефлексия просит объект рассказать вам о его свойствах и методах, и изменяет эти элементы (даже частные). В этом уроке мы разберемся, как это достигается и когда это может оказаться полезным.


На заре эры программирования появился язык ассемблера. Программа, написанная на ассемблере, находится на физических регистрах внутри компьютера. Его состав, методы и значения могут быть проверены в любое время путем чтения регистров. Более того, вы можете изменить программу во время ее работы, просто изменив эти регистры. Это потребовало некоторых глубоких знаний о работающей программе, но это было по сути своей рефлексивным.

Как с любой классной игрушкой, используйте отражение, но не злоупотребляйте им.

По мере появления языков программирования более высокого уровня (таких как 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».

Спасибо за прочтение!