Для понимания того, как соотносятся друг с другом внутренности классов которые связаны наследованием, нужно разобраться с таким понятием как позднее связывание (late binding).

Вспомним базовый класс HTMLElement из прошлого урока. Внутри него активно используется $this для обращения к свойствам:

<?php

// Базовый класс для всех тегов. Умеет работать с аттрибутами.
class HTMLElement
{
    public $attributes = [];

    public function __construct($attributes = [])
    {
        $this->attributes = $attributes;
    }

    public function getAttribute(string $key)
    {
        return $this->attributes[$key];
    }
}

Предположим, что мы создаем объект класса HTMLAnchorElement (который наследуется от HTMLElement). Тогда объектом какого класса будет $this внутри методов родительского класса? Правильный ответ: HTMLAnchorElement, то есть того класса, объект которого мы прямо сейчас создаем.

Эта особенность $this называется поздним связыванием. Оно означает, что на момент определения класса, тип $this не известен. В качестве текущего объекта может выступать объект любого класса, наследуемого от текущего. Все выглядит так, как будто весь код внутри базового класса, скопировали и перенесли в каждый класс наследник. Для позднего связывания не важно насколько глубокая иерархия наследования. $this всегда будет объектом того класса, который конструируется в коде.

Позднее связывание не просто теоретическое понятие, раскрывающее особенности реализации. Оно дает понимание того, что у объектов нет разных уровней, нет свойств в разных классах. Все что есть внутри объекта, принадлежит самому объекту. Любые свойства определенные в любом классе иерархии – часть объекта.

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

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

Возьмем для примера наши теги. Посмотрите на метод __toString(). Видно, что его код, останется идентичным для большинства тегов. Единственное что меняется – название самого тега.

<?php

class HTMLAnchorElement extends HTMLElement
{
    public function __toString()
    {
        // Родительский метод
        $attrLine = $this->stringifyAttributes();
        // Родительский метод
        $body = $this->getTextContent();
        return "<a{$attrLine}>{$body}</a>"
    }
}

Мы можем модифицировать код так, что метод __toString() переместится в HTMLElement. И единственная вещь, которая останется за подклассами – имя тега:

<?php

class HTMLElement
{
    public function __toString()
    {
        $attrLine = $this->stringifyAttributes();
        $body = $this->getTextContent();
        // getTagName – метод, который должны реализовать все подклассы
        $tagName = $this->getTagName();
        return "<{$tagName}{$attrLine}>{$body}</{$tagName}>"
    }
}

Получившийся код лучше исходного варианта, так как он значительно сокращает дублирование (тегов около 100 штук!). Но есть одна загвоздка. Теги бывают одиночные. Из этой ситуации можно выйти разными способами, например, с помощью наследования. Создадим у HTMLElement два подкласса: один HTMLSingleElement и HTMLPairElement. Теперь классы конкретных тегов, должны наследоваться от одного из указанных классов. В каждом из этих классов будет своя реализация метода __toString().

<?php

class HTMLSingleElement extends HTMLElement
{
    public function __toString()
    {
        $attrLine = $this->stringifyAttributes();
        // getTagName – метод, который должны реализовать все подклассы
        $tagName = $this->getTagName();
        // Создается одиночный тег
        return "<{$tagName}{$attrLine}>"
    }
}

Несмотря на различия в реализации __toString(), оба этих подкласса требуют от своих наследников реализации одного и того же метода getTagName().

Кстати, внимательный читатель заметит. Что в исходном классе HTMLElement есть метод setTextContent(), который устанавливает тело внутрь тега. Но мы знаем что у одиночных тегов тела быть не может. Следовательно этот метод, должен переехать в класс HTMLPairElement.


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

  1. Позднее связывание (Wiki)

Для продолжения нужно перейти в курс и вступить в него.