Статьи

Расшифровка магических методов в PHP

PHP предоставляет несколько «волшебных» методов, которые позволяют вам делать довольно приятные трюки в объектно-ориентированном программировании. Эти методы, идентифицируемые префиксом с двумя подчеркиваниями (__), функционируют как перехватчики, которые автоматически вызываются при выполнении определенных условий. Магические методы предоставляют чрезвычайно полезную функциональность, и этот урок продемонстрирует использование каждого метода.


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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?php
class Device {
    public $name;
    public $battery;
    public $data = array();
    public $connection;
 
    protected function connect() {
        // connect to some external network
        $this->connection = ‘resource’;
        echo $this->name .
    }
 
    protected function disconnect() {
        // safely disconnect from network
        $this->connection = null;
        echo $this->name .
    }
}
 
class Battery {
    private $charge = 0;
 
    public function setCharge($charge) {
        $charge = (int)$charge;
        if($charge < 0) {
            $charge = 0;
        }
        elseif($charge > 100) {
            $charge = 100;
        }
        $this->charge = $charge;
    }
}
?>

Если такие слова, как «метод» и «свойство» кажутся вам чуждыми, вы можете сначала прочитать об этом.

Объекты устройства будут содержать имя, объект Battery, массив данных и дескриптор какого-либо внешнего ресурса. У них также есть методы для подключения и отключения внешнего ресурса. Объекты батареи просто хранят заряд в частной собственности и имеют метод для установки заряда.

В этом руководстве предполагается, что у вас есть базовое понимание объектно-ориентированного программирования. Если такие слова, как «метод» и «свойство» кажутся вам чуждыми, вы можете сначала прочитать об этом.

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


Конструкторы и деструкторы вызываются, когда объект создается и уничтожается соответственно. Объект «уничтожается», когда на него больше нет ссылок, либо потому, что переменная, содержащая его, была неустановлена ​​/ переназначена, либо сценарий завершил выполнение.

Метод __construct() является наиболее распространенным магическим методом. Здесь вы делаете любую инициализацию, которая вам нужна при создании объекта. Здесь вы можете определить любое количество аргументов, которые будут переданы при создании объектов. Любое возвращаемое значение будет передано через new ключевое слово. Любые исключения, сгенерированные в конструкторе, остановят создание объекта.

01
02
03
04
05
06
07
08
09
10
11
class Device {
    //…
    public function __construct(Battery $battery, $name) {
        // $battery can only be a valid Battery object
        $this->battery = $battery;
        $this->name = $name;
        // connect to the network
        $this->connect();
    }
    //…
}

Объявление метода конструктора «private» не позволяет внешнему коду напрямую создавать объект.

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

Совет : Объявление метода конструктора ‘private’ не позволяет внешнему коду напрямую создавать объект. Это удобно для создания одноэлементных классов, которые ограничивают количество объектов, которые могут существовать.

С указанным выше конструктором, вот как вы создаете устройство под названием «iMagic»:

1
2
3
4
$device = new Device(new Battery(), ‘iMagic’);
// iMagic connected
echo $device->name;
// iMagic

Как видите, аргументы, передаваемые классу, фактически передаются в метод конструктора. Вы также можете сказать, что был вызван метод connect и заполнено свойство $name .

Допустим, мы забываем передать имя. Вот что происходит:

1
2
$device = new Device(new Battery());
// Result: PHP Warning: Missing argument 2 for Device::__construct()

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

1
2
3
4
5
6
7
8
9
class Device {
    //…
    public function __destruct() {
        // disconnect from the network
        $this->disconnect();
        echo $this->name .
    }
    //…
}

С указанным выше деструктором, вот что происходит, когда объект Device уничтожается:

1
2
3
4
5
$device = new Device(new Battery(), ‘iMagic’);
// iMagic connected
unset($device);
// iMagic disconnected
// iMagic was destroyed

Здесь мы уничтожили объект с помощью unset() . Прежде чем он будет уничтожен, деструктор вызывает метод disconnect() и печатает сообщение, которое вы можете увидеть в комментариях.


Примечание . PHP-версия «перегрузки» не совсем такая же, как и в большинстве других языков, хотя можно достичь тех же результатов.

Этот следующий набор магических методов предназначен для работы с доступом к свойству, определения того, что происходит, когда вы пытаетесь получить доступ к свойству, которое не существует (или недоступно). Они могут быть использованы для создания псевдо-свойств. Это называется перегрузкой в ​​PHP.

Метод __get() вызывается, когда код пытается получить доступ к свойству, которое недоступно. Он принимает один аргумент, который является именем свойства. Он должен возвращать значение, которое будет рассматриваться как значение свойства. Помните свойство $data в нашем классе Device? Мы будем хранить эти «псевдо-свойства» как элементы в массиве данных, и мы можем позволить пользователям нашего класса обращаться к ним через __get() . Вот как это выглядит:

01
02
03
04
05
06
07
08
09
10
11
12
class Device {
    //…
    public function __get($name) {
        // check if the named key exists in our array
        if(array_key_exists($name, $this->data)) {
            // then return the value from the array
            return $this->data[$name];
        }
        return null;
    }
    //…
}

Популярное использование метода __get() — расширение контроля доступа путем создания свойств «только для чтения». Возьмем, к примеру, наш класс Battery, который имеет частную собственность. Мы можем разрешить частное свойство $charge читать из внешнего кода, но не изменять. Код будет выглядеть так:

01
02
03
04
05
06
07
08
09
10
11
class Battery {
    private $charge = 0;
 
    public function __get($name) {
        if(isset($this->$name)) {
            return $this->$name;
        }
        return null;
    }
    //…
}

В этом примере обратите внимание на использование переменных переменных для динамического доступа к свойству. Предполагая значение ‘user’ для $name , $this->$name переводится в $this->user .

Метод __set() вызывается, когда код пытается изменить значение недоступного свойства. Он принимает два аргумента: имя свойства и значение. Вот как это выглядит для массива «псевдопеременных» в нашем классе Device:

1
2
3
4
5
6
7
8
class Device {
    //…
    public function __set($name, $value) {
        // use the property name as the array key
        $this->data[$name] = $value;
    }
    //…
}

Метод __isset() вызывается, когда код вызывает isset() для свойства, которое недоступно. Он принимает один аргумент, который является именем свойства. Он должен возвращать логическое значение, представляющее существование значения. Снова используя наш массив переменных, вот как это выглядит:

1
2
3
4
5
6
7
8
class Device {
    //…
    public function __isset($name) {
        // you could also use isset() here
        return array_key_exists($name, $this->data);
    }
    //…
}

Метод __unset() вызывается, когда код пытается unset() свойство, которое недоступно. Он принимает один аргумент, который является именем свойства. Вот как выглядит наш:

1
2
3
4
5
6
7
8
class Device {
    //…
    public function __unset($name) {
        // forward the unset() to our array element
        unset($this->data[$name]);
    }
    //…
}

Вот все объявленные нами свойства магических методов:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Device {
    //…
    public $data = array();
    //…
    public function __get($name) {
        // check if the named key exists in our array
        if(array_key_exists($name, $this->data)) {
            // then return the value from the array
            return $this->data[$name];
        }
        return null;
    }
 
    public function __set($name, $value) {
        // use the property name as the array key
        $this->data[$name] = $value;
    }
 
    public function __isset($name) {
        // you could also use isset() here
        return array_key_exists($name, $this->data);
    }
 
    public function __unset($name) {
        // forward the unset() to our array element
        unset($this->data[$name]);
    }
    //…
}

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

1
2
3
$device->user = ‘Steve’;
echo $device->user;
// Steve

Мы установили и успешно получили значение несуществующего свойства. Ну, где это хранится тогда?

1
2
3
4
5
6
7
print_r($device->data);
/*
Array
(
    [user] => Steve
)
*/

Как видите, свойство $data теперь содержит элемент name с нашим значением.

1
2
var_dump(isset($device->user));
// bool(true)

Выше приведен результат вызова isset() для свойства fake.

1
2
3
unset($device->user);
var_dump(isset($device->user));
// bool(false)

Выше приведен результат сброса фальшивого свойства. Просто чтобы убедиться, вот наш пустой массив данных:

1
2
3
4
5
6
print_r($device->data);
/*
Array
(
)
*/

Иногда вы можете захотеть преобразовать объект в строковое представление. Если вы просто попытаетесь напечатать объект, мы получим ошибку, такую ​​как приведенная ниже:

1
2
3
$device = new Device(new Battery(), ‘iMagic’);
echo $device;
// Result : PHP Catchable fatal error: Object of class Device could not be converted to string

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

01
02
03
04
05
06
07
08
09
10
11
12
class Device {
    …
    public function __toString() {
        // are we connected?
        $connected = (isset($this->connection)) ?
        // how much data do we have?
        $count = count($this->data);
        // put it all together
        return $this->name .
    }
    …
}

С указанным выше методом, вот что происходит, когда мы пытаемся напечатать объект Device:

1
2
3
$device = new Device(new Battery(), ‘iMagic’);
echo $device;
// iMagic is connected with 0 items in memory

Объект Device теперь представлен краткой сводкой, содержащей имя, статус и количество сохраненных элементов.

Статический __set_state() (доступный с версии PHP 5.1) вызывается, когда var_export() функция var_export() для нашего объекта. Функция var_export() используется для преобразования переменной в код PHP. Этот метод принимает ассоциативный массив, содержащий значения свойств объекта. Для простоты, хорошо используйте его в нашем классе батареи.

1
2
3
4
5
6
7
8
9
class Battery {
    //…
    public static function __set_state(array $array) {
        $obj = new self();
        $obj->setCharge($array[‘charge’]);
        return $obj;
    }
    //…
}

Наш метод просто создает экземпляр своего родительского класса и устанавливает для зарядки значение в переданном массиве. С указанным выше методом, вот что происходит, когда мы используем var_export() для объекта Device:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
$device = new Device(new Battery(), ‘iMagic’);
var_export($device->battery);
/*
Battery::__set_state(array(
   ‘charge’ => 0,
))
*/
eval(‘$battery = ‘ . var_export($device->battery, true) . ‘;’);
var_dump($battery);
/*
object(Battery)#3 (1) {
  [«charge:private»]=>
  int(0)
}
*/

Первый комментарий показывает, что на самом деле происходит, то есть var_export() просто вызывает Battery::__set_state() . Второй комментарий показывает нам успешно воссоздание батареи.


Объекты по умолчанию передаются по ссылке. Таким образом, назначение других переменных объекту на самом деле не скопирует объект, а просто создаст новую ссылку на тот же объект. Чтобы действительно скопировать объект, мы должны использовать ключевое слово clone .

Эта политика передачи по ссылке также применяется к объектам внутри объектов. Даже если мы клонируем объект, любые дочерние объекты, которые он содержит, не будут клонированы. Таким образом, мы получим два объекта с одним и тем же дочерним объектом. Вот пример, который иллюстрирует это:

1
2
3
4
5
6
$device = new Device(new Battery(), ‘iMagic’);
$device2 = clone $device;
 
$device->battery->setCharge(65);
echo $device2->battery->charge;
// 65

Здесь мы клонировали объект Device. Помните, что все объекты Device содержат объект Battery. Чтобы продемонстрировать, что оба клона устройства используют одну и ту же батарею, изменение, внесенное нами в батарею $ device, отражается в батарее $ device2.

Для решения этой проблемы можно использовать метод __clone() . Он вызывается для копии клонированного объекта после клонирования. Здесь вы можете клонировать любые дочерние объекты.

1
2
3
4
5
6
7
8
class Device {
    …
    public function __clone() {
        // copy our Battery object
        $this->battery = clone $this->battery;
    }
    …
}

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

1
2
3
4
5
6
$device = new Device(new Battery(), ‘iMagic’);
$device2 = clone $device;
 
$device->battery->setCharge(65);
echo $device2->battery->charge;
// 0

Изменения батареи одного устройства не влияют на другое.


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

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

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

В нашем примере Device свойство connection представляет собой внешний ресурс, который нельзя сериализовать. Поэтому наш __sleep() просто возвращает массив всех свойств, кроме $connection .

01
02
03
04
05
06
07
08
09
10
11
12
class Device {
    public $name;
    public $battery;
    public $data = array();
    public $connection;
    //…
    public function __sleep() {
        // list the properties to save
        return array(‘name’, ‘battery’, ‘data’);
    }
    //…
}

Наш __sleep() просто возвращает список имен свойств, которые должны быть сохранены.

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

В нашем примере Device нам просто нужно восстановить наше соединение, вызвав наш метод connect() .

1
2
3
4
5
6
7
8
class Device {
    //…
    public function __wakeup() {
        // reconnect to the network
        $this->connect();
    }
    //…
}

Эти два последних метода предназначены для работы с методами. Это та же концепция, что и у методов перегрузки свойств ( __get() , __set() и т. Д.), Но она применяется к методам.

__call() вызывается, когда код пытается вызвать недоступные или несуществующие методы. Он принимает два аргумента: имя вызываемого метода и массив аргументов. Вы можете использовать эту информацию, например, для вызова того же метода в дочернем объекте.

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

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

01
02
03
04
05
06
07
08
09
10
11
12
class Device {
    //…
    public function __call($name, $arguments) {
        // make sure our child object has this method
        if(method_exists($this->connection, $name)) {
            // forward the call to our child object
            return call_user_func_array(array($this->connection, $name), $arguments);
        }
        return null;
    }
    //…
}

Вышеупомянутый метод будет вызван, если мы попытаемся вызвать метод iDontExist() :

1
2
3
$device = new Device(new Battery(), ‘iMagic’);
$device->iDontExist();
// __call() forwards this to $device->connection->iDontExist()

Функция __callStatic() (доступная с версии PHP 5.3) идентична __call() за исключением того, что она вызывается, когда код пытается вызвать недоступные или несуществующие методы в статическом контексте .

Единственное отличие в нашем примере состоит в том, что мы объявляем метод как static и ссылаемся на имя класса вместо объекта.

01
02
03
04
05
06
07
08
09
10
11
12
class Device {
    //…
    public static function __callStatic($name, $arguments) {
        // make sure our class has this method
        if(method_exists(‘Connection’, $name)) {
            // forward the static call to our class
            return call_user_func_array(array(‘Connection’, $name), $arguments);
        }
        return null;
    }
    //…
}

Вышеупомянутый метод будет вызван, если мы попытаемся вызвать статический iDontExist() :

1
2
Device::iDontExist();
// __callStatic() forwards this to Connection::iDontExist()

Иногда вы можете захотеть использовать объект как функцию. Возможность использовать объект в качестве функции позволяет передавать функции в качестве аргументов, как в других языках.

__invoke() (доступно с версии PHP 5.3) вызывается, когда код пытается использовать объект в качестве функции. Любые аргументы, определенные в этом методе, будут использоваться в качестве аргументов функции. В нашем примере мы просто напечатаем полученный аргумент.

1
2
3
4
5
6
7
class Device {
    //…
    public function __invoke($data) {
        echo $data;
    }
    //…
}

С учетом вышеизложенного, это то, что происходит, когда мы используем Устройство в качестве функции:

1
2
3
4
$device = new Device(new Battery(), ‘iMagic’);
$device(‘test’);
// equiv to $device->__invoke(‘test’)
// Outputs: test

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

Функция принимает один аргумент: имя ссылочного класса. Скажем, у вас есть каждый класс в файле с именем ‘classname.class.php’ в каталоге ‘inc’. Вот как будет выглядеть ваша автозагрузка:

1
2
3
4
function __autoload($class_name) {
    $class_name = strtolower($class_name);
    include_once ‘./inc/’ .
}

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