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

в 17:17, , рубрики: 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;
?>

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

Поскриптум

Просмотрел багтрекер, оказывается проблема не у меня одного. Но разработчики видимо решили, что подобный баг, багом не является, поэтому оставили как есть.

Автор: torf

Источник

Поделиться

* - обязательные к заполнению поля