Особенности при перехватах вызовов методов с помощью __call() и __callStatic() в PHP

Пролог


Написать эту статью помогли мои эксперименты с Объектно-ориентированной парадигмой.
Собственно, php-гуру эта статья вряд ли покажется интересной, а вот только что знакомящимся с языком, я надеюсь, поможет обойти подводные камни. Статья не претендует на мануал по ООП, а лишь разъясняет некоторые моменты.

Что такое __call() и __callStatic()?


Начнём с простого: у вас есть класс, описывающий методы и свойства какого-либо объекта (что, в принципе, логично). Представьте, что вы решили обратиться к несуществующему методу этого объекта. Что вы получите? Правильно — фатальную ошибку! Ниже привожу простейший код.

<?php
class OurClass{}
$Object=new OurClass;
$Object->DynamicMethod(); #Получаем Fatal error: Call to undefined method OurClass::DynamicMethod()
?>


В статическом контексте наблюдаем аналогичное поведение:

<?php
class OurClass{}
OurClass::StaticMethod(); #Получаем Fatal error: Call to undefined method OurClass::StaticMethod()
?>


Так вот, иногда возникает необходимость либо выполнить какой-то код при отсутствии нужного нам метода, либо узнать какой метод пытались вызвать, либо использовать другое API для вызова нужного нам метода. С этой целью и существуют методы __call() и __callStatic() — они перехватывают обращение к несуществующему методу в контексте объекта и в статическом контексте, соответственно.
Перепишем наши примеры с использованием «магических методов». Примечание: Каждый из этих волшебников принимает два параметра: первый — имя метода, который мы пытаемся вызвать, второй — список, содержащий параметры вызываемого метода. Ключ — номер параметра вызываемого метода, значение — собственно сам параметр:

<?php
class OurClass
{
	public function __call($name,array $params)
	{
		echo 'Вы хотели вызвать $Object->'.$name.', но его не существует, и сейчас выполняется '.__METHOD__.'()<br>'
		.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 звучат как «текучие интерфейсы»:

<?php
abstract class Manager
{
	public
		$content_storage='';
	
	public function __toString()
	{
		return $this->content_storage;
	}
	
	public function __call($name,array $params)
	{
		$this->content_storage.=$this->_GetObject($name,$params).'<br>'.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.
Набросал примерно такой кусок кода (в оригинале было чуть по другому, но для статьи сократил, ибо остальное не несло смысловой нагрузки для данной проблемы):

<?php
abstract class Base
{
	public function __call($n,array$p)
	{
		echo __METHOD__.' says: '.$n;
	}
}

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 воспринимает статический контекст внутри класса и вне его по-разному. Не поняли? Попытаюсь проиллюстрировать. Возьмём предыдущий пример и добавим в него одну строчку:

<?php
abstract class Base
{
	public function __call($n,array$p)
	{
		echo __METHOD__.' says: '.$n;
	}
	
}

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() контекст вызова не изменится. Поведение в этом случае будет одинаковым. Опять же, проиллюстрирую:

<?php
abstract class Base
{
	public function __call($n,array$p)
	{
		echo __METHOD__.' says: '.$n;
	}
	
}

class Main extends Base
{
	public function __construct()
	{
		# Все три строчки ниже вызывают Base::__call()
		self::Launch();
		static::Launch();
		Main::Launch();
	}
}

$M=new Main;
?>


Итог статьи прост: будьте внимательными (впрочем, как и всегда) и проверяйте поведение при вызовах методов.

Поскриптум


Просмотрел багтрекер, оказывается проблема не у меня одного. Но разработчики видимо решили, что подобный баг, багом не является, поэтому оставили как есть.
Поделиться публикацией
Комментарии 34
    +3
    Это не внутри и вне класса, а внутри и вне созданного объекта этого класса. Если не инстанцировать то изнутри класса всё тоже прекрасно работает.
      +1
      Спасибо за верное замечание. Статью поправил.
      –6
      Все равно его не брошу… потому, что он хороший
        +5
        Все дело в том, как в php внутри устроен $this. На удивление, это очень похоже на JavaScript =)

        Эта история тянется со времен php4, когда не было четкого указания статического вызова в объявлении метода. Например, такой прием можно встретить, если покопаться в коде 15-летней давности:

        class A {
             function foo() {
                 if (!isset($this) || get_class($this) !== __CLASS__) {
                      $self = new A;
                      return $self->foo();
                 }
                 // do smth
             }
        }
        A::foo();
        


        С тех пор изменились только внешние декорации.

        Вот такая прелесть бросает E_STRICT, но до сих пор работает:

        <?php
        class A {
            function x() {
                var_dump(get_class($this));
            }
        }
        class B {
            function __construct() {
                A::x();
            }
        }
        new B;
        

        PHP Strict standards:  Non-static method A::x() should not be called statically, assuming $this from incompatible context in test.php on line 9
        string(1) "B"
        

        Deprecated этот, очевидно, делает то самое банальное сравнение (перепишем поумнее на LSB)
        isset($this) && $this instanceof get_called_class()
        

        поскольку если сделать так
        class A {
            public function __construct() {
                A::x();
            }
            function x() {
                var_dump(get_class($this));
            }
        }
        new A;
        

        или даже так
        class A {
            public function __construct() {
                A::x();
            }
            function x() {
                var_dump(get_class($this));
            }
        }
        class B extends A {
            function __construct() {
                A::x();
            }
        }
        new B;
        

        то никакого E_STRICT не будет: $this-то «правильный» ;)

        Так что ООП тут никаким местом, это просто очередная особенность реализации php.
          0
          Спасибо за разъяснения и приобретённый опыт. Сделаю выводы. Думаю многие, кто читает ваш комментарий и не знают этой особенности также приобретут новые знания. Но всё же, мне кажется, что разработчикам стоило бы задокументировать неочевидное поведение интерпретатора. Ведь если наперёд не знать, что это капризы PHP, то программист, в особенности новичок, может потратить не один час в тщетных поисках ошибки. Зная такую особенность, программисты смогут избегать, а ещё лучше, учитывать это при использовании такого рода конструкций.
            +1
            Так задокументировано ведь.

            The pseudo-variable $this is available when a method is called from within an object context. $this is a reference to the calling object (usually the object to which the method belongs, but possibly another object, if the method is called statically from the context of a secondary object).


            Большой красной таблички «АХТУНГ, ЗДЕСЬ ПРОИСХОДИТ Н.Ё.Х.» не хватает, это согласен.
              0
              Перевод: «Псевдо-переменная $this доступна в том случае, если метод был вызван в контексте объекта. $this является ссылкой на вызываемый объект. Обычно это тот объект, которому принадлежит вызванный метод, но может быть и другой объект, если метод был вызван статически из контекста другого объекта.»

              Мой мозг не мог провести аналогию с этим абзацем, до того как вы не объяснили причины такого поведения в предыдущем комментарии. Спасибо.

              Да, вот именно таблички и не хватает, а жаль. Серьёзно, для меня не было очевидным такое поведение $this. Но это обозначает лишь только, что есть куда развиваться.
                0
                Поправьте меня, если я неправильно понял… Но получается не-статический метод вполне реально вызвать статически (с использованием ::)?
                <?php
                class A
                {
                	public function DynamicMethod()
                	{
                		var_dump(debug_backtrace());
                		return 'I am Dynamic';
                	}
                }
                echo A::DynamicMethod();
                /*
                Вывод:
                array (size=1)
                  0 => 
                    array (size=6)
                      'file' => string 'C:\OpenServer\domains\test1.ru\index.php' (length=40)
                      'line' => int 11
                      'function' => string 'DynamicMethod' (length=13)
                      'class' => string 'A' (length=1)
                      'type' => string '::' (length=2)
                      'args' => 
                        array (size=0)
                          empty
                
                I am Dynamic
                */
                ?>
                
                  +1
                  Можно, но не нужно — E_STRICT будет: включите error_reporting=E_ALL. Или обновите php :)

                  Рано или поздно это окончательно выпилят.
            +2
            Мне кажется так было всегда, вспомнить хотя бы parent::__construct();
              +1
              Более того, можно похожим способом вызывать метод «дедушки», если знать его имя:

              <?php
              class A { function test() { echo $this->property . " A\n"; } }
              class B extends A { function test() { echo $this->property . " B\n"; } }
              class C extends B {
                public $property;
                function test() { echo $this->property . " C\n"; }
                function run() { $this->property = "TEST"; A::test(); }
              }
              
              $obj = new C;
              $obj->run();
              


              Выведет «TEST A»
                0
                Извините, не вижу в этом коде, за исключением вышеупомянутых странностей, ничего необычного.
                  +1
                  Это просто потому, что с вышеупомянутыми странностями вряд ли что-то может сравниться :). Каюсь, не прочитал до конца все комментарии перед тем, как написать свой пример. Тем не менее, мне кажется, мой пример хотя бы может иметь какую-то отдаленную практическую ценность :).
                    0
                    мой пример хотя бы может иметь какую-то отдаленную практическую ценность

                    Чтобы окончательно запутать людей, которые будут поддерживать код после вас — да.

                    По факту — «вы не должны этого хотеть»
                      0
                      Чтобы окончательно запутать людей, которые будут поддерживать код после вас — да.
                      По факту — «вы не должны этого хотеть»

                      Согласен, лучше воздержаться от использования такого кода. Тем не менее, в очень редких случаях это бывает удобным и оправданным (например, если вы пишете «одноразовый» класс/скрипт, которому такое нужно :))
                –2
                Вот вам еще немножко статических пхп ужасов:
                class A {
                    static $i;
                    public static function i(){
                        return self::$i? self::$i: self::$i = new static;
                    }
                    function say(){ echo get_class($this) . "\n"; }
                }
                
                class B extends A{}
                class C extends A{}
                
                B::i()->say();
                C::i()->say();
                

                выдаст
                B
                B
                

                но
                class A {
                    public static function i(){
                        static $i;
                        return $i? $i: $i = new static;
                    }
                    function say(){ echo get_class($this) . "\n"; }
                }
                
                class B extends A{}
                class C extends A{}
                
                B::i()->say();
                C::i()->say();
                

                выдаст
                B
                C
                


                Кто расскажет почему?
                Создается впечатление, что статические методы, вместо наследования, тупо копируются.
                  +4
                  Так задумано, self — это compile time static binding, static — runtime static binding.
                  php.net/LSB
                    –1
                    ОК. пойдем дальше
                    class A {
                        public static function i(){
                            $f = function($name){
                                static $i;
                                return $i? $i: $i = new $name;
                            };
                            return $f(get_called_class());
                        }
                        function say(){ echo get_class($this) . "\n"; }
                    }
                    
                    class B extends A{}
                    class C extends A{}
                    
                    B::i()->say();
                    C::i()->say();
                    

                    B
                    C
                    


                    function j($name){
                        static $i;
                        return $i? $i: $i = new $name;
                    }
                    
                    class A {
                        public static function i(){
                            return j(get_called_class());
                        }
                        function say(){ echo get_class($this) . "\n"; }
                    }
                    
                    class B extends A{}
                    class C extends A{}
                    
                    B::i()->say();
                    C::i()->say();
                    

                    B
                    B
                    


                    правильно ли я понимаю, что статические переменные внутри функций не имеют ничего общего с статическими переменными внутри методов класса? иначе что за static binding происходит внутри функции?
                  0
                  Почему бы и нет? Просто у self, parent и static разный способ поведения.
                    0
                    По-порядку (разбираю второй вариант):
                    1. Выполняем
                    B::i()

                    а). Т.к. $i — не присвоено значение, то при выполнении тернарного оператора NULL, при преобразовании в булев контекст будет равен FALSE. Следовательно $i=new static; Где static — это контекст класса B. Следовательно $i — экземпляр (объект)
                    класса B.
                    б). Функция $i->Say(), где $i — объект класса B совершенно логично выполнит get_class($this). Т.е. вернёт имя класса, к которому принадлежит $this — в данном случае B.
                    2. Выполняем
                    C::i()

                    а). Т.к. $i — не присвоено значение (то, что оно помечено статическим — ничего не значит, т.к. по сути B::i() и C::i() не зависят друг от друга в плане инициализации свойств), то при выполнении тернарного оператора NULL, при преобразовании в булев контекст будет равен FALSE. Следовательно $i=new static; Где static — это контекст класса C. Следовательно $i — экземпляр (объект)
                    класса C.
                    б). Функция $i->Say(), где $i — объект класса C совершенно логично выполнит get_class(). Т.е. вернёт имя класса, к которому принадлежит $this — в данном случае C.

                    Надеюсь это то, что вы хотели услышать
                      0
                      Я хотел лишь узнать, почему при вызове одного и того же метода статическая переменная внутри этого метода «теряет» свое значение.
                      Я хотел сказать, что метод то один, почему статических перемнных стало две?
                      Это же не свойство класса, а статическая переменная внутри метода?
                        0
                        Она не теряет своего значения, B::i()!=C::i() (они наследуются от A, но переменные внутри методов имеют разное значение ($i1!=$i2)). Ну не может терять значение то, что никогда не было инициализировано. Проверьте var_dump'ом.
                        class A {
                            public static function i(){
                                static $i;
                        		var_dump($i);
                                return $i? $i: $i = new static;
                            }
                            function say(){ echo get_class($this) . "\n"; }
                        }
                        
                        class B extends A{}
                        class C extends A{}
                        
                        B::i()->say();
                        C::i()->say();
                        
                        /*
                        Вывод:
                        null
                        B
                        
                        null
                        C 
                        */
                        


                        Я в прошлом комментарии описался, прошу прощения, естественно имел ввиду переменную $i внутри метода, а не свойство класса.
                          –1
                          хочется понять, почему B::i()!=C::i() когда метод один. Ну и зачем так сделано
                            +1
                            <?php
                            class A {
                                public static function Method($value)
                            	{
                            		static $v;
                            			$v=$v ? : $value;
                            		echo'Я метод класса '.get_called_class().' $v='.$v.'<br>'.PHP_EOL;
                                }
                            }
                            
                            class B extends A{}
                            class C extends A{}
                            
                            B::Method(5);
                            C::Method(8);
                            ?>
                            


                            Метод не один. Поймите. Как бы вам объяснить. Ну вот откройте документ любой у себя на компьютере, например в формате .docx. Теперь возьмите и распечатайте две копии на принтере. А теперь возьмите и напишите ручкой на одном из распечатанных документов «PHP». Теперь переведите взгляд на второй распечатанный документ. Там появилось слово «PHP»?
                            Так вот, документ на компьютере — это класс A, две копии, распечатанные на принтере, это классы B и C.

                            Так и тут, методы B::i() и C::i() — это два образца основанные на одном прототипе. Но друг от друга они не зависят.
                              0
                              Скажите пожалуйста: при наследовании наследства (извините за тавтологию), вашей горячо любимой троюродной прабабушки, копируется ли оно, или нет?
                              Так вот, к чему это я? К тому, что каждый понимает программные абстракции по своему. Аналогии ложны, а вашему принтеру стоило быть более экономным, чтобы сберечь леса. Истинным, в данном случае, может быть только мануал. Мне хотлось узнать, где можно почитать, как внутри устроено наследование и как оно превращается в копирование
                              • НЛО прилетело и опубликовало эту надпись здесь
                                  0
                                  К сожалению, в мануале ничего не написано про механизмы наследования. О том что методы при наследовании копируются. Пока это мои догадки. Хочется быть уверенным.
                                  Я надеялся на ссылки на какую-нибудь статью, с описаниями механизмов. Но, похоже, придется лезть в код и самому писать.
                                    0
                                    Дело не в копировании, а именно в биндинге www.php.net/manual/en/language.oop5.late-static-bindings.php тут достаточно понятно описано.
                                      0
                                      А если вообще конкретизировать, начните чтение с различий между ключевыми словами self и static
                                      • НЛО прилетело и опубликовало эту надпись здесь
                                      0
                                      Вы зря иронизируете. Я хотел просто более понятным, «человеческим» языком, объяснить, как это выглядит и с чем можно провести аналогию. Видимо, я потерпел в этом полный крах. Помню в то время, когда заинтересовался языком C++, я открыл книгу Джесса Либерти — «C++ за 21 день». Так вот там, автор, для наглядности, абстрактный класс сравнивает со словом «автомобиль» в целом, обычный класс, который наследуется, от выше упомянутого абстрактного, сравнивает с маркой машины, а объект — с конкретным, выпущенным на конвейере. (Пишу по памяти, если немного приврал — уж простите, но суть от этого не меняется). Где-то ещё видел как классы сравнивали со зверями. В общем, по-моему, такие аналогии даже полезны, поскольку помогают понять основные моменты ООП-парадигмы, основная суть в которой, если вы не забыли, это описание классов (описание методов, обозначение свойств, полиморфизм и инкапсуляция) и манипулирование с экземплярами этих классов.
                          0
                          Уважаемые хабра-минусующие, поясните свою позицию, пожалуйста. Что вам не понравилось — материал статьи, форма изложения, оформление. Это мой первый пост и если я и сделал какие-то ошибки, вы не молчите, я не экстрасенс, не хочется упасть лицом в лужу в следующий раз.
                            0
                            заглушки __call и __callStatic работают правильно. Попробую объяснить почему.
                            Когда вы ведете обращение к несуществующему методу из созданого объекта, то механизм объекта не принимает решение статический метод или объектный не существует — для него все несуществующие только обэктные.

                            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                            Самое читаемое