Алгоритм наследования классов на PHP

наследование классов на PHPСодержание:

Хранение переменных в PHP

Прежде чем перейдем к рассмотрению классов и алгоритма их наследования, мы посмотрим, как PHP хранит переменные в памяти.

Переменные хранятся в таблице переменных (simbol_table), которая представляет из себя ассоциативный массив, ключом элемента массива является имя переменной, а в значении хранится ссылка на контейнер переменной или zval контейнер, в котором хранятся значение переменной, его тип и дополнительная информация о количестве ссылок на переменную. zval контейнер представляет структуру:

typedef struct _zval_struct {
    zvalue_value value;
    zend_uint refcount__gc;
    zend_uchar type;
    zend_uchar is_ref__gc;
} zval;

zvalue_value хранит само значение переменной и представляет структуру объединение (union):

typedef union _zvalue_value {
    long lval;
    double dval;
    struct {
        char *val;
        int len;
    } str;
    HashTable *ht;
    zend_object_value obj;
} zvalue_value;

В зависимости от типа значения, возможен доступ только к одному полю. Тип переменной хранится в zval.type и может иметь следующие значения:

Поле type используемое поле для хранения данных
IS_NULL нет
IS_BOOL long lval
IS_LONG long lval
IS_DOUBLE double dval
IS_STRING struct { char *val; int len; } str
IS_ARRAY HashTable *ht
IS_OBJECT zend_object_value obj
IS_RESOURCE long lval

Теперь мы имеем представление об организации хранения переменных в PHP. Более подробно, особенно про использование refcount__gc и is_ref__gc смотрите в источниках [1,2 и 5]. В дальнейшем я буду ссылаться на zvalконтейнер и вы уже будете знать что это такое и как используется.

Резюме

  1. Каждая переменная, независимо от типа данных, хранится в едином контейнере, называемый zval.
  2. Этот контейнер имеет все необходимые параметры для работы с переменными во всех возможных режимах.

Простой класс в PHP

Для начала рассмотрим, как же PHP хранит классы. Пример:

class A {
    private $count = 1;

    public function getCount() {
        return $this->count;
    }
}

Кстати, данный простейший класс из нашего примера в памяти занимает 1160 Байт.

PHP согласно [3] считается транслирующим интерпретатором. Наш скрипт при загрузке в память транслируется в байт-коды и затем полученные байт-коды интерпретируются. Но такие крупные объекты, как классы, в ходе трансляции формируются в определенную структуру zend_class_entry, который хорошо описан в [1] и [3]. А коды методов транслируются в байт коды. Схематично данную структуру можно представить следующим образом:

Если внимательно посмотрите на источники, указанные выше, структура zend_class_entry представляет собой солидный объект, где учтены всевозможные варианты использования классов. Я на рисунке указал только те параметры класса, которые могут быть интересны в рамках настоящей статьи.

Разберем структуру.

name — название класса.

parent — ссылка на класс родителя. В нашем случае NULL, поскольку наш класс ни от кого не наследуется.

function_table — массив методов класса. Каждая ячейка этого массива ссылается на объект метода, который описывается структурой zend_function. Эта структура (в упрощенном виде) имеет поля function_name — имя функции, class — ссылка на объект класса, op_array — массив байт-кодов функции и fn_flags, который имеет биты, определяющие область видимости функции (public, protected или private). Также структура function_table содержит поля, описывающие аргументов, их количество, количество обязательных аргументов, список аргументов, в котором каждый аргумент описывается отдельно и другие параметры, но они в этой статье не указаны, поскольку порядок передачи и использования аргументов в методах здесь не рассматриваются.

properties_info — массив описаний свойств класса. Каждая ячейка этого массива ссылается на описание свойства класса.

Описание свойства класса представлено структурой zend_property_info. Эта структура содержит поля name— имя свойства, class — ссылка на объект класса, offset — индекс в массиве значений по умолчанию и flags — область видимости свойства (public, protected или private).

default_properties_table — массив значений по умолчанию. Каждая ячейка массива ссылается на контейнер zval, который содержит заданное в коде класса значение. В нашем примере это код

private $count = 1;

Параметр offset в структуре properties_info как раз содержит индекс этого массива. Почему так сделано, будет понятно далее, когда мы перейдем на рассмотрение объектов PHP.

Хоть в статье и не используются, но я показал в структуре класса два параметра, это static_members_table — таблица статических свойств и constants_table — таблица констант класса. В нашем примере статические свойства и константы не используются и поэтому их значение NULL.

Примечание. Поля fn_flags и flags кроме модификатора доступа хранят информацию об статических (static) методах или свойствах.

Резюме

  1. Каждый класс, каждый метод и каждое свойство хранятся в соответствующих структурах и имеют все необходимые параметры для работы с ними.
  2. Каждая структура, описывающая метод или свойство, имеет обратную ссылку на свой класс, тем самым принадлежность метода или свойтсва четко отслеживается.

Наследование классов

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

class A {
    private $count = 1;

    public function getCount() {
        return $this->count;
    }
}

class B extends A {}

Пока что наш класс пустой и все поля, ссылающиеся на массив методов и свойств, имеют значение NULL. В памяти теперь содержатся 2 структуры класса и поле parent класса В теперь ссылается на структуру класса А. Добавим в наш класс В свойство и новый метод.

class A {
    private $count = 1;

    public function getCount() {
        return $this->count;
    }
}

class B extends A {
    public $count=5;

    public function getCountB() {
        return $this->count;
    }
}

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

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

В памяти в это время структура класса В стал похож на структуру класса А из рисунка выше и соответственно поле parent ссылается на класс А.

Резюме

  1. Каждый класс хранится в структуре zend_class_entry и его свойства и методы в соответствующих структурах.
  2. При наследовании классов, поле parent наследуемого класса ссылается на структуру класса родителя.

Объекты

В наш код вводим переменные, а именно создадим экземпляр объекта класса В. Код:

$obj = new B();
var_dump($obj);

Вывод:

object(B)#1 (2) {
  ["count"]=>
  int(5)
  ["count":"A":private]=>
  int(1)
}

Как видите, в объекте хранятся свойства обоих классов со своими значениями. Но второе свойство имеет указание, что это свойство класса А.

При создании нашего объекта что происходит:

  1. В памяти создается структура объекта zend_object.
  2. В поле ce вносится ссылка на класс В.
  3. Поле properties содержит массив объявленных свойств во всех классах, начиная с родительского. Каждый элемент массива содержит ссылку в объект zend_property_info классов.
  4. А поле properties_table является символьной таблицей объекта и содержит ссылки на zval контейнеры. При этом ключами элементов массива являются названия свойств. Обратите внимание, первый элемент обозначен как \0A\0сount (здесь \0 обозначает символ #00, то есть нулевой байт). В этой записи А означает название класса А, а сount соответственно название свойства. Такая запись означает, что count является приватным свойством класса А и доступен только методам данного класса. Второй элемент просто обозначен своим названием, поскольку он публичное свойство и второго такого во всей цепочке не может быть. Что интересно, если свойство count класса В обозначить как protected, то запись в properties_table была бы такой \0 * \0сount, вместо названия класса знак *. Это значит, что свойство доступно всем классам цепочки, но не извне. Такое свойство извне не видно. Кстати, эти же записи показаны в выводе функции var_dump($obj);, только в более читабельном виде.
  5. Поле guards используется для защиты от циклических вызовов в методах объекта.
  6. Созданный объект размещается в хранилище объектов zend_object_store, который обеспечивает хранение объекта в единственном экземпляре. Здесь эту структуру рассматривать не будем, за подробностями обратитесь к источникам.
  7. Объект привязывается к имени переменной.

Дополним наш код:

echo $obj->getCount(); // выводит 1
echo $obj->getCountB(); // выводит 5

Почему так происходит. При обращении к переменной в методе класса, PHP начинает просмотр массива поля properties_table объекта. Расшифровывает запись \0A\0сount, получает имя класса А и имя переменной count. При случае, если это метод getCount(), то все условия выполняются и значение переменной по ссылке проставляется в выражение. Во второй строке кода запись \0A\0сount не удовлетворяет условиям (метод getCountB() относится к классу В) и PHP в выражении использует значение публичного свойства count, который удовлетворяет всем условиям.
Обратите внимание, на рисунке элементы массива properties_table ссылаются на zval контейнеры значений по умочанию соответствующих классов. Вспомните, раздел настоящей статьи Простой класс в PHP. Для того, чтобы не расходовать лишнюю память, первоначально объект ссылается на значения по умолчанию класса. Но стоит нам изменить какое-либо свойство, в памяти создается новый zval контейнер с новым значением.

А теперь попробуем нашему объекту назначить динамическое свойство. Код:

$obj->newVar = "Это свойство не определено в классах А и В.";
var_dump($obj);

Вывод:

object(B)#1 (3) {
  ["count":protected]=>
  int(5)
  ["count":"A":private]=>
  int(1)
  ["newVar"]=>
  string(77) "Это свойство не определено в классах А и В."
}

Как видите, наше свойство newVar успешно добавлено в объект и оно определено как публичное свойство (public). Новое свойство добавилось в таблицу свойств properties_table как новый элемент массива со своим контейнером zval.

А как объекты видят статические свойства? Изменим код нашего класса В:

class B extends A {
    public $count=5;
    static $staticVar = 7;

    public function getCountB() {
        return $this->count;
    }
}

$obj = new B();
var_dump($obj);

Вывод:

object(B)#1 (2) {
  ["count"]=>
  int(5)
  ["count":"A":private]=>
  int(1)
}

Как видите, в таблице свойств объекта статического свойства нет. И, соответственно, обращение к этому свойству от объекта приведет к ошибке:

echo $obj->staticVar;

Вывод:

PHP Notice:  Undefined property: B::$staticVar in /home/shah/projects/objects/test1.php on line 21

Резюме

  1. Объекты, в отличие от классов, занимают очень мало места в памяти. Правда, по мере присваивания новых значений свойствам объекта, под них выделяются отдельная память и размер объекта увеличивается.
  2. Объект хранит ссылки на структуру описание свойства классов, и отдельно, таблицу переменных объекта.
  3. При обращении к свойствам в методах объекта, элементы массива перебираются последовательно и первое свойство, отвечающее всем условиям (имя переменной, видимость для метода текущего класса) используется в выражении.
  4. Статические свойства доступны только при обращении как к свойству класса $A::staticVar.

Оставить комментарий