Особенности при перехватах вызовов методов с помощью __call() и __callStatic() в PHP
Написать эту статью помогли мои эксперименты с Объектно-ориентированной парадигмой.
Собственно, php-гуру эта статья вряд ли покажется интересной, а вот только что знакомящимся с языком, я надеюсь, поможет обойти подводные камни. Статья не претендует на мануал по ООП, а лишь разъясняет некоторые моменты.
Что такое __call() и __callStatic()?
Начнём с простого: у вас есть класс, описывающий методы и свойства какого-либо объекта (что, в принципе, логично). Представьте, что вы решили обратиться к несуществующему методу этого объекта. Что вы получите? Правильно — фатальную ошибку! Ниже привожу простейший код.
$Object=new OurClass; $Object->DynamicMethod(); #Получаем Fatal error: Call to undefined method OurClass::DynamicMethod() ?>
В статическом контексте наблюдаем аналогичное поведение:
OurClass::StaticMethod(); #Получаем Fatal error: Call to undefined method OurClass::StaticMethod() ?>
Так вот, иногда возникает необходимость либо выполнить какой-то код при отсутствии нужного нам метода, либо узнать какой метод пытались вызвать, либо использовать другое API для вызова нужного нам метода. С этой целью и существуют методы __call() и __callStatic() — они перехватывают обращение к несуществующему методу в контексте объекта и в статическом контексте, соответственно.
Перепишем наши примеры с использованием «магических методов». Примечание: Каждый из этих волшебников принимает два параметра: первый — имя метода, который мы пытаемся вызвать, второй — список, содержащий параметры вызываемого метода. Ключ — номер параметра вызываемого метода, значение — собственно сам параметр:
'.$name.', но его не существует, и сейчас выполняется '.__METHOD__.'()
' .PHP_EOL; return; > public static function __callStatic($name,array $params) < echo 'Вы хотели вызвать '.__CLASS__.'::'.$name.', но его не существует, и сейчас выполняется '.__METHOD__.'()'; return; >> $Object=new OurClass; #Вы хотели вызвать $Object->DynamicMethod, но его не существует, и сейчас выполняется OurClass::__call() $Object->DynamicMethod(); #Вы хотели вызвать OurClass::StaticMethod, но его не существует, и сейчас выполняется OurClass::__callStatic() OurClass::StaticMethod(); ?>
Практическое применение этих двух товарищей зависит только от вашей фантазии. В качестве примера, приведу набросок реализации техники программирования Fluent Interface (некоторые считают это паттерном проектирования, но от названия суть не меняется). Коротко говоря, fluent interface позволяет составлять цепочки вызовов объектов (с виду что-то похожее на jQuery). На хабре есть пару статей про реализацию такого рода алгоритмов. На ломаном русском переводе fluent interfaces звучат как «текучие интерфейсы»:
content_storage; > public function __call($name,array $params) < $this->content_storage.=$this->_GetObject($name,$params).'
'.PHP_EOL; return $this; > > abstract class EntryClass < public static function Launch() < return new FluentInterface; >> class FluentInterface extends Manager < public function __construct() < /** * Что-нибудь инициализируем */ >public static function _GetObject($n,array $params) < return $n; >> echo $FI=EntryClass::Launch() ->First() ->Second() ->Third(); /* Выведет First Second Third */ ?>
Ты кажется хотел рассказать нам что-то про особенности перехвата?
Обязательно расскажу. Сидя вчера за компьютером, решил систематизировать свои знания по PHP.
Набросал примерно такой кусок кода (в оригинале было чуть по другому, но для статьи сократил, ибо остальное не несло смысловой нагрузки для данной проблемы):
> class Main extends Base < public function __construct() < self::Launch(); >> $M=new Main; ?>
Обновил страничку. Моему удивлению не было предела. Я сразу побежал на php.net смотреть мануал.
Вот выдержка из документации
public mixed __call ( string $name , array $arguments ) public static mixed __callStatic ( string $name , array $arguments )
В контексте объекта при вызове недоступных методов вызывается метод __call().
В статическом контексте при вызове недоступных методов вызывается метод __callStatic().
Я долго не мог понять в чём проблема. Версия PHP: 5.4.13. То есть те времена, когда вызовы несуществующих методов из любого контекста приводили к вызову __call() давно прошли. Почему вместо логичной Fatal Error, я получаю вызов __call()? Я пошёл исследовать дальше. Добавил в абстрактный класс Base метод __callStatic(). Снова обновил страницу. Вызов по-прежнему адресовался в __call(). Промучавшись полдня, всё-таки понял в чём была проблема. Оказывается PHP воспринимает статический контекст внутри класса и вне его по-разному. Не поняли? Попытаюсь проиллюстрировать. Возьмём предыдущий пример и добавим в него одну строчку:
> class Main extends Base < public function __construct() < self::Launch(); >> $M=new Main; Main::Launch(); # Добавили вот эту строчку. Теперь мы получаем Fatal error: Call to undefined method Main::Launch() ?>
То есть статический контекст — статическому контексту рознь.
Чудеса да и только. Когда я изучал «магические методы», я не думал, что название стоит воспринимать настолько буквально.
Ну здесь становится уже всё понятно: если мы добавим метод __callStatic() в класс Base приведённого выше примера, то вместо вывода фатальной ошибки, PHP выполнит __callStatic().
Резюме
Если для вас ещё не всё понятно: речь идёт о том, что обращение к статическому методу внутри экземпляра класса и обращение к статическому методу вне экземпляра класса — воспринимаются интерпретатором по-разному. Если вы поменяете self::Launch() на Main::Launch() контекст вызова не изменится. Поведение в этом случае будет одинаковым. Опять же, проиллюстрирую:
> class Main extends Base < public function __construct() < # Все три строчки ниже вызывают Base::__call() self::Launch(); static::Launch(); Main::Launch(); >> $M=new Main; ?>
Итог статьи прост: будьте внимательными (впрочем, как и всегда) и проверяйте поведение при вызовах методов.
Поскриптум
Просмотрел багтрекер, оказывается проблема не у меня одного. Но разработчики видимо решили, что подобный баг, багом не является, поэтому оставили как есть.
Магические методы
Имена методов __construct(), __destruct(), __call(), __callStatic(), __get(), __set(), __isset(), __unset(), __sleep(), __wakeup(), __toString(), __invoke(), __set_state(), __clone() и __debugInfo() зарезервированы для «магических» методов в PHP. Не стоит называть свои методы этими именами, если вы не хотите использовать их «магическую» функциональность.
PHP оставляет за собой право все методы, начинающиеся с __, считать «магическими». Не рекомендуется использовать имена методов с __ в PHP, если вы не желаете использовать соответствующий «магический» функционал.
__sleep() и __wakeup()
Функция serialize() проверяет, присутствует ли в вашем классе метод с «магическим» именем __sleep(). Если это так, то этот метод выполняется прежде любой операции сериализации. Он может очистить объект и предполагается, что будет возвращен массив с именами всех переменных объекта, который должен быть сериализован. Если метод ничего не возвращает кроме NULL , то это значит, что объект сериализован и выдается предупреждение E_NOTICE .
Замечание:
Недопустимо возвращать в __sleep() имена приватных свойств объекта в родительский класс. Это приведет к предупреждению E_NOTICE . Вместо этого вы можете использовать интерфейс Serializable.
Рекомендованное использование __sleep() состоит в завершении работы над данными, ждущими обработки или других подобных задач очистки. Кроме того, этот метод можно выполнять в тех случаях, когда нет необходимости сохранять полностью очень большие объекты.
С другой стороны, функция unserialize() проверяет наличие метода с «магическим» именем __wakeup(). Если такой имеется, то он может воссоздать все ресурсы объекта, принадлежавшие ему.
Обычно __wakeup() используется для восстановления любых соединений с базой данных, которые могли быть потеряны во время операции сериализации и выполнения других операций повторной инициализации.
Пример #1 Sleep и wakeup
class Connection
protected $link ;
private $dsn , $username , $password ;
?php
public function __construct ( $dsn , $username , $password )
$this -> dsn = $dsn ;
$this -> username = $username ;
$this -> password = $password ;
$this -> connect ();
>
private function connect ()
$this -> link = new PDO ( $this -> dsn , $this -> username , $this -> password );
>
public function __sleep ()
return array( ‘dsn’ , ‘username’ , ‘password’ );
>
public function __wakeup ()
$this -> connect ();
>
> ?>
__toString()
Метод __toString() позволяет классу решать самостоятельно, как он должен реагировать при преобразовании в строку. Например, что напечатает echo $obj;. Этот метод должен возвращать строку, иначе выдастся неисправимая ошибка E_RECOVERABLE_ERROR .
Нельзя бросить исключение из метода __toString(). Попытка это сделать закончится фатальной ошибкой.
Пример #2 Простой пример
// Объявление простого класса
class TestClass
public $foo ;
?php
public function __construct ( $foo )
$this -> foo = $foo ;
>
public function __toString ()
return $this -> foo ;
>
>
$class = new TestClass ( ‘Привет’ );
echo $class ;
?>
Результат выполнения данного примера:
Ранее, до PHP 5.2.0, метод __toString() вызывался только непосредственно в сочетании с функциями echo или print . Начиная с PHP 5.2.0, он вызывается в любом строчном контексте (например, в printf() с модификатором %s), но не в контекстах других типов (например, с %d модификатором). Начиная с PHP 5.2.0, преобразование объекта в строку при отсутствии метода __toString() вызывает ошибку E_RECOVERABLE_ERROR .
__invoke()
Метод __invoke() вызывается, когда скрипт пытается выполнить объект как функцию.
Замечание:
Данный метод доступен начиная с PHP 5.3.0.
Пример #3 Использование __invoke()
class CallableClass
public function __invoke ( $x )
var_dump ( $x );
>
>
$obj = new CallableClass ;
$obj ( 5 );
var_dump ( is_callable ( $obj ));
?>?php
Результат выполнения данного примера:
__set_state()
Этот статический метод вызывается для тех классов, которые экспортируются функцией var_export() начиная с PHP 5.1.0.
Параметр этого метода должен содержать массив, состоящий из экспортируемых свойств в виде array(‘property’ => value, . ).
Пример #4 Использование __set_state() (начиная с PHP 5.1.0)
class A
public $var1 ;
public $var2 ;
public static function __set_state ( $an_array ) // С PHP 5.1.0
$obj = new A ;
$obj -> var1 = $an_array [ ‘var1’ ];
$obj -> var2 = $an_array [ ‘var2’ ];
return $obj ;
>
>
$a = new A ;
$a -> var1 = 5 ;
$a -> var2 = ‘foo’ ;
eval( ‘$b = ‘ . var_export ( $a , true ) . ‘;’ ); // $b = A::__set_state(array(
// ‘var1’ => 5,
// ‘var2’ => ‘foo’,
// ));
var_dump ( $b );
Результат выполнения данного примера:
object(A)#2 (2) < ["var1"]=>int(5) ["var2"]=> string(3) "foo" >
__debugInfo()
Этот метод вызывается функцией var_dump() , когда необходимо вывести список свойств объекта. Если этот метод не определен, тогда будут выведены все public, protected и private свойства объекта.
Этот метод был добавлен в PHP 5.6.0.
Пример #5 Использование __debugInfo()
public function __construct ( $val ) $this -> prop = $val ;
>
public function __debugInfo () return [
‘propSquared’ => $this -> prop ** 2 ,
];
>
>
Результат выполнения данного примера: