Эпизод 2. Классы – часть 2. Магические методы

Привет всем во втором эпизоде сериала “Сущности ООП”. Здесь я расскажу о магических методах классов. Несмотря на то, что приводимые здесь названия методов(и код) будет о пхп, данные методы есть и в других ЯП; конечно, если он поддерживает ООП, ну и, вероятно, под другим именем…

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

Это методы класса, которые по умолчанию создаются(и, по умолчанию, делают ничего!) для каждого класса. Их отличие от обычных методов в том, что они могут быть переписаны в этом же классе(как и в наследниках). Теперь главное – почему они магические? Связано это с тем, каким образом они вызываются: они работают не потому, что их явно вызвали в коде, а по срабатыванию определенного события. Эти методы нужно просто знать, ибо фреймворки часто их используют. Да, вероятно, для написания более понятного кода стоит использовать явные вызовы, но все-же – они есть и используются…

Касательно конкретно PHP – название этих методов начинается с двойного нижнего подчеркивания. Также, они могут иметь все три(описаны в предыдущей серии) модификатора области видимости; при этом: public ведет открыто и наследуется при событиях с наследником, protected – закрыто, но наследование сохраняется; private – события с наследниками их не дергают.

Создание и разрушение объекта

Магический метод , отслеживающий создание объекта с класса – наиболее часто используемый метод из всех – __construct. Немного о передаче переменных в конструктор: до PHP 8.0 было так

<?php
class Page
{
    public $name;
    public $id;
    
    public function __construct($name,$id)
    {
        $this->id = $id;
        $this->name  = $name;
    }
}

$page  = new Page('Главная', 1);

после(включая ее) cтал возможен такой синтаксис(помимо первого)

<?php
class Page
{
    public function __construct(public $name, public $id)
    {
    }
}

$page  = new Page('Главная', 1);

Он единственный кто кладет болт на правило совместимости сигнатуры при наследовании.

Антипод данного метода – __destruct – срабатывает каждый раз когда объект разрушается(в том числе и при завершении работы скрипта)

<?php
class Page
{
    public function __construct($name)
    {
        print_r('Объект создан'.PHP_EOL);
        print_r('Название страницы: '.$name.PHP_EOL);
    }

    public function __destruct()
    {
        print_r('Объект уничтожен'.PHP_EOL);
    }
}

$page  = new Page('Главная');
unset($page);
echo 'Страница';

/*
Вывод:
Объект создан
Название страницы: Главная
Объект уничтожен
Страница

Перечитывал написанное – вспомнил… вспомнил, что не уточнил – в конструктор может быть передан любой тип переменных кроме callable(груб говоря – функции).

Перегрузка(обращение к недоступным активам класса)

Под “недоступными активами класса” я подразумеваю несуществующие или недоступные в текущей области видимости свойства/методы.

Свойства

Начну с работы с недоступными свойствами. Для манипуляции с ними есть методы __set, __get, __unset и __isset. Первые два – это динамические сеттеры и геттеры:

<?php
class Page
{
    private $fields;

    public function __set($name, $value)
    {
        $this->fields[$name] = $value;
        print_r("Свойство [$name] установлено".PHP_EOL);
    }

    public function __get($name)
    {
        $value = $this->fields[$name];
        print_r("Свойство [$name] равно $value".PHP_EOL);
    }
}

$page  = new Page();
$page->name = 'Главная';
$page->name;

/*
Вывод:
Свойство [name] установлено
Свойство [name] равно Главная

Вторые два метода срабатывают когда происходим проверка на существование/пустоту недоступного свойства, а также его удаления

<?php
class Page
{
    private $fields;

    public function __set($name, $value)
    {
        $this->fields[$name] = $value;
    }

    public function __isset($name)
    {
        print_r('Вызван метод '.__METHOD__.PHP_EOL);
    }

    public function __unset($name)
    {
        print_r('Вызван метод '.__METHOD__.PHP_EOL);
    }
}

$page = new Page();
$page->name = 'Главная';
isset($page->name); // существует ли
empty($page->name); // пуст ли
unset($page->name); // уничтожить свойство

/*
Вывод:
Вызван метод Page::__isset
Вызван метод Page::__isset
Вызван метод Page::__unset

Методы

У методов все попроще, всего два один на обычные методы(__call) и один на статические(__callStatic)

<?php
class Page
{
    public function __call($name, $args)
    {
        print_r('Вызван метод '.__METHOD__.PHP_EOL);
        print_r($name.PHP_EOL);
        print_r($args);
    }

    public static function __callStatic($name, $args)
    {
        print_r('Вызван метод '.__METHOD__.PHP_EOL);
        print_r($name.PHP_EOL);
        print_r($args);
    }
}

$page = new Page();
$page->setTitle('Главная', 1);
Page::setTitle('Главная', 1);

/*
Вывод:
Вызван метод Page::__call
setTitle
Array
(
    [0] => Главная
    [1] => 1
)
Вызван метод Page::__callStatic
setTitle
Array
(
    [0] => Главная
    [1] => 1
)

Сериализация объектов

Сериализация — это процесс преобразования объекта в поток байтов для сохранения или передачи в память, базу данных или файл; вот такое определение дает Google этому процессу…

Прямой процесс – сериализация

В моем ЯП есть функция сериализации в поток байтов – serialize. На выходе будет строка

<?php
class Page
{
    public $id = 1;
    public $name = 'Главная';

    public function doSomeThink()
    {}
}

$page = serialize(new Page());
echo $page;

/*
Вывод:
O:4:"Page":2:{s:2:"id";i:1;s:4:"name";s:14:"Главная";}

Здесь целых два магических метода будут проверены – __sleep и __serialize; Если первый метод в объекте присутствует – он должен вернуть массив с именами свойств для сериализованного объекта

<?php
class Page
{
    public $id = 1;
    public $name = 'Главная';

    public function __sleep()
    {
        return ['id'];
    }
}

$page = serialize(new Page());
echo $page;

/*
Вывод:
O:4:"Page":1:{s:2:"id";i:1;}

Вторая функция, доступная с PHP 7.4, будет перезаписывать первую(если присутствуют обе) и должна возвращать массив “ключ – значение”, который и будет сериализован

<?php
class Page
{
    public $id = 1;
    public $name = 'Главная';

    public function __serialize()
    {
        return [
            'id' => 2,
            'name' => 'Карточка'
        ];
    }
}

$page = serialize(new Page());
echo $page;

/*
Вывод:
O:4:"Page":2:{s:2:"id";i:2;s:4:"name";s:14:"Карточка";}

Зачем это нужно? Классический пример – одно из полей объекта – ссылка на соединение с БД. Поскольку сериализованный объект не может хранить его, это поле не нужно.

Десериализация

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

<?php
class Page
{
    public $id = 1;
    public $name = 'Главная';

    public function __sleep()
    {
        return ['id'];
    }

    public function __wakeup()
    {
        $this->id = 5;
    }
}

print_r(unserialize('O:4:"Page":1:{s:2:"id";i:2;}'));

/*
Вывод:
Page Object
(
    [id] => 5
    [name] => Главная
)

Cоответственно, здесь можно восстановить соединение с БД. Однако же, в любом случаи, для правильной десериализации объекта – должен быть доступ к классу, с которого он был создан.

Иная магия

К рассмотрению осталось пять методов: __toString отрабатывает, когда объект пытаются представить в виде строки

<?php
class Page
{
    public $id = 1;
    public $name = 'Главная';

    public function __toString()
    {
        return "это обьект Главная Страница";
    }
}

echo new Page();

/*
Вывод:
это обьект Главная Страница

При экспорте свойств объекта – работает статический __set_state. Я не понял(и, пока что, не понимаю), что она делает и к чему это – поэтому пропустим ее. Третья – __invoke – срабатывает когда объект пытаются запустить как функцию

<?php
class Page
{
    public $id = 1;
    public $name = 'Главная';

    public function __invoke()
    {
        echo 'Главная страница';
    }
}

call_user_func(new Page());

/*
Вывод:
Главная страница
Process finished with exit code 0

Cледующий метод – метод __clone – срабатывает после того, как объект клонируют. Этот код увеличивает идентификатор объекта

<?php
class Page
{
    public $id = 1;
    public $name = 'Главная';

    public function __clone()
    {
        $this->id ++;
    }
}

print_r(clone new Page());

/*
Вывод:
Page Object
(
    [id] => 2
    [name] => Главная
)

Process finished with exit code 0

и последний – __debugInfo – создан для вывода объекта через var_dump. Эта функция показывает все поля объекта(публичные, скрытые…), но если объект имеет метод __debugInfo – можно задать свой вывод.

Эпизод 1. Классы – часть 1. Ключевые слова и модификаторы

Привет всем читателям! Это первый эпизод сериала “Сущности ООП”. Здесь я расскажу о классах: что это и какие модификаторы они имеют – но поскольку о них можно поговорить побольше, то это всего лишь первая часть.

Классы

Как сообщает нам Википедия: класс – это шаблон для создания объекта; но мне более нравится: Класс – описание структуры объекта и методов работы с ним – оно более емкое. Если просто говорить – это набор свойств и методов, которые при использовании определенных модификаторов могут быть скрыты в объекте, а также вызваны без создания объекта; также есть вариант, что класс не может порождать объект – здесь мы все это и обсудим…

Класс, не умеющий рождать объект

Обычный класс делать это умеет(“Process finished with exit code 0” – говорит об отсутствии ошибок)

<?php
class Page
{}

$page = new Page();

// OUTPUT:
// Process finished with exit code 0

но если перед class словом добавить abstract – он потеряет данную возможность. Класс не умеющий рождать объекты? зачем? как с ними работать?

<?php
abstract class Page
{}

$page = new Page();

// OUTPUT:
// Fatal error: Uncaught Error: Cannot instantiate abstract class Page
// Process finished with exit code 255

Начнем по порядку – зачем? Полазив на разных сайтах(блогах, форумах…), посмотрев разные мнения и учев свои опыт и взгляд – пришел к выводу – обеспечение полиморфизма классов(принципы OCP, LSP – реализация в виде паттернов); и, мало не забыл сказать, что важно – абстрактные классы могут содержать абстрактные свойства и методы(читай далее). Теперь как с ними играть: наследовать, наследовать и только наследовать!

<?php
abstract class Page
{}

class MainPage extends Page
{}

$page = new MainPage();

// OUTPUT:
// Process finished with exit code 0

Модификаторы доступности свойств и методов

И методы, и свойства имеют одинаковый набор модификаторов открывающих им определенную область видимости – public, рrotected и private. Первый модификатор(public)

<?php
class Page
{
    public $id;

    public function getId()
    {
        return $this->id;
    }
}

class MainPage extends Page
{
    public function index()
    {
        $id = $this->id;
        $id = $this->getId();
    }
}

$page = new MainPage();
$page->index();
$id = $page->getId();
$id = $page->id;

// OUTPUT:
// Process finished with exit code 0

открывает полную область – как внутри объекта класса и классов-наследников, так и при обращении к объекту вне класса; второй – только внутри себя

<?php
class Page
{
    protected $id;

    protected function getId()
    {
        return $this->id;
    }
}

class MainPage extends Page
{
    public function index()
    {
        $id = $this->id;
        $id = $this->getId();
    }
}

$page = new Page();
$page->index();
$id = $page->getId();// error
$id = $page->id;// error

// OUTPUT:
// Fatal error: Uncaught Error: Call to protected method Page::getId() from context ''
// line 23
// Process finished with exit code 255

и классов-наследников; и третий – только внутри себя.

<?php
class Page
{
    private $id;

    private function getId()
    {
        return $this->id;
    }
}

class MainPage extends Page
{
    public function index()
    {
        $id = $this->id;// error
        $id = $this->getId();// error
    }
}

$page = new MainPage();
$page->index();
$id = $page->getId();// error
$id = $page->id;// error

// OUTPUT:
// Fatal error: Uncaught Error: Call to private method Page::getId() from context 'MainPage'
// line 17
// Process finished with exit code 255

Кстати, у функции(метода) класса слово отвечающее за область видимости может отсутствовать – тогда ее область видимости равна public.

Ключевые слова в свойствах и методах

С одним ключевым словом(abstract) уже разобрались. Эти слова являются дополнительными(необязательными) инструкциями к тому, как работают свойства и методы. Начнем их рассмотрение с директивы static – она позволяет доступ к активу не создавая объект; иначе говоря – прикрепляет функцию внутрь класса.

<?php
class Page
{
    public static $id;
    
    public static function getId()
    {}
}

Page::$id;
Page::class;
Page::getId();

$page = new Page();
$page::$id;
$page::class;
$page::getId();

// OUTPUT:
// Process finished with exit code 0

Кста, внутри класса автоматически создаётся статическая публичная переменная class, содержащая имя класса с его неймспейсом.

И последний модификатор(для пхп) это final – говорит о том, что при наследовании класса данный метод не возможно переопределить.