Магические методы 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"
}
Что нужно знать
- Магические методы — методы, которые можно определить для своих классов.
- Прерывают стандартную логику PHP
- Множество различных способов и вариантов использования