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

Источник: «PHP’s Magic Methods»
PHP разработчику, нужно знать множество особенностей языка, чтобы облегчить написание и сопровождение кода. Без явного рассказа о той или иной части языка сложно даже узнать о её существовании, поэтому сегодня поговорим о магических методах, которыми обладают классы, и о том, как их использовать при написании кода.

Что такое магические методы

Магические методы — особые методы, определённые в ядро языка PHP, вызываемые при выполнении определённых действий над объектом. Они позволяют отменить обычное взаимодействие PHP с объектом и внедрить вместо него собственную логику.

Магические методы имеют префикс из двух знаков подчёркивания (__) и обладают широким спектром применения. Все магические методы необязательны, так что не думайте, что вы обязаны их создавать. Они нужны для того, чтобы облегчить работу, а не для того, чтобы создавать лишнюю работу. По крайней мере, не специально.

__construct() и __destruct()

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

class User
{
public int $id;

public function __construct(public string $name)
{
$this->id = rand(1, 9999);
}
}

$testUser = new User("Scott Keck-Warren");
echo $testUser->id;

В отличие от других языков программирования, PHP допускает только один конструктор, поэтому, если необходимо иметь несколько способов инициализации класса, придётся либо прыгать через обручи, предоставляя внутреннюю логику, определяющую, что делать в зависимости от типов параметров, либо использовать паттерн Factory Method, предоставляющий статические функции для различных способов инициализации класса.

Функция __destruct используется, когда нет ссылок на определённый объект или во время завершения работы PHP. Это помогает очистить ресурсы, такие как соединения с внешними сервисами или указатели на файлы, которые класс мог создать в течение жизненного цикла.

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

<?php
class StartUpAndShutDown
{
public function __construct()
{
echo "__construct", PHP_EOL;
}

public function __destruct()
{
echo "__destruct", PHP_EOL;
}

public function doSomething(): void
{
echo "doSomething", PHP_EOL;
}
}

$testClass = new StartUpAndShutDown();
$testClass->doSomething();
unset($testClass);

В результате получается следующее:

__construct
doSomething
__destruct

__call() и __callStatic()

Следующие две функции, — это функции __call и __callStatic. Они вызываются PHP, когда происходит попытка вызова несуществующей функции или статической функции, поэтому вместо того, чтобы выдать фатальную ошибку, можно перехватить вызов и выполнить какое-то действие вместо неё.

<?php
class ClassWithCallAndStaticCall
{
public function __call(string $name, array $arguments): mixed
{
echo PHP_EOL, PHP_EOL;
echo $name, " ", var_export($arguments);

return null;
}

public static function __callStatic(string $name, array $arguments): mixed
{
echo PHP_EOL, PHP_EOL;
echo $name, " ", var_export($arguments);

return null;
}
}

$testClass = new ClassWithCallAndStaticCall();
$testClass->oldFunctionName();
ClassWithCallAndStaticCall::mispelledFunction();

Аргумент $name — имя вызываемого метода, а аргумент $arguments — массив, содержащий параметры, переданные функции, которую пытаются вызвать.

Существует несколько вариантов их использования, но я предпочитаю использовать их для написания функций, которые позволяют передавать параметр как часть имени функции. Таким образом, мы можем написать вызов функции типа whereName("Scott") и сделать его эквивалентом where("name", "Scott") — эта небольшая разница делает код немного легче для восприятия.

<?php
class ClassWithCallAndStaticCall
{
public function __call(string $name, array $arguments): mixed
{
if ($name == "whereName") {
$this->where("name", $arguments[0]);
return $this;
}

return $this;
}

public function where(string $key, string $value): void
{
var_dump("where {$key} = {$value}");
}
}

$testClass = new ClassWithCallAndStaticCall();
$testClass->whereName("Scott");

Другой вариант использования — возможность динамически переназначать вызов функции. Например, можно переименовать функцию (один из лучших инструментов рефакторинга в арсенале) и сохранить старое имя функции, но при этом сделать его "скрытым" от новой разработки. Это можно сделать с помощью функций __call() и __callStatic.

<?php
class OurClass
{
public function __call(string $name, array $arguments): mixed
{
if ($name == "oldName") {
$this->newName($arguments[0]);
return $this;
}

return $this;
}

public function newName(string $value): void
{
var_dump("newName with {$value}");
}
}

$testClass = new OurClass();
$testClass->oldName("Scott");

Огромный минус использования __call и __callStatic заключается в том, что редакторы (и любые инструменты статического анализа кода, например PHPStan) не будут о них знать. Чтобы это обойти, можно определить функцию как часть docBlock в начале определения класса.

/**
* @method oldMethodName(): void
*/

class OurClass {

}

__get(), __set(), __isset(), и __unset()

Следующая группа магических методов предоставляет логику для поддержки недоступных или несуществующих свойств. Магические методы __get(), __set() используются при чтении или записи, в недоступное или несуществующее свойство. Магический метод __isset() используется при вызове isset() или empty() для недоступного или несуществующего свойства. Магический метод __unset() вызывается при выполнении unset() для недоступного или несуществующего свойства.

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

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

<?php
class DynamicFields
{
public array $properties = [];

public function __set(string $name, mixed $value): void
{
$this->properties[$name] = $value;
}

public function __get(string $name): mixed
{
return $this->properties[$name];
}

public function __isset(string $name): bool
{
return isset($this->properties[$name]);
}

public function __unset(string $name): void
{
unset($this->properties[$name]);
}
}

$dynamicField = new DynamicFields();
$dynamicField->name = "Scott";
echo $dynamicField->name, PHP_EOL;
echo isset($dynamicField->name) ? "Yes" : "No", PHP_EOL;
unset($dynamicField->name);
echo isset($dynamicField->name) ? "Yes" : "No", PHP_EOL;

В результате будет выведено:

Scott
Yes
No

Другой вариант — переименовать свойство и предоставить доступ к старому имени, пока обновляется код.

<?php

class DeprecatedName
{
public string $email = "scott@phparch.com";

public function __set(string $name, mixed $value): void
{
if ($name == "emailAddress") {
$this->email = $value;
}
}

public function __get(string $name): mixed
{
if ($name == "emailAddress") {
return $this->email;
}
}
}

$deprecatedName = new DeprecatedName();
$deprecatedName->emailAddress = "updatedEmail@phparch.com";

// выводит: "updatedEmail@phparch.com"
echo $deprecatedName->email, PHP_EOL;

Опять, огромным минусом этих функций является то, что редакторы и инструменты статического анализа кода не будут знать об этих свойствах. Чтобы обойти это, можно определить свойства как часть docBlock в начале определения класса.

__serialize() и __unserialize()

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

В качестве примера далее приведён класс user.

<?php

class User
{
public string $password = "";

public function __construct(
public string $name,
public string $email
) {
$this->password = "originalPassword";
}

public function __serialize(): array
{
return [
"name" => $this->name,
"email" => $this->email,
];
}

public function __unserialize(array $data): void
{
$this->name = $data["name"];
$this->email = $data["email"];
$this->password = "Monkey1234!";
}
}

$user = new User("Scott", "scott@phparch.com");
echo $user->name, PHP_EOL;
echo $user->password, PHP_EOL;
$serilized = serialize($user);
$redone = unserialize($serilized);
echo $redone->name, PHP_EOL;
echo $redone->password, PHP_EOL;

Можно вызвать функцию serialize() для экземпляра класса и получить строку, представляющую класс. Обычно PHP просто берет все свойства и помещает их в строку. В данном случае был определён метод __serialize для класса, вызываемый для определения экспортируемых свойств.

Затем можно вызвать функцию unserialize() для строки и воссоздать класс в том виде, в котором он существовал. В данном случае для класса был определён метод __unserialize(), вызываемый вместо него с ассоциативным массивом, содержащим значения.

В результате будет получен следующий результат:

Scott
originalPassword
Scott
Monkey1234!

Также существует магический метод __set_state(), вызываемый так же, как и функция __unserialize, но используемый для воссоздания класса, экспортированного с помощью функции var_export(). Не буду показывать, как это работает, потому что очень сложно аргументировать, зачем это нужно большинству людей, когда есть лучшие варианты. Если ошибаюсь, напишите об этом в комментариях.

__toString()

Магический метод __toString используется, если необходимо преобразовать экземпляр класса в строку.

Это удобно, если хотите легко перевести класс в удобочитаемый формат. Иногда включаю этот метод в классы, к которым нужно применить некоторое форматирование, например в класс пользователя, где отдельно отслеживаются имя и фамилия. Затем с помощью метода __toString можно автоматически объединить их при выводе имени пользователя.

<?php

class StringableUser
{
public function __construct(private string $first, private string $last) { }

public function __toString(): string
{
return "{$this->first} {$this->last}";
}
}

$stringableUser = new StringableUser("Scott", "Keck-Warren");

// выводит: "Scott Keck-Warren"
echo $stringableUser, PHP_EOL;

__invoke()

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

Для примера, есть следующий код:

<?php

function displayInformation(Callable $func) {
echo "Calling Callable", PHP_EOL;
$func();
}

class Scott {
public function __invoke() {
echo "Keck-Warren";
}
}

$callableClass = new Scott();
echo displayInformation($callableClass);

Выведет следующее:

Calling Callable
Keck-Warren

__clone()

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

Например, в классе ниже отслеживается время создания класса. Когда класс клонируется, нужно обновить свойство created новым DateTimeImmutable[()], что легко реализуется в методе __clone().

<?php

class CloneableClass
{
public \DateTimeImmutable $created;

public function __construct()
{
$this->created = new \DateTimeImmutable();
}

public function __clone()
{
$this->created = new \DateTimeImmutable();
}
}

$original = new CloneableClass();
// выводит: 2023-12-28 12:14:35
echo $original->created->format("Y-m-d H:i:s"), PHP_EOL;
sleep(4);
$cloned = clone $original;
// выводит: 2023-12-28 12:14:39
echo $cloned->created->format("Y-m-d H:i:s"), PHP_EOL;

__debugInfo()

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

<?php

class User
{
private string $id = "neverShowThis";

public function __construct(public string $email, private string $password) { }

public function __debugInfo()
{
return [
"email" => $this->email,
"password" => "it's a secret",
];
}
}

$testUser = new User("scott@phparch.com", "mySecurePassword");
var_dump($testUser);

В результате выведет следующее:

object(User)#1 (2) {
["email"]=>
string(17) "scott@phparch.com"
["password"]=>
string(13) "it's a secret"
}

Что нужно знать

Дополнительные материалы

Предыдущая Статья

AbortController в JavaScript

Следующая Статья

Бесконечная прокрутка логотипов на чистом HTML и CSS