20 февр. 2011 г.

Магические методы в PHP5

В PHP5 существуют следующие основные магические методы:
  • __construct () — конструктор класса. Срабатывает при создании объекта.
  • __destruct () — деструктор класса. Срабатывает при удалении объекта.
  • __clone () — срабатывает при клонировании объекта.
  • __sleep () — срабатывает при сериализации объекта.
  • __wakeup () — срабатывает при десериализации объекта.
  • __toString () — задает поведение класса при конвертировании его в строку.
  • __set_state () — статический метод срабатывает при вызове var_export().
Методы-перехватчики:
  • __get ( $property ) — перехватчик запроса к не существующему свойству.
    • __isset ( $property ) — перехватчик запроса на вызове метода isset() для не существующего свойства.
  • __set ( $property, $value ) — перехватчик запроса на присвоение некоторого значения не существующему свойству.
    • __unset ( $property ) — перехватчик запроса на вызов метода unset() для не существующего свойства.
  • __call ( $method, $arg_array ) — перехватчик запроса на вызов не существующего метода.
  • __callStatic () — перехватчик запроса на вызов не существующего статического метода(появился в PHP 5.3).
  • __invoke () — срабатывает при попытке вызова объекта как функции (появился в PHP 5.3).

 

Конструктор __construct()

Если в теле класса задан конструктор, то при создании объекта он будет вызван в первую очередь. Использование конструктора позволяет производить различные необходимые подготовительные действия и настройки, которые обязательно нужно выполнить при создании объекта. Магический метод __construct() появился в PHP5. В версии PHP4 конструктор задавался путем задания имени метода совпадающим с именем класса.
ЗАМЕЧАНИЕ:
Версия PHP4 не понимает магический метод __construct(). Поэтому при написании кода, который должен быть совместим с PHP4 необходимо методу конструктору давать имя совпадающее с именем класса. В остальных случаях, если вы пишете современное приложение под PHP5, рекомендуется использовать __construct(), хотя PHP5 поймет и старый вариант задания конструктора.

Деструктор __destruct()

Если задан деструктор, то он вызывается при удалении объекта из памяти программы сборщиком мусора. Основное предназначение деструктора заключается в обеспечении выполнения дополнительных операций при удалении ненужного объекта. Также деструктор вызывается при ручном удалении объекта c помощью метода unset().

Клонирование __clone()

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

Более подробнее про клонирование объектов и различия реализации в версиях PHP4 и PHP5 рассказывается в статье " Клонирование объектов в PHP5 ".

Методы-перехватчики (interseptors)

PHP предоставляет встроенные методы, которые могут перехватывать запросы к не существующим свойствам и методам класса. Такое поведение в других языках, например Java и С++, называется перегрузкой свойств и методов (overloading). Но в PHP перегрузка свойств и методов отличается от перегрузки в Java и С++. Применительно к PHP это поведение лучше охарактеризовать как перехват (interception). PHP5 поддерживает три основных метода-перехватчика и два дополнительных:
  • __get ()
    • __isset ()
  • __set ()
    • __unset ()
  • __call ()
Также как и конструктор класса эти методы срабатывают только в определенных условиях.
Ниже приводятся подробные описания каждого метода-перехватчика.

Метод __get()

Магический метод __get() срабатывает при попытке прочесть значение не существующего свойства класса. Он вызывается автоматически с одним строковым аргументом, содержащим название не существующего свойства, к которому попытался обратиться клиент. То что разработчик заложил для возврата клиенту в теле метода __get(), то и будет возвращено обратно клиенту, как-будто запрашиваемое им свойство существует.

Ниже приведен небольшой пример:
01class someClass
02{
03 function __get( $property )
04 {
05 $method = "get{$property}";
06
07 if ( method_exists( $this, $method ) ) {
08 return $this->$method();
09 }
10 }
11
12 function getName()
13 {
14 return "someName";
15 }
16}
{code listing}
Когда клиент пытается получить доступ к не существующему свойству, сработает магический метод __get(). В классе someClass мы реализовали этот метод. Он получает строку, содержащую название, и добавляет к названию приставку «get». Далее полученная строка пропускается через функцию проверки метода method_exists(). Если указанный метод существует, то мы его вызываем и возвращаем результат его работы клиенту.

Так, если клиент, например, запросит свойство $name:{code listing}то в этом случае сработает метод getName().

Если же метода не существует, то ничего не произойдет. Свойство, к которому попытался обратиться клиент будет иметь значение NULL. Это значение и получит клиент в ответ на свой запрос.

Метод __isset()

Этот метод срабатывает при вызове клиентом метода isset() для проверки существования некоторого свойства, которого, на самом деле, не существует.

Для демонстрации примера работы дополним класс someClass из предыдущего примера следующим методом:
1function __isset( $property )
2{
3 $method = "get{$property}";
4
5 return ( method_exists( $this, $method ) );
6}
{code listing}
Теперь когда осторожный пользователь перед использованием некоторого свойства попытается вначале проверить есть ли оно:
1if ( isset( $obj->name ) ) {
2 echo $obj->name;
3}
{code listing}
то при проверке наличия в объекте метода name сработает магический метод __isset(), который вернет true, если метод существует или false — если метод не существует. В данном случае результат выполнения isset() будет true, т.к. метод getName() существует.

Метод __set()

Данный метод срабатывает когда клиент пытается присвоить некоторое значение не существующему свойству. Магический метод __set() принимает два аргумента: название свойства и присваиваемое ему значение. Разработчик может сам решать, что ему делать с полученными аргументами.

В примере ниже мы дополнили класс someClass новым матодом:
01class someClass
02{
03 private $_name;
04
05 function __set( $property, $value )
06 {
07 $method = "set{$property}";
08
09 if ( method_exists( $this, $method ) ) {
10 return $this->$method( $value );
11 }
12 }
13
14 function setName( $name )
15 {
16 $this->_name = $name;
17
18 if ( ! is_null( $name ) ) {
19 $this->_name = strtoupper($this->_name);
20 }
21 }
22}
{code listing}
В этом примере мы работаем с методами-сеттерами (setters) — методами устанавливающими значения некоторых свойств, а также с методами-геттерами (getters) — методами получающими значения некоторых свойств.

Если пользователь попытается присвоить некоторое значение не существующему свойству, то сработает магический метод __set(). Он получит название свойства, которому пользователь попытался присвоить некоторое значение, а также само присваиваемое значение.

В приведенном примере, в методе __set() мы проверяем существует ли указанный метод. Если метод существует, то запускаем его на выполнение. Таким же образом мы можем осуществлять и фильтрацию клиентских значений.
ЗАМЕЧАНИЕ:
Вы можете заметить, что часто в документации PHP свойства и методы описываются в терминах статических свойств и методов, для подчеркивания того, что они рассматриваются в контексте класса.
Так, например, при чтении различной документации по PHP, можно встретить описание какого-либо свойства с указанием следующей нотации: someClass::$name даже, если свойство $name не задано как статическое (static). Такая нотация указывает на то, что свойство рассматривается в контексте класса, т.е. применительно к пространству имен класса, а не объекта.

Таким образом, если мы создадим объект obj и затем попытаемся назначить некоторое значение свойству obj::$name, то сработает метод __set().
Этот метод получит строку «name» и значение, которое мы хотим присвоить.

Теперь все, что мы будем делать с полученной информацией зависит от нас. В примере ниже, мы формируем название метода добавляя к полученному названию приставку «set». В итоге получаем название метода setName(), который существует и будет выполнен.
1$obj = new someClass();
2
3$obj->name = "xml"; // свойство $_name примет значение 'xml'
{code listing}


Метод __unset()

Как вы наверное догадались метод __unset() является зеркальной проекцией метода __set(). Когда клиент вызывает метод unset() для удаления не существующего свойства или метода, срабатывает магический метод __unset(). Этот метод принимает, в качестве параметра, название свойства (которого на самом деле может и не быть, но которое клиент попытался удалить). С полученной информацией мы можем делать, то что нам необходимо.

В примере ниже мы передаем null в существующие методы класса, используя туже технику, что и ранее:
1function __unset( $property )
2{
3 $method = "set{$property}";
4
5 if ( method_exists( $this, $method ) ) {
6 $this->$method( null );
7 }
8}
{code listing}


Метод __call()

Данный метод один из часто используемых методов-перехватчиков в PHP. Он срабатывает при вызове не существующего метода и принимает название запрашиваемого метода и все переданные в этот метод аргументы. Все что возвращает магический метод __call() будет передано клиенту в ответ на его запрос. Тем самым клиент даже не будет и подозревать, что тот метод, который он запросил, на самом деле не существует.

Магический метод __call() удобен для делегирования вызовов. Делегирование (delegation) это механизм с помощью которого один объект передает поступающие в него запросы другому объекту (другими словами это можно назвать передачей полномочий). Это нечто схожее с наследованием (inheritance) в классах, когда некоторый подкласс наследует свойства и методы своего родительского класса.

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

Давайте проясним описанное выше на следующем примере. Мы имеем простой класс personWriter для форматирования информации получаемой из класса person:
01class personWriter
02{
03 function writeName( Person $p )
04 {
05 echo $p->getName() . PHP_EOL;
06 }
07
08 function writeAge( Person $p )
09 {
10 echo $p->getAge() . PHP_EOL;
11 }
12}
13// константа PHP_EOL это кросс платформенная форма перевода строки - аналог "\n".
{code listing}
Ниже приведена реализация класса person, который использует как объект personWriter так и метод __call(). Конечно же мы могли бы использовать механизм наследования, но сейчас, для примера, продемонстрируем использование механизма делегирования.
01class person {
02 private $_writer;
03
04 function __construct( personWriter $writer )
05 {
06 $this->_writer = $writer;
07 }
08
09 function __call( $methodname, $args )
10 {
11 if ( method_exists( $this->_writer, $methodname ) ) {
12 return $this->_writer->$methodname( $this );
13 }
14 }
15
16 function getName() { return "Bob"; }
17
18 function getAge() { return 44; }
19}
{code listing}
Здесь видно, что класс person использует обращение к классу personWriter. В методе __call() мы используем аргумент $methodname, проверяем существует ли в созданном объекте personWriter метод с указанным названием. Если мы находим указанный метод, то делегируем вызов этого метода в объект personWriter, передавая ему также и экземпляр текущего объекта (при помощи псевдо-переменной $this).

Таким образом, если клиентская программа выполнит следующий запрос к объекту person:
1$person = new person( new personWriter() );
2
3$person->writeName();
{code listing}
то сработает магический метод _call(). Мы найдем вызванный метод writeName() в нашем объекте personWriter и сразу же выполним его. Это избавляет нас от необходимости вручную создавать и использовать метод writeName(), как это пришлось бы сделать без использования механизма делегирования запросов:
1function writeName()
2{
3 $this->writer->writeName( $this );
4}
{code listing}
Как мы видим класс person чудесным образом получил два новых метода. Также автоматическое делегирование может избавить от лишней работы, но при этом теряется очевидность реализации той или иной задачи.

Механизм делегирования вызовов относится к области так называемого рефлексивного (reflection) программирования.
Методы-перхватчики занимают свое место в языке PHP и использовать их надо очень осторожно, т.к. они стирают очевидность алгоритмов реализации. Поэтому если вы собираетесь использовать эти методы, позаботьтесь о подробном документировании алгоритмов работы, чтобы другие разработчики, увидев ваш код, не путались и не ломали голову как это работает и почему.