Пользователь
0,0
рейтинг
7 июля 2014 в 14:48

Разработка → [Перевод] Магические методы в PHP из песочницы tutorial

PHP*
Если Вы когда-нибудь изучали PHP-код открытых проектов, то вы могли встречать методы, начинающиеся с двойного подчеркивания. Это и есть те самые магические методы, с помощью которых вы сможете определить поведение вашего объекта при различных манипуляциях с его экземпляром.

Предполагаю, что вы уже сталкивались с некоторыми из них, ведь существуют довольно распространенные методы, и тем не менее, я считаю, что компетентному программисту PHP необходимо уверенное владение всеми возможностями языка.
Я думаю, это можно считать, своего рода, отправной точкой в мир Магических методов.

Приступая к изучению


Когда я сам изучал этот материал, я использовал всевозможные учебники и статьи, в которых излагались довольно глупые или вообще бесполезные примеры. Я считаю, что для того чтобы понять что-то нужно попробовать это в контексте реальной задачи. Именно с этого мы и начнем.

Представим себе, что мы хотим получать все твиты, при помощи Tweeter Api. Мы получаем JSON всех твитов текущего пользователя и хотим превратить каждый твит в объект с методами, которые позволят проводить определенные операции.

Ниже, я представил базовый класс Tweet:
class Tweet {
 
}


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

Конструкторы и Деструкторы


Пожалуй, одним из самых наиболее распространенных магических методов является конструктор ( __construct() ). Если вы достаточно внимательно следили за созданием приложения Cribbb в моем блоге, вы достаточно осведомлены об этом методе.

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

Пример использования:

public function __construct($id, $text)
{
  $this->id = $id;
  $this->text = $text;
}
 
$tweet = new Tweet(123, 'Hello world');


Когда мы создаем экземпляр класса Tweet, мы можем передать параметры, которые поступят в метод __construct(). Из примера выше, вы можете видеть, что мы не вызываем этот метод и не должны вызывать — он вызывается автоматически.

Со временем у вас возникнет необходимость расширение класса путем его наследования. Иногда родительский класс так же имеет метод __construct(), который совершает определенные действия, таким образом чтобы не потерять функционал класса-родителя, нужно вызвать и его конструктор.

class Entity {
  protected $meta;
  public function __construct(array $meta)
  {
    $this->meta = $meta;
  }
 
}
 
class Tweet extends Entity {
  protected $id;
  protected $text;
  public function __construct($id, $text, array $meta)
  {
    $this->id = $id;
    $this->text = $text;
    parent::__construct($meta);
  }
}


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

public function __destruct()
{
  $this->connection->destroy();
}


Если быть честным, то большую часть метода __destruct(), изложенного выше я скрыл от вас. PHP на самом деле не из тех языков, где процесс будет существовать достаточно длительное время, так что я не думаю, что у вас будет что-либо для чего мог бы понадобиться деструктор. Сам по себе жизненный цикл запроса в PHP настолько мал, что от данного метода будет скорее больше хлопот, чем пользы.

Геттеры и сеттеры


Когда вы работаете с обьектами в PHP, вам бы очень хотелось обращаться к свойствам объекта как-то так:

$tweet = new Tweet(123, 'hello world');
echo $tweet->text; // 'hello world'


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

public function __get($property)
{
  if (property_exists($this, $property)) {
    return $this->$property;
  }
}


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

Как и в примерах выше — вы не должны вызывать этот метод напрямую, PHP будет вызывать его каждый раз, при попытке получения доступа к не публичным свойствам класса.

В обратной ситуации — если вы попытаетесь установить значение свойства, которое не является публичным — вы получите ошибку. И опять же, в PHP есть свой метод, который будет вызван при попытке установить в не публичное поле какое-либо значение. Данный метод принимает 2 параметра в качестве аргументов — свойство, в которое хотели записать значение, и само значение.

Если вы хотите использовать данный метод, ваш класс получит свойство, на подобии этого:

public function __set($property, $value)
{
  if (property_exists($this, $property)) {
    $this->$property = $value;
  }
}
 
$tweet->text = 'Setting up my twttr';
echo $tweet->text; // 'Setting up my twttr'


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

Впрочем, вы все равно иногда будете встречать методы __get() и __set(), которые принято называть геттерами и сеттерами соотвественно. Это довольно хорошее решение, если вы решили изменить какое-либо значение или добавить немножко бизнес-логики.

Проверка свойства на существование


Если вы знакомы с PHP, вы скорее всего знаете о существовании функции isset(), которую обычно применяют при работе с массивами. Вы так же можете использовать эту функцию, для того чтобы понять — задано свойство в обьекте или нет. Вы сможете определить магический метод __isset(), для того чтобы можно проверять не только общедоступные свойства, но и другие.

public function  __isset($property)
{
  return isset($this->$property);
}
 
isset($tweet->text); // true


Как вы видите выше, __isset() метод отслеживает вызов функции на проверку существования и получает в качестве аргумента — название свойства. В свою очередь, в методе вы можете использовать функцию isset(), для проверки существования.

Очистка переменной
По аналогии с функцией isset(), функция unset() обычно используется при работе с массивами. Опять же, вы можете использовать функцию unset() для того чтобы очистить значение не публичного свойства. Чтобы применить данный метод на не публичные свойства, вам понадобиться метод __unset(), который будет отслеживать попытки очистить не публичный свойства класса.

public function __unset($property)
{
  unset($this->$property);
}


Приведение к строке


Метод __toString() позволит вам определить логику работы вашего приложения, при попытке привести обьект к типу строке.
Например:

public function __toString()
{
  return $this->text;
}
 
$tweet = new Tweet(1, 'hello world');
echo $tweet; // 'hello world'


Можно сказать, что когда вы пытаетесь обратиться к обьекту, как к строке, например при использовании echo, обьект будет возвращен так, как вы определите в __toString() методе.

Хорошей иллюстрацией в данном случае может случить Eloquent Models из фреймворка Laravel. При попытке приведения обьекта к строке вы получите json. Если вы хотите увидеть как Laravel это делает, рекомендую обратиться к исходному коду.

Сон и пробуждение


Функция сериализации ( serialize() ), является довольно распространенным способом хранения обьекта. Например, если бы вы хотели сохранить обьект в базе данных, для начала вы должны были бы его сериализовать, затем сохранить, а когда бы он вам потребовался снова, вы должны были бы его получить и десериализовать ( unserialise() ).

Метод __sleep(), позволяет определить какие свойства должны быть сохранены. Если бы мы к примеру, не хотели сохранять какие-либо связи или внешние ресурсы.

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

$tweet = new Tweet(123, 'Hello world', new PDO ('mysql:host=localhost;dbname=twttr', 'root'));


Когда мы готовим к сохранению обьект, нам естественно не нужно сохранить подключение к базе данных, ведь в будущем это будет бессмысленно.
Поэтому в методе __sleep() мы определим массив свойств, которые должны быть сохранены.

public function __sleep()
{
  return array('id', 'text');
}


А после того как настанет время для пробуждения обьекта, нам могут понадобиться все то, что мы не сохранили при сериализации. В конкретном примере нам нужно установить соединение с базой данных. Это можно сделать, при помощи магического метода __wakeup().

public function __wakeup()
{
  $this->storage->connect();
}


Вызов методов


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

class Tweet {
  protected $id;
  protected $text;
  protected $meta;
 
  public function __construct($id, $text, array $meta)
  {
    $this->id = $id;
    $this->text = $text;
    $this->meta = $meta;
  }
 
  protected function retweet()
  {
    $this->meta['retweets']++;
  }
 
  protected function favourite()
  {
    $this->meta['favourites']++;
  }
 
  public function __get($property)
  {
    var_dump($this->$property);
  }
 
  public function __call($method, $parameters)
  {
    if (in_array($method, array('retweet', 'favourite')))
    {
      return call_user_func_array(array($this, $method), $parameters);
    }
  }
}
 
$tweet = new Tweet(123, 'hello world', array('retweets' => 23, 'favourites' => 17));
 
$tweet->retweet();
$tweet->meta; // array(2) { ["retweets"]=> int(24) ["favourites"]=> int(17) }


Еще один типичный пример это использование другого публичного API в своем обьекте.

class Location {
	protected $latitude;
	protected $longitude;
	
	public function __construct($latitude, $longitude)
	{
		$this->latitude = $latitude;
		$this->longitude = $longitude;
	}

	public function getLocation()
	{
		return array(
			'latitude' => $this->latitude,
			'longitude' => $this->longitude,
		);
	}
}
 
class Tweet {
	protected $id;
	protected $text;
	protected $location;

	public function __construct($id, $text, Location $location)
	{
		$this->id = $id;
		$this->text = $text;
		$this->location = $location;
	}
	public function  __call($method, $parameters)
	{
		if(method_exists($this->location, $method))
		{
		  return call_user_func_array(array($this->location, $method), $parameters);
		}
	}
}
 
$location = new Location('37.7821120598956', '-122.400612831116');
$tweet = new Tweet(123, 'Hello world', $location);
 
var_dump($tweet->getLocation()); // array(2) { ["latitude"]=> string(16) "37.7821120598956" ["longitude"]=> string(17) "-122.400612831116" }


В приведенном выше примере, мы можем вызвать метод getLocation на обьекте класса Tweet, но на самом деле мы его делегируем классу Location.
Если вы пытаетесь вызвать статический метод, вы можете так же воспользоваться __callStatic() магическим методом. Главное помните, что работает он лишь при вызове статичных методов.

Клонирование


Когда вы создаете копию объекта в PHP, по факту в переменную записывается не новый объект, а идентификатор, ссылающийся на оригинальный обьект. То есть любое изменение в ссылающимся объекте влечет за собой изменение первоначального объекта, однако удаление любого из объектов не повлияет на существование других:

$sheep1 = new stdClass;
$sheep2 = $sheep1;
 
$sheep2->name = "Polly";
$sheep1->name = "Dolly";
 
echo $sheep1->name; // Dolly
echo $sheep2->name; // Dolly


$a = new StdClass;
$b = $a;
$a = null;
var_dump($b); // object(stdClass)#1 (0) {  }


Для того чтобы создать копию обьекта вам следуюет использовать ключевое слово clone.

$sheep1 = new stdClass;
$sheep2 = clone $sheep1;
 
$sheep2->name = "Polly";
$sheep1->name = "Dolly";
 
echo $sheep1->name; // Dolly
echo $sheep2->name; // Polly


Однако, если у нас есть несколько связанных обьектов, зависимости, которые находятся в них, так же будут скопированы.

class Notification {
  protected $read = false;
 
  public function markAsRead()
  {
    $this->read = true;
  }
 
  public function isRead()
  {
    return $this->read == true;
  }
 
}
 
class Tweet {
  protected $id;
  protected $text;
  protected $notification;
 
  public function __construct($id, $text, Notification $notification)
  {
    $this->id = $id;
    $this->text = $text;
    $this->notification = $notification;
  }
 
  public function  __call($method, $parameters)
  {
    if(method_exists($this->notification, $method))
    {
      return call_user_func_array(array($this->notification, $method), $parameters);
    }
  }
}
 
$tweet1 = new Tweet(123, 'Hello world', new Notification);
$tweet2 = clone $tweet1;
 
$tweet1->markAsRead();
var_dump($tweet1->isRead()); // true
var_dump($tweet2->isRead()); // true


Для того чтобы решить данную проблему мы можем определить метод __clone() для того чтобы определить правильное поведение:

class Tweet {
 
  protected $id;
  protected $text;
  protected $notification;
 
  public function __construct($id, $text, Notification $notification)
  {
    $this->id = $id;
    $this->text = $text;
    $this->notification = $notification;
  }
 
  public function  __call($method, $parameters)
  {
    if(method_exists($this->notification, $method))
    {
      return call_user_func_array(array($this->notification, $method), $parameters);
    }
  }
 
  public function  __clone()
  {
    $this->notification = clone $this->notification;
  }
 
}
 
$tweet1 = new Tweet(123, 'Hello world', new Notification);
$tweet2 = clone $tweet1;
 
$tweet1->markAsRead();
var_dump($tweet1->isRead()); // true
var_dump($tweet2->isRead()); // false


Вызов обьекта как функции


Магический метод __invoke() позволяет определить логику работы обьекта, при попытке обратиться к обьекту как к функции.

class User {
 
  protected $name;
  protected $timeline = array();
 
  public function __construct($name)
  {
    $this->name = $name;
  }
 
  public function addTweet(Tweet $tweet)
  {
    $this->timeline[] = $tweet;
  }
 
}
 
class Tweet {
 
  protected $id;
  protected $text;
  protected $read;
 
  public function __construct($id, $text)
  {
    $this->id = $id;
    $this->text = $text;
    $this->read = false;
  }
 
  public function __invoke($user)
  {
    $user->addTweet($this);
    return $user;
  }
 
}
 
$users = array(new User('Ev'), new User('Jack'), new User('Biz'));
$tweet = new Tweet(123, 'Hello world');
$users = array_map($tweet, $users);
 
var_dump($users);


В данном примере я применяю обьект $tweet, как callback-функцию, ко всем значениям массива $users. В данном примере, мы добавим твит каждому пользователю. Согласен, данный пример является немного искусственным, однако я уверен, что вы действительно найдете применение этому методу.

Заключение


Как вы могли уже убедиться, PHP использует магические методы для реагирования на те или иные действия с методами или свойствами обьекта. Каждый из данных методов срабатывает автоматически, вы просто определяете что должно произойти, а об остальном позаботиться PHP.

В течении довольно длительного времени я не понимал истинного значения магических методов. Я думал, что они нужны, лишь для того чтобы делать какие-либо интересные вещи с обьектами. И когда я, наконец, понял их истинное предназначение, я смог писать более мощные обьекты в рамках более серьезных приложений.

Надеюсь, что в каждом из примеров, представленных в этом руководстве, я смог показать как магические методы могут помочь в повседневных задачах. И я буду не первым кто согласиться, что когда вам обьясняют какой-либо материал, но не обьясняют его практическое применение это реально раздражает.

Автор: Philip Brown
Оригинал: culttt.com/2014/04/16/php-magic-methods
Благодарности: werdender, HighQuality

P.S. Простите, знаю что перевод в некоторых местах довольно корявый. Если вы знаете как это звучало бы лучше — напишите мне, я попытаюсь это исправить.
Николаев Андрей @gromdron
карма
2,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (18)

  • +3
    Это типа для тех, кто не осилил мануал?
    • –1
      Вы видимо не читали. Целью автора была не скопировать мануал, а дополнить его примерами не искусственными, которые помогут понять смысл. Я не сомневаюсь в вашей компетенции как разработчика, но все ли смогут привести пример использования метода __invoke()?

      И да, новичкам оно тоже подойдет, иначе зачем бы я поставил флаг tutorial?

      P.S. Печально видеть осуждение от пользователя, который не потрудился написать ни одного полезного поста или комментария.
      • +5
        «Сначала добейся» — не лучший ответ.
        Tuman_2 прав, статья ни о чем. Примеров и так хватает. Я не умаляю вашу работу: перевод большого текста (и я не заметил сильных погрешностей в переводе) — дело не из легких. Но ценность статьи сомнительна.
        • 0
          Спасибо за наиболее развернутый ответ. И прошу прощенья, вспылил.
          Да, согласен, ценность статьи можно поставить под сомнение, но ведь ведь и изначально статья Филиппа Брауна не была расчитана на профессионалов. Скорее на новичков или немного более продвинутых, кому было бы неплохо закрепить свои навыки.

          А насчет ценности статьи, то для меня наиболее обьективный показатель ценности — это количество добавления в закладки, а их уже, ни много ни мало, 35 человек, что уже о чем-то да говорит.
          • +1
            Скорее на новичков или немного более продвинутых, кому было бы неплохо закрепить свои навыки.

            Просто последнее время на хабре куча статей «для новичков» и очень мало глубоких технических статей. Мое личное мнение: статьи «для новичков» допустимы только на те темы, о которых вообще мало материала в рунете и на хабре в частности.

            это количество добавления в закладки, а их уже, ни много ни мало, 35 человек, что уже о чем-то да говорит.

            Если я не ошибаюсь, то аудитория хабра — больше 5М уникумов в месяц.
      • +1
        При всем уважении к вашему труду, но вы видимо тоже не читали, т.к. говорите, что
        Целью автора была не скопировать мануал, а дополнить его примерами не искусственными, которые помогут понять смысл.
        , а написано:
        Согласен, данный пример является немного искуственным, однако я уверен, что вы действительно найдете применение этому методу.
        (это все о методе __invoke()) Ну это так, ответ на то, что вы считаете, что я не читал. А по делу ниже.

        Смысл в том, что нужно прочитать довольно длинную статью, с кучей примеров, чтобы получить пару мыслей (важность которых сомнительна) отличных от мануала. При этом, почерпнуть эти мысли в отдельном виде не получится.

        Мне скорее жалко ваш труд, так как текст переведен хорошо, но его значимость сомнительна. Еще мне жалко время, которые вы украли у меня этой статьей =)

        P.S. искуственным на искусственным исправьте.
        • 0
          Простите за украденное время и спасибо за оценку перевода и ошибочку.

          А насчет __invoke(), по крайней мере более вразумительный пример в документации.
    • –1
      Спасибо, что слили карму за этот коммент!
  • +1
    Тоже в некотором роде магический метод: www.php.net/manual/en/datetimeimmutable.modify.php
    • 0
      Да, как же уже все надоели с этим методом! Twitter, Facebook заполнены такими «шутками», а теперь и на Хабр просочилось… Никакой логической ошибки тут нет и быть не может т.к. возвращается новый объект, а старый остается без изменений, что является обычной практикой для Value Object к коим относится и DateTimeImmutable.
      • 0
        Но это же не modify, логика совсем другая. Почему метод не называется create, copyAndModify или cloneWithParameters?
        Более того, в документации написано, что этот метод «Alters the timestamp». Поэтому может поведение метода и соответствует семантике типа, но его название и тем более документация абсолютно неверные
  • 0
    Когда вы создаете копию обьекта в PHP, то сначала она становиться просто ссылкой на оригинальный обьект

    А потом? :)
    В общем-то и в оригинале не оч. точно написано, кстати.
    • 0
      Спасибо, исправлено.
      • 0
        Гмм… Вообще нет (и я не про «ться»). Хотя тут, наверное, неточность не у вас, а у автора оригинала.

        Когда вы создаете копию обьекта в PHP, то она становиться просто ссылкой на оригинальный обьект. То есть изменяя оригинальный обьект вы изменяете и его копию


        Тут не создается никакой копии объекта, никто не становится ссылкой на оригинальный объект, никакую копию вы не изменяете.
        Просто $sheep1 и $sheep2 содержат идентификаторы, которые указывают на один и тот же объект.

        А вот так они были бы ссылками:
        $sheep1 = &$sheep2


        Разница наглядно:
        image
  • 0
    Эх… а я надеялся увидеть примеры магии, именно полезные примеры. Proxy-объекты, декораторы/адаптеры и все такое прочее. Если очень хочется увидеть такую магию — рекомендую посмотреть исходники PhpSpec и в частности библиотеки для создания моков — prophecy. Вот там магии хватает.

    А использовать магию типа get/set/call я бы вообще не рекомендовал. Во всяком случае не для сущностей.
  • 0
    По мне так __toString один из самых полезных magic методов в php. Часто приходится делать что-то типа мелких процессоров для обработки текста и вызовы типа
    $a = (string) new Processor($b); 
    

    весьма удобны оказываются
    • 0
      Как раз на днях делал генератор xml отчетов с использованием этого метода. Для генерации отчета нужно было просто насоздавать экземпляров нужных классов и заполнить из свойства. И все. Потом просто передать этот объект в file_put_contents(), а дальше он сам сгенерит необходимую разметку. Очень удобно, что не надо вызывать отдельные методы типа ->toString() и т.п.
    • 0
      Весьма сомнительное решение если честно. Если бы класс назывался Report еще ладно…

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