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

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

Вот список того, о чем я расскажу вам в этой статье:

  • Конструкторы и деструкторы, которые позволяют назначить определенные действия объекту при его создании и удалении;
  • Статические поля и методы - это такие поля и методы, которые не связаны с конкретными объектами класса;
  • Константы класса, удобные для хранения фиксированных значений, относящихся к определенному классу;
  • Явное указание типа, используемое для задания ограничения типов параметров, которые можно передавать в тот или иной метод;
  • Специальные методы __get() и __set(), которые используются для задания и чтения значений полей классов;
  • Специальный метод __call(), применяемый для вызова метода класса.

Вы готовы? Тогда вперед!

Конструкторы и деструкторы

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

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

На заметку: как удалить объект? PHP автоматически удаляет объект из памяти, когда не остается ни одной переменной, указывающей на него. Например, если вы создадите новый объект и сохраните его в переменной $myObject, а затем удалите ее с помощью метода unset($myObject), то сам объект также удалится. Также, если вы создали локальную переменную в какой-либо функции, она (вместе с объектом) удалится, когда функция завершит работу.

В PHP есть два специальных метода, которые можно применять для совершения определенных действий по созданию и удалению объектов:

  • Конструктор вызывается сразу после того, как вы создали объект;
  • Деструктор вызывается строго перед тем, как объект удаляется из памяти.

Работа с конструкторами

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

Чтобы создать конструктор, добавьте в ваш класс специальный метод __construct() (перед словом construct - два символа подчеркивания). PHP автоматически вызовет этот метод при реализации вашего класса, то есть, при создании объекта этого класса.

Вот пример конструктора:

Class MyClass { public function __construct() { echo "I"ve just been created!"; } } $myObject = new MyClass(); // отобразит "I"ve just been created!"

В классе MyClass есть конструктор, который выводит на страницу строку "I"ve just been created!". Последняя строка кода создает новый объект класса MyClass. Когда это происходит, PHP автоматически вызывает конструктор, и сообщение отображается в браузере. Теперь на практике - инициализация полей класса:

Class Member { private $username; private $location; private $homepage; public function __construct($username, $location, $homepage) { $this->username = $username; $this->location = $location; $this->homepage = $homepage; } public function showProfile() { echo "

"; echo "
Username:
$this->username
"; echo "
Location:
$this->location
"; echo "
Homepage:
$this->homepage
"; echo "
"; } } $aMember = new Member("fred", "Chicago", "http://example.com/"); $aMember->showProfile();

Данный скрипт отобразит на странице следующее:

Username: fred Location: Chicago Homepage: http://example.com/

В нашем классе Member есть три поля и конструктор, который принимает в качестве параметров 3 значения - по одному для каждого поля. Конструктор назначит полям объекта значения, полученные в качестве аргументов. В классе также есть метод для отображения на странице значений полей объекта.

Затем в коде создается объект класса Member, в который мы передаем 3 значения "fred", "Chicago", и "http://example.com/", так как конструктор принимает именно 3 параметра. Конструктор записывает эти значения в поля созданного объекта. В завершение, вызывается метод showProfile() для созданного объекта, чтобы отобразить полученные значения.

Работа с деструкторами

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

Class MyClass { public function __destruct() { echo "I"m about to disappear - bye bye!"; // (очистить память) } } $myObject = new MyClass(); unset($myObject); // отобразит "I"m about to disappear - bye bye!"

Мы создали простенький деструктор, который отображает на странице сообщение. Затем мы создали объект нашего класса и сразу же удалили его, вызвав метод unset() для переменной, которая ссылается на объект. Перед самым удалением объекта вызвался деструктор, который отобразил в браузере сообщение "I"m about to disappear - bye bye!".

На заметку: в отличие от конструкторов, в деструкторы нельзя передавать никакие параметры.

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

Class MyClass { public function __destruct() { echo "I"m about to disappear - bye bye!"; // (очистить память) } } $myObject = new MyClass(); exit; // отобразит "I"m about to disappear - bye bye!"

Также, если работа скрипта прекратится из-за возникшей ошибки, деструктор тоже вызовется.

На заметку: при создании объектов класса-наследника, конструкторы класса-родителя не вызываются автоматически. Вызывается только конструктор самого наследника. Тем не менее вы можете вызвать конструктор родителя из класса-наследника таким образом:

parent::__construct(). То же самое касается деструкторов. Вызвать деструктор родителя можно так: parent:__destruct(). Я расскажу вам о классах-родителях и наследниках в следующем уроке, посвященном наследованию.

Статические поля класса

Мы рассмотрели статические переменные в статье PHP Variable Scope: All You Need to Know. Как обычная локальная переменная, статическая переменная доступна только в пределах функции. Тем не менее, в отличие от обычных локальных, статические переменные сохраняют значения между вызовами функции.

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

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

Чтобы создать статическую переменную, добавьте ключевое слово static в ее задании:

Class MyClass { public static $myProperty; }

Вот пример того, как работают статические переменные:

Class Member { private $username; public static $numMembers = 0; public function __construct($username) { $this->username = $username; self::$numMembers++; } } echo Member::$numMembers . "
"; // отобразит "0" $aMember = new Member("fred"); echo Member::$numMembers . "
"; // отобразит "1" $anotherMember = new Member("mary"); echo Member::$numMembers . "
"; // отобразит "2"

Есть несколько интересных вещей, так что давайте разберем данный скрипт:

  • В классе Member два поля: частное поле $username и статическое $numMembers, которое изначально получает значение 0;
  • Конструктор получает в качестве параметра аргумент $username и устанавливает полю только что созданного объекта значение этого параметра. В то же время, он инкрементирует значение поля $numMembers, тем самым давая понять, что число объектов нашего класса увеличилось на 1.

Отметьте, что конструктор обращается к статическому полю так: self::$numMembers. Ключевое слово self похоже на $this, которое мы рассмотрели в прошлом уроке. Тогда как $this ссылается на текущий объект, self - на текущий класс. Также тогда как для получения доступа к полям и методам объекта вы используете ->, то в этом случае используйте:: для получения доступа к полям и методам класса.

  • В завершении скрипт создает несколько объектов класса Member и отображает на странице их количество, т.е. значение статической переменной $numMembers. Отметьте, что данная переменная сохраняет свое значение на протяжении всей работы скрипта, несмотря на объекты класса.

Итак, чтобы получить доступ к статическому полю класса, применяйте оператор::. Здесь мы не можем воспользоваться ключевым словом self, так как код находится за пределами класса, поэтому мы пишем имя класса, затем::, а затем имя поля (Member::$numMembers). В пределах конструктора тоже нужно использовать именно такую структуру, а не self.

На заметку: нашему скрипту ничего не стоило получить доступ к полю класса $numMembers перед тем, как создался первый объект данного класса. Нет необходимости создавать объекты класса для того, чтобы пользоваться его статическими полями.

Статические методы

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

Чтобы создать статический метод, нужно добавить в его объявлении ключевое слово static:

Class MyClass { public static function myMethod() { // (действия) } }

В нашем предыдущем примере, касающемся статических полей, было статическое поле $numMembers. Делать поля частными, а методы для доступа к ним - открытыми, - это хорошая практика. Давайте сделаем наше статическое поле частным и напишем статический метод public для получения значения данного поля:

Class Member { private $username; private static $numMembers = 0; public function __construct($username) { $this->username = $username; self::$numMembers++; } public static function getNumMembers() { return self::$numMembers; } } echo Member::getNumMembers() . "
"; // отобразит "0" $aMember = new Member("fred"); echo Member::getNumMembers() . "
"; // отобразит "1" $anotherMember = new Member("mary"); echo Member::getNumMembers() . "
"; // отобразит "2"

Здесь мы создали статический метод getNumMembers(), который возвращает значение статического поля $numMembers. Мы также сделали это поле частным, чтобы нельзя было получить его значение извне.

Мы также изменили код вызова, применив метод getNumMembers() для получения значения поля $numMembers. Отметьте, можно вызывать данный метод без того, чтобы создавать объект класса, потому что метод - статический.

Константы класса

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

Определить классовую константу можно с помощью ключевого слова const. Например:

Class MyClass { const CONSTANT_NAME = value; }

Обратиться в последствии к классовой константе можно через имя класса и оператор::. Например, так:

MyClass::CONSTANT_NAME

На заметку: как и в случае со статическими полями и методами, вы можете обратиться к константе через ключевое слово self.

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

Class Member { const MEMBER = 1; const MODERATOR = 2; const ADMINISTRATOR = 3; private $username; private $level; public function __construct($username, $level) { $this->username = $username; $this->level = $level; } public function getUsername() { return $this->username; } public function getLevel() { if ($this->level == self::MEMBER) return "a member"; if ($this->level == self::MODERATOR) return "a moderator"; if ($this->level == self::ADMINISTRATOR) return "an administrator"; return "unknown"; } } $aMember = new Member("fred", Member::MEMBER); $anotherMember = new Member("mary", Member::ADMINISTRATOR); echo $aMember->getUsername() . " is " . $aMember->getLevel() . "
"; // отобразит "fred is a member" echo $anotherMember->getUsername() . " is " . $anotherMember->getLevel() . "
"; // отобразит "mary is an administrator"

Мы создали три классовые константы: MEMBER, MODERATOR и ADMINISTRATOR, и задали им значения 1, 2 и 3 соответственно. Затем мы добавляем поле $level для хранения ролей и немного изменяем конструктор так, чтобы инициализировать еще и это поле. В классе также появился еще один метод - getLevel(), который возвращает определенное сообщение в зависимости от значения поля $level. Он сравнивает это значение с каждой из классовых констант и возвращает нужную строку.

Скрипт создает несколько объектов с разными ролями. Для задания объектам ролей используются именно классовые константы, а не простые численные значения. Затем идут вызовы методов getUsername() и getLevel() для каждого объекта, и результаты отображаются на странице.

Явное указание типов аргументов функций

В PHP можно не задавать типы данных, так что можно не переживать о том, какие аргументы вы передаете в методы. Например, вы можете спокойно передать в функцию strlen(), считающую длину строки, численное значение. PHP сперва переведет число в строку, а затем вернет ее длину:

Echo strlen(123); // отобразит"3"

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

Например

Посмотрите на этот код:

Class Member { private $username; public function __construct($username) { $this->username = $username; } public function getUsername() { return $this->username; } } class Topic { private $member; private $subject; public function __construct($member, $subject) { $this->member = $member; $this->subject = $subject; } public function getUsername() { return $this->member->getUsername(); } } $aMember = new Member("fred"); $aTopic = new Topic($aMember, "Hello everybody!"); echo $aTopic->getUsername(); // отобразит "fred"

Данный скрипт работает так:

  • Мы создаем наш класс Member с полем $username, конструктором и методом getUsername();
  • Также создаем класс Topic для управления статьями форума. У него два поля: $member и $subject. $member - это объект класса Member, это будет автор статьи. Поле $subject - это тема статьи.
  • В классе Topic также содержится конструктор, который принимает объект класса Member и строку - тему статьи. Этими значениями он инициализирует поля класса. У него еще есть метод getUsername(), который возвращает имя участника форума. Это достигается через вызов метода getUsername() объекта Member.
  • В завершении создаем объект класса Member со значением поля username “fred”. Затем создаем объект класса Topic, передав ему Фреда и тему статьи “Hello everybody!”. В конце вызываем метод getUsername() класса Topic и отображаем на странице имя пользователя (“fred”).

Это все очень хорошо, но...

Давайте сделаем лучше!

Добавим этот фрагмент кода в конце:

Class Widget { private $colour; public function __construct($colour) { $this->colour = $colour; } public function getColour() { return $this->colour; } } $aWidget = new Widget("blue"); $anotherTopic = new Topic($aWidget, "Oops!"); // отобразит "Fatal error: Call to undefined method Widget::getUsername()" echo $anotherTopic->getUsername();

Здесь мы создаем класс Widget с полем $colour, конструктором и методом getColour(), который возвращает цвет виджета.

Затем мы создадим объект данного класса, а за ним объект Topic с аргументом $aWidget, когда на самом деле нужно передавать автора статьи, т.е. объект класса Member.

Теперь попытаемся вызвать метод getUsername() класса Topic. Этот метод обращается к методу getUsername() класса Widget. И так как в этом классе нет такого метода, мы получаем ошибку:

Fatal error: Call to undefined method Widget::getUsername()

Проблема в том, что причина ошибки не так легко уяснима. Почему объект Topic ищет метод в классе Widget, а не Member? В сложной иерархии классов будет очень сложно найти выход из такого рода ситуации.

Даем подсказку

Было бы лучше ограничить конструктор класса Topic на прием аргументов так, чтобы он мог принимать в качестве первого параметра объекты только класса Member, тем самым предостеречься от фатальных ошибок.

Это как раз то, чем занимается явное указание типов. Чтобы явно указать тип параметра, вставьте имя класса перед названием аргумента в объявлении метода:

Function myMethod(ClassName $object) { // (действия) }

Давайте подкорректируем конструктор класса Topic так, чтобы он принимал только Member:

Class Topic { private $member; private $subject; public function __construct(Member $member, $subject) { $this->member = $member; $this->subject = $subject; } public function getUsername() { return $this->member->getUsername(); } }

Теперь снова попытаемся создать объект Topic, передав ему Widget:

$aWidget = new Widget("blue"); $anotherTopic = new Topic($aWidget, "Oops!");

На этот раз PHP отобразит конкретную ошибку:

Catchable fatal error: Argument 1 passed to Topic::__construct() must be an instance of Member, instance of Widget given, called in script.php on line 55 and defined in script.php on line 24

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

Инициализация и чтение значений полей класса при помощи __get() и __set()

Как вы уже знаете, классы обычно содержат поля:

Class MyClass { public $aProperty; public $anotherProperty; }

Если поля класса - public, вы можете получить к ним доступ с помощью оператора ->:

$myObject = new MyClass; $myObject->aProperty = "hello";

Тем не менее, PHP позволяет создавать “виртуальные” поля, которых на самом деле нет в классе, но к которым можно получить доступ через оператор ->. Они могут быть полезны в таких случаях:

  • Когда у вас очень много полей, и вы хотите создать для них массив, чтобы не объявлять каждое поле отдельно;
  • Когда вам нужно хранить поле за пределами объекта, например, в другом объекте, или даже в файле или базе данных;
  • Когда вам нужно вычислять значения полей “на лету”, а не хранить их значения где-либо.

Чтобы создать такие “виртуальные” поля, нужно добавить в класс парочку волшебных методов:

  • __get($propName) вызывается автоматически при попытке прочитать значение “невидимого” поля $propName;
  • __set($propName,$propValue) вызывается автоматически при попытке задать “невидимому” полю $propName значение $propValue.

“Невидимый” в данном контексте значит, что на данном участке кода нельзя прямо получить доступ к данным полям. Например, если такого поля вообще нет в классе, или если оно существует, но оно частное, и за пределами класса нет доступа к такому полю.

Перейдем к практике. Изменим наш класс Member так, чтобы в дополнение полю $username были еще и другие случайные поля, которые будут храниться в массиве $data:

Class Member { private $username; private $data = array(); public function __get($property) { if ($property == "username") { return $this->username; } else { if (array_key_exists($property, $this->data)) { return $this->data[$property]; } else { return null; } } } public function __set($property, $value) { if ($property == "username") { $this->username = $value; } else { $this->data[$property] = $value; } } } $aMember = new Member(); $aMember->username = "fred"; $aMember->location = "San Francisco"; echo $aMember->username . "
"; // отобразит "fred" echo $aMember->location . "
"; // отобразит "San Francisco"

Вот, как это работает:

  • В классе Member есть постоянное поле private $username и private массив $data для хранения случайных “виртуальных” полей;
  • Метод __get() принимает единственный параметр $property - имя поля, значение которого нужно вернуть. Если $property = “username”, то метод вернет значение поля $username. В другом случае, метод проверит, встречается ли такой $property в ключах массива $data. Если найдется такой ключ, он вернет значение данного поля, в противном случае - null.
  • Метод __set() принимает 2 параметра: $property - имя поля, которое нужно инициализировать, и $value - значение, которое нужно задать данному полю. Если $property = “username”, метод инициализирует поле $username значением из параметра $value. В противном случае, добавляет в массив $data ключ $property со значением $value.
  • После создания класса Member, создаем объект этого класса и инициализируем его поле $username значением “fred”. Это вызывает метод __set(), который задаст значение $username объекту. Затем устанавливаем значение поля $location в “San Francisco”. Так как такого поля не существует в объекте, метод записывает его в массив $data.
  • В конце, достаем значения $username и $location и выводим их на страницу. Метод __get() достает действительное значение $username из существующего поля $username, а значение $location - из массива $data.

Как видите, с помощью методов __get() и __set() мы создали класс, в котором могут быть как настоящие поля, так и любые “виртуальные”. Из фрагмента кода, где задается значение тому или иному полю, не обязательно знать, существует ли такое поле или нет в объекте. Через обычный оператор -> можно задать полю значение или прочитать его.

В примере также показано, как можно легко создать методы, называемые “геттерами” и “сеттерами”, для доступа к частным полям. Нам не нужно создавать отдельные методы getUsername() и setUsername() для получения доступа к частному полю $username. Вместо этого мы создали методы __get() и __set() для манипулирования данным полем. Это значит, что нам нужно всего 2 метода в общем, а не по 2 метода для каждого частного поля.

На заметку: Слово о инкапсуляции. Использование частных полей класса в комбинации с геттерами и сеттерами - это лучше, чем использование переменных public. Геттеры и сеттеры дополнительно могут обрабатывать данные, задаваемые полям объекта и получаемые из них, например, проверять, в правильном ли формате находится значение, или конвертировать его в нужный формат. Геттеры и сеттеры также скрывают детали того, как имплементируются поля класса, что упрощает процесс модификации внутренней части класса, так как не нужно переписывать код, который оперирует объектами данного класса. Например, вы вдруг захотели хранить значение поля в базе данных. Если у вас уже были геттеры и сеттеры, все, что вам нужно, - это переписать их. А вызывающий код останется таким же. Эта техника называется инкапсуляцией, и это одно из главных преимуществ ООП.

Перегрузка методов с помощью __call()

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

Public function __call($methodName, $arguments) { // (действия) }

Когда производится попытка вызвать недоступный метод класса, PHP автоматически вызывает метод __call(), в который передает строку - имя вызываемого метода и список передаваемых параметров в массиве. Затем ваш метод __call() должен будет определенным способом обработать вызов и, в случае необходимости, вернуть значения.

Метод __call() полезен в ситуациях, когда вам нужно передать некую функциональность класса другому классу. Вот простой пример:

Class Member { private $username; public function __construct($username) { $this->username = $username; } public function getUsername() { return $this->username; } } class Topic { private $member; private $subject; public function __construct($member, $subject) { $this->member = $member; $this->subject = $subject; } public function getSubject() { return $this->subject; } public function __call($method, $arguments) { return $this->member->$method($arguments); } } $aMember = new Member("fred"); $aTopic = new Topic($aMember, "Hello everybody!"); echo $aTopic->getSubject() . "
"; // отобразит "Hello everybody!" echo $aTopic->getUsername() . "
"; // отобразит "fred"

Данный пример похож на тот, что приводился в разделе о явном указании типов. У нас есть класс Member с полем $username и класс Topic с полем - объектом класса Member (автор статьи) и полем $subject - темой статьи. Класс Topic содержит метод getSubject() для получения темы статьи, но в нем нет метода, который возвращал бы имя автора статьи. Вместо него в нем есть метод __call(), который вызывает несуществующий метод и передает аргументы методу класса Member.

Когда в коде вызывается метод $aTopic->getUsername(), PHP понимает, что такого метода в классе Topic не существует. Поэтому вызывается метод __call(), который в свою очередь, вызывает метод getUsername() класса Member. Этот метод возвращает имя автора методу __call(), а тот отправляет полученное значение вызывающему коду.

На заметку: в PHP есть и другие методы, касающиеся перегрузки, например, __isset(), __unset(), и __callStatic().

Заключение

В этом уроке вы углубили свои знания по ООП в PHP, рассмотрев более детально поля и методы. Вы изучили:

  • Конструкторы и деструкторы, полезные для инициализации полей и очистки памяти после удаления объектов;
  • Статические поля и методы, которые работают на уровне класса, а не на уровне объекта;
  • Классовые константы, полезные для хранения фиксированных значений, необходимых на уровне класса;
  • Явное указание типов, с помощью которого можно лимитировать типы аргументов, передаваемых в метод;
  • Волшебные методы __get(), __set() и __call(), которые служат для получения доступа к частным полям и методам класса. Реализация этих методов позволяет вам создавать “виртуальные” поля и методы, которые не существуют в классе, но в то же время, могут быть вызваны.

Со знаниями, полученными в этом и предыдущем уроках, вы можете начать писать на ООП. Но на этом все только начинается! В следующем уроке мы поговорим о силе ООП - способности классов наследовать функциональность от других классов.

Удачного программирования!

Поскольку именно классы описывают объекты, мы начнем описание с определения классов.

Определение класса

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

Класс first из приведенного примера - уже полноправный класс, хотя пока и не слишком полезный. Но тем не менее мы сделали нечто очень важное. Мы определили тип, т.е. создали категорию данных, которые мы можем использовать в своих сценариях. Важность этого станет для вас очевидной по мере дальнейшего чтения главы.

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

Так как класс - это шаблон для создания объектов, следовательно, объект - это данные, которые создаются и структурируются в соответствии с шаблоном, определенным в классе. Объект также называют экземпляром класса, тип которого определяется классом. Для создания нового экземпляра класса нам понадобится оператор new . Он используется совместно с именем класса следующим образом:

После оператора new указывается имя класса на основе которого будет создан объект. Оператор new создает экземпляр класса и возвращает ссылку на вновь созданный объект. Эта ссылка сохраняется в переменной соответствующего типа. В результате выполнения этого кода будет создано два объект типа first . Хотя функционально они идентичны (т.е. пусты) $obj1 и $obj2 - это два разных объекта одного типа, созданных с помощью одного класса.

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

Давайте сделаем эти объекты немного интереснее, изменив класс first , добавив в него специальные поля данных, называемые свойства.

Определение свойств

В классе можно определить переменные. Переменные, которые определены в классе называются свойствами (или полями данных). Они определяются с одним из ключевых слов protected, public или private , характеризующих управление доступом. Эти ключевые слова мы рассмотрим подробно в следующей главе. А сейчас давайте определим некоторые свойства с помощью ключевого слова public:

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

Примечание: значения инициализирующие свойства должны быть литералами (константными значениями), инициализировать свойства в классе не обязательно (если значение не указано, по умолчанию это будет NULL).

К свойствам объекта можно обращаться с помощь символов " -> ", указав объект и имя свойства. Поскольку свойства объектов были определены как public , мы можем считывать их значения, а также присваивать им новые значения, заменяя тем самым начальные значения, определенные в классе:

str; // присваиваем свойству объекта новое значение $obj->str = "новая строка"; echo "
$obj->str"; ?>

На самом деле в PHP необязательно объявлять все свойства в классе. Свойства можно добавлять к объекту динамически:

newprop = "новое свойство"; echo $obj->newprop; ?>

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

Работа с методами

Методы - это обычные функции, которые определяются внутри класса, они позволяют объектам выполнять различные задачи. Объявление метода напоминает определение обычной функции, за исключением предваряемого одного из ключевых слов protected, public или private . Если в определении метода вы опустите ключевое слово, определяющее видимость, то метод будет объявлен неявно как public . К методам объекта можно обращаться с помощь символов " -> ", указав объект и имя метода. При вызове метода, так же как и при вызове функции нужно использовать круглые скобки.

str; } } $obj = new first(); // вызов метода объекта $obj->getstr(); ?>

Мы добавили метод getstr() к классу first . Обратите внимание на то, что при определении метода мы не использовали ключевое слово, определяющее область видимости. Это означает, что метод getstr() относится к типу public и его можно вызвать за пределами класса.

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

$this->имя переменной или метода

Class first { public $str = "some text"; // при определении метода в классе, переменная $this не имеет никакого значения function getstr() { echo $this->str; } } // создаем объект $obj = new first(); // созданный нами объект имеет свойство и метод // теперь в методе объекта переменная $this имеет // ссылку на текущий объект, а именно на $obj // т.е. если в методе заменить $this текущим экземпляром объекта $this->str; // это будет выглядеть как простое // обращение к свойству текущего объекта $obj->str;

Примечание: переменной $this нельзя ничего присваивать. Помните, что переменная $this всегда ссылается на текущий объект.

Специальный метод - конструктор

У класса может быть определен специальный метод - конструктор , который вызывается каждый раз при создании нового экземпляра класса (объекта) с целью инициализировать его, например установить значения свойств. Конструктор, как и любой другой метод может иметь параметры. Чтобы определить метод в качестве конструктора его необходимо назвать __construct() . Обратите внимание на то, что имя метода должно начинаться с двух символов подчеркивания. Посмотрим, как это работает:

num1 = $num1; $this->num2 = $num2; } // метод, который складывает два числа function summa() { return $this->num1 + $this->num2; } } // создаем объект и передаем два аргумента $obj = new first(15, 35); // вызываем метод и сразу выводим результат его работы echo $obj->summa(); ?>

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

Примечание: если конструктор не имеет параметров и при создании новых экземпляров класса не передаются никакие аргументы, круглые скобки () после имени класса можно опустить: $obj = new first;

Указание типа аргумента в методах

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

getobj(new cat()); // здесь будет ошибка: передали в качестве аргумента экземпляр типа wrong $kitty->getobj(new wrong()); ?>

Теперь в качестве аргумента методу getobj() можно передавать только экземпляры типа cat . Поскольку метод getobj() содержит уточнение типа класса, передача ему объекта типа wrong приведет к ошибке.

Указание типа нельзя использовать для определения параметров элементарных типов, таких как строки, числа и т.д. Для этой цели в теле метода следует использовать функции проверки типов, например is_string() . Также есть возможность определить, что передаваемый аргумент является массивом:

my_arr = $some_arr; } } ?>

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

Function getobj(cat $getCat = null) { $this->someVar = $getCat; }

Если вместо NULL указать какое-либо другое значение по умолчанию, будет выдана ошибка.

  • Перевод

Сегодня объекты используются очень активно, хотя это трудно было предположить после выхода PHP 5 в 2005 году. Тогда я ещё мало что знал о возможностях этого языка. Пятую версию PHP сравнивали с предыдущей, четвёртой, и главным преимуществом нового релиза стала новая, очень мощная объектная модель. И сегодня, десять лет спустя, около 90% всего PHP-кода содержит объекты, не изменившиеся со времени PHP 5.0. Это убедительно говорит о том, какую роль сыграло внедрение объектной модели, неоднократно улучшавшейся на протяжении последующих лет. В этом посте я хотел бы рассказать о том, как всё устроено «под капотом». Чтобы люди понимали суть процессов - почему сделано так, а не иначе - и лучше, полнее использовали возможности языка. Также я затрону тему использования памяти объектами, в том числе в сравнении с эквивалентными массивами (когда это возможно).

Я буду рассказывать на примере версии PHP 5.4, и описываемые мной вещи справедливы для 5.5 и 5.6, потому что устройство объектной модели там почти не претерпело изменений. Обратите внимание, что в версии 5.3 всё не так хорошо с точки зрения возможностей и общей производительности.

В PHP 7, который пока ещё активно разрабатывается, объектная модель переработана не сильно, были внесены лишь незначительные изменения. Просто потому что всё и так хорошо работает, а лучшее - враг хорошего. Были добавлены возможности, не затрагивающие ядро, но здесь об этом речи не пойдёт.

В качестве демонстрации начну с синтетических бенчмарков:

Class Foo { public $a = "foobarstring"; public $b; public $c = ["some", "values"]; } for ($i=0; $i<1000; $i++) { $m = memory_get_usage(); ${"var".$i} = new Foo; echo memory_get_usage() - $m"\n"; }
Здесь объявляется простой класс с тремя атрибутами, а затем в цикле создаётся 1000 объектов этого класса. Обратите внимание, как в этом примере используется память: при создании объекта класса Foo и переменной для его хранения выделяется 262 байт динамической памяти PHP.

Давайте заменим объект на эквивалентный массив:

For ($i=0; $i<1000; $i++) { $m = memory_get_usage(); ${"var".$i} = [["some", "values"], null, "foobarstring"]; echo memory_get_usage() - $m . "\n"; }
В данном случае используются те же элементы: сам массив, null и строковая переменная foobarstring . Вот только потребляется уже 1160 байт памяти, что в 4,4 раза больше.

Вот ещё один пример:

$class = <<<"CL" class Foo { public $a = "foobarstring"; public $b; public $c = ["some", "values"]; } CL; echo memory_get_usage() . "\n"; eval($class); echo memory_get_usage() . "\n";
Поскольку класс декларируется во время компиляции, то для декларирования и измерения используемой памяти (с помощью диспетчера памяти PHP) мы используем оператор eval() . При этом никакие объекты в данном коде не создаются. Объём задействованной памяти (diff memory) составляет 2216 байт.

Теперь давайте разберём, как всё это устроено в недрах PHP, подкрепив теорией практические наблюдения.

Всё начинается с классов

Внутри PHP класс представляется с помощью структуры zend_class_entry:

Struct _zend_class_entry { char type; const char *name; zend_uint name_length; struct _zend_class_entry *parent; int refcount; zend_uint ce_flags; HashTable function_table; HashTable properties_info; zval **default_properties_table; zval **default_static_members_table; zval **static_members_table; HashTable constants_table; int default_properties_count; int default_static_members_count; union _zend_function *constructor; union _zend_function *destructor; union _zend_function *clone; union _zend_function *__get; union _zend_function *__set; union _zend_function *__unset; union _zend_function *__isset; union _zend_function *__call; union _zend_function *__callstatic; union _zend_function *__tostring; union _zend_function *serialize_func; union _zend_function *unserialize_func; zend_class_iterator_funcs iterator_funcs; /* handlers */ zend_object_value (*create_object)(zend_class_entry *class_type TSRMLS_DC); zend_object_iterator *(*get_iterator)(zend_class_entry *ce, zval *object, int by_ref TSRMLS_DC); int (*interface_gets_implemented)(zend_class_entry *iface, zend_class_entry *class_type TSRMLS_DC); /* a class implements this interface */ union _zend_function *(*get_static_method)(zend_class_entry *ce, char* method, int method_len TSRMLS_DC); /* serializer callbacks */ int (*serialize)(zval *object, unsigned char **buffer, zend_uint *buf_len, zend_serialize_data *data TSRMLS_DC); int (*unserialize)(zval **object, zend_class_entry *ce, const unsigned char *buf, zend_uint buf_len, zend_unserialize_data *data TSRMLS_DC); zend_class_entry **interfaces; zend_uint num_interfaces; zend_class_entry **traits; zend_uint num_traits; zend_trait_alias **trait_aliases; zend_trait_precedence **trait_precedences; union { struct { const char *filename; zend_uint line_start; zend_uint line_end; const char *doc_comment; zend_uint doc_comment_len; } user; struct { const struct _zend_function_entry *builtin_functions; struct _zend_module_entry *module; } internal; } info; };
Размер структуры, исходя из модели LP64, составляет 568 байт . То есть каждый раз, когда PHP декларирует класс, он вынужден создавать zend_class_entry , используя только для этого более половины килобайта динамической памяти. Конечно, дело этим не ограничивается: как вы заметили, структура содержит немало указателей, которые тоже надо разместить в памяти. То есть сами по себе классы потребляют памяти гораздо больше, чем все создаваемые из них впоследствии объекты.

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

Важно знать ещё об одном моменте, связанном с zend_class_entry - о PHP-комментариях. Они также известны как аннотации. Это строковые переменные (в языке С - буферы char*), которые тоже надо разместить в памяти. Для языка С, не использующего Unicode, в отличие от PHP, правило очень простое: один символ = один байт. Чем больше у вас в классе аннотаций, тем больше памяти будет использовано после парсинга.

У zend_class_entry поле doc_comment содержит аннотации класса. У методов и атрибутов тоже есть такое поле.

Пользовательские и внутренние классы

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

Это означает, что когда PHP заканчивает обработку текущего HTTP-запроса, он убирает из памяти и уничтожает все пользовательские классы, готовясь к обработке следующего запроса. Этот подход известен под названием «архитектура без разделения ресурсов» (the share nothing architecture). Так было заложено в PHP с самого начала, и изменять это пока не планируется.

Итак, каждый раз при формировании запроса и парсинге классов происходит выделение памяти для них. После использования класса уничтожается всё, что с ним связано. Так что обязательно используйте все объявленные классы, в противном случае будет теряться память. Применяйте автозагрузчики, они задерживают парсинг/объявление во время выполнения, когда PHP нужно задействовать класс. Несмотря на замедление выполнения, автозагрузчик позволяет грамотно использовать память, поскольку он не будет запущен, пока действительно не возникнет потребность в классе.

С внутренними классами всё иначе. Они размещаются в памяти постоянно, вне зависимости от того, использовали их или нет. То есть они уничтожаются только тогда, когда прекращается работа самого PHP - после завершения обработки всех запросов (подразумеваются веб SAPI, например, PHP-FPM). Поэтому внутренние классы более эффективны, чем пользовательские (в конце запроса уничтожаются только статические атрибуты, больше ничего).

If (EG(full_tables_cleanup)) { zend_hash_reverse_apply(EG(function_table), (apply_func_t) clean_non_persistent_function_full TSRMLS_CC); zend_hash_reverse_apply(EG(class_table), (apply_func_t) clean_non_persistent_class_full TSRMLS_CC); } else { zend_hash_reverse_apply(EG(function_table), (apply_func_t) clean_non_persistent_function TSRMLS_CC); zend_hash_reverse_apply(EG(class_table), (apply_func_t) clean_non_persistent_class TSRMLS_CC); } static int clean_non_persistent_class(zend_class_entry **ce TSRMLS_DC) { return ((*ce)->type == ZEND_INTERNAL_CLASS) ? ZEND_HASH_APPLY_STOP: ZEND_HASH_APPLY_REMOVE; }
Обратите внимание, что даже при кешировании опкодов, как OPCache, создание и уничтожение класса осуществляется при каждом запросе, как и в случае с пользовательскими классами. OPCache просто ускоряет оба этих процесса.

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

Классы, интерфейсы или трейты - без разницы

Для управления классами, интерфейсами и трейтами в PHP используется одна и та же структура - zend_class_entry . И как вы уже видели, эта структура весьма громоздка. Иногда разработчики объявляют в коде интерфейсы, чтобы иметь возможность использовать свои названия в catch-блоках. Это позволяет ловить только определённый вид исключений. Например, так:

Interface BarException { } class MyException extends Exception implements BarException { } try { $foo->bar(): } catch (BarException $e) { }
Не слишком хорошо, что здесь используется 912 байт всего лишь для декларирования интерфейса BarException.

$class = <<<"CL" interface Bar { } CL; $m = memory_get_usage(); eval($class); echo memory_get_usage() - $m . "\n"; /* 912 bytes */
Не хочу сказать, что это плохо или глупо, я не пытаюсь никого и ничто обвинять. Просто обращаю ваше внимание на этот момент. С точки зрения внутренней структуры PHP, классы, интерфейсы и трейты используются совершенно одинаково. В интерфейс нельзя добавить атрибуты, парсер или компилятор просто не позволят этого сделать. Однако структура zend_class_entry никуда не девается, просто ряд полей, включая static_members_table , не будут размещёнными в памяти указателями. Объявление класса, эквивалентного трейта или эквивалентного интерфейса потребует одинакового объёма памяти, поскольку все они используют одну и ту же структуру.

Привязка класса

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

Всё сильно усложняется, если речь заходит о внедрении интерфейсов или наследовании классов. Тогда в ходе привязки класса у родительских и дочерних объектов (будь то классы или интерфейсы) копируется абсолютно все.

/* Single class */ case ZEND_DECLARE_CLASS: if (do_bind_class(CG(active_op_array), opline, CG(class_table), 1 TSRMLS_CC) == NULL) { return; } table = CG(class_table); break;
В случае простого объявления класса мы запускаем do_bind_class() . Эта функция всего лишь регистрирует полностью определённый класс в таблице классов с целью дальнейшего использования во время выполнения, а также осуществляет проверку на возможные абстрактные методы:

Void zend_verify_abstract_class(zend_class_entry *ce TSRMLS_DC) { zend_abstract_info ai; if ((ce->ce_flags & ZEND_ACC_IMPLICIT_ABSTRACT_CLASS) && !(ce->ce_flags & ZEND_ACC_EXPLICIT_ABSTRACT_CLASS)) { memset(&ai, 0, sizeof(ai)); zend_hash_apply_with_argument(&ce->function_table, (apply_func_arg_t) zend_verify_abstract_class_function, &ai TSRMLS_CC); if (ai.cnt) { zend_error(E_ERROR, "Class %s contains %d abstract method%s and must therefore be declared abstract or implement the remaining methods (" MAX_ABSTRACT_INFO_FMT MAX_ABSTRACT_INFO_FMT MAX_ABSTRACT_INFO_FMT ")", ce->name, ai.cnt, ai.cnt > 1 ? "s" : "", DISPLAY_ABSTRACT_FN(0), DISPLAY_ABSTRACT_FN(1), DISPLAY_ABSTRACT_FN(2)); } } }
Тут добавить нечего, простой случай.

При привязке класса, внедряющего интерфейс, нужно осуществить следующие действия:

  • Проверить, не объявлен ли уже интерфейс.
  • Проверить, действительно ли нужный класс является классом, а не самим интерфейсом (как говорилось выше, с точки зрения внутренней структуры они устроены одинаково).
  • Скопировать константы из интерфейса в класс, проверяя на наличие возможных коллизий.
  • Скопировать методы из интерфейса в класс, проверяя на наличие возможных коллизий и несоответствий в декларировании (например, превращая в дочернем классе методы интерфейса в статические).
  • Добавить интерфейс и все возможные материнские интерфейсы к списку интерфейсов, внедряемых классом.
Под «копированием» подразумевается не полное глубокое копирование. Для констант, атрибутов и функций по очереди ведется пересчет, сколько сущностей в памяти их использует.

ZEND_API void zend_do_implement_interface(zend_class_entry *ce, zend_class_entry *iface TSRMLS_DC) { /* ... ... */ } else { if (ce->num_interfaces >= current_iface_num) { if (ce->type == ZEND_INTERNAL_CLASS) { ce->interfaces = (zend_class_entry **) realloc(ce->interfaces, sizeof(zend_class_entry *) * (++current_iface_num)); } else { ce->interfaces = (zend_class_entry **) erealloc(ce->interfaces, sizeof(zend_class_entry *) * (++current_iface_num)); } } ce->interfaces = iface; zend_hash_merge_ex(&ce->constants_table, &iface->constants_table, (copy_ctor_func_t) zval_add_ref, sizeof(zval *), (merge_checker_func_t) do_inherit_constant_check, iface); zend_hash_merge_ex(&ce->function_table, &iface->function_table, (copy_ctor_func_t) do_inherit_method, sizeof(zend_function), (merge_checker_func_t) do_inherit_method_check, ce); do_implement_interface(ce, iface TSRMLS_CC); zend_do_inherit_interfaces(ce, iface TSRMLS_CC); } }
Обратите внимание на разницу между внутренними и пользовательскими классами. Первые для распределения памяти будут использовать realloc() , вторые - erealloc() . realloc() распределяет «постоянную» память, а erealloc() оперирует памятью, «выделяемой по запросу».

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

Для каждой из таблиц функций (методов) используется do_inherit_method:

Static void do_inherit_method(zend_function *function) { function_add_ref(function); } ZEND_API void function_add_ref(zend_function *function) { if (function->type == ZEND_USER_FUNCTION) { zend_op_array *op_array = &function->op_array; (*op_array->refcount)++; if (op_array->static_variables) { HashTable *static_variables = op_array->static_variables; zval *tmp_zval; ALLOC_HASHTABLE(op_array->static_variables); zend_hash_init(op_array->static_variables, zend_hash_num_elements(static_variables), NULL, ZVAL_PTR_DTOR, 0); zend_hash_copy(op_array->static_variables, static_variables, (copy_ctor_func_t) zval_add_ref, (void *) &tmp_zval, sizeof(zval *)); } op_array->run_time_cache = NULL; } }
К OPArray функции добавлен refcount , а также с помощью zval_add_ref скопированы все возможные статические переменные, объявленные в функции (здесь это метод). Таким образом, для всего процесса копирования нужно немало вычислительных ресурсов, потому что здесь задействовано много циклов и проверок. Но памяти задействуется немного. К сожалению, сегодня привязка интерфейса полностью протекает во время выполнения, и вы будете это чувствовать при каждом запросе. Возможно, скоро разработчики это изменят.

Что касается наследования, то здесь, в принципе, всё то же самое, что и при внедрении интерфейса. Только вовлечено ещё больше «участников». Но хочу отметить, что если PHP уже знает о классе, то привязка осуществляется во время компилирования, а если не знает - то во время выполнения. Так что лучше объявлять так:

/* good */ class A { } class B extends A { }
вместо:

/* bad */ class B extends A { } class A { }
Кстати, рутинная процедура привязки класса может привести к очень странному поведению:

/* это работает */ class B extends A { } class A { }

/* а это нет */ Fatal error: Class "B" not found */ class C extends B { } class B extends A { } class A { }

В первом варианте привязка класса В отложена на время выполнения, потому что когда компилятор доходит до объявления этого класса, он ещё ничего не знает о классе А. Когда начинается выполнение, то привязка класса А происходит без вопросов, потому что он уже скомпилирован, будучи одиночным классом. Во втором случае всё иначе. Привязка класса С отложена на время выполнения, потому что компилятор ещё ничего не знает о В, пытаясь скомпилировать его. Но когда во время выполнения начинается привязка класса С, то он ищет В, который не существует, поскольку не скомпилирован по причине того, что В является дополнением. Вылетает сообщение “Class B doesn’t exist”.

Объекты

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

Теперь поговорим об объектах. В первой главе показано, что создание «классического» объекта («классического» пользовательского класса) потребовало очень мало памяти, около 200 байт. Всё дело в классе. Дальнейшая компиляция класса тоже потребляет память, но это к лучшему, потому что для создания одиночного объекта требуется меньше байт. По сути, объект представляет собой крохотный набор из крохотных структур.

Управление методами объекта

На уровне движка методы и функции являются одним и тем же - структурой zend_function_structure . Различаются лишь названия. Методы компилируются и добавляются к атрибуту function_table в zend_class_entry . Поэтому во время выполнения представлен каждый метод, это лишь вопрос перевода указателя на исполнение.

Typedef union _zend_function { zend_uchar type; struct { zend_uchar type; const char *function_name; zend_class_entry *scope; zend_uint fn_flags; union _zend_function *prototype; zend_uint num_args; zend_uint required_num_args; zend_arg_info *arg_info; } common; zend_op_array op_array; zend_internal_function internal_function; } zend_function;
Когда объект пытается вызвать метод, то движок по умолчанию ищет в таблице значений функций класса этого объекта. Если метод не существует, то вызывается __call() . Также проверяется видимость - public/protected/private - в зависимости от чего предпринимаются следующие действия:

Static union _zend_function *zend_std_get_method(zval **object_ptr, char *method_name, int method_len, const zend_literal *key TSRMLS_DC) { zend_function *fbc; zval *object = *object_ptr; zend_object *zobj = Z_OBJ_P(object); ulong hash_value; char *lc_method_name; ALLOCA_FLAG(use_heap) if (EXPECTED(key != NULL)) { lc_method_name = Z_STRVAL(key->constant); hash_value = key->hash_value; } else { lc_method_name = do_alloca(method_len+1, use_heap); zend_str_tolower_copy(lc_method_name, method_name, method_len); hash_value = zend_hash_func(lc_method_name, method_len+1); } /* If the method is not found */ if (UNEXPECTED(zend_hash_quick_find(&zobj->ce->function_table, lc_method_name, method_len+1, hash_value, (void **)&fbc) == FAILURE)) { if (UNEXPECTED(!key)) { free_alloca(lc_method_name, use_heap); } if (zobj->ce->__call) { /* if the class has got a __call() handler */ return zend_get_user_call_function(zobj->ce, method_name, method_len); /* call the __call() handler */ } else { return NULL; /* else return NULL, which will likely lead to a fatal error: method not found */ } } /* Check access level */ if (fbc->op_array.fn_flags & ZEND_ACC_PRIVATE) { zend_function *updated_fbc; updated_fbc = zend_check_private_int(fbc, Z_OBJ_HANDLER_P(object, get_class_entry)(object TSRMLS_CC), lc_method_name, method_len, hash_value TSRMLS_CC); if (EXPECTED(updated_fbc != NULL)) { fbc = updated_fbc; } else { if (zobj->ce->__call) { fbc = zend_get_user_call_function(zobj->ce, method_name, method_len); } else { zend_error_noreturn(E_ERROR, "Call to %s method %s::%s() from context "%s"", zend_visibility_string(fbc->common.fn_flags), ZEND_FN_SCOPE_NAME(fbc), method_name, EG(scope) ? EG(scope)->name: ""); } } } else { /* ... ... */ }
Вы могли заметить интересную вещь, посмотрите на первые строки:

If (EXPECTED(key != NULL)) { lc_method_name = Z_STRVAL(key->constant); hash_value = key->hash_value; } else { lc_method_name = do_alloca(method_len+1, use_heap); /* Create a zend_copy_str_tolower(dest, src, src_length); */ zend_str_tolower_copy(lc_method_name, method_name, method_len); hash_value = zend_hash_func(lc_method_name, method_len+1); }
Это проявление невосприимчивости PHP к разным регистрам. Система сначала должна привести каждую функцию к нижнему регистру (zend_str_tolower_copy()), прежде чем вызывать её. Не совсем каждую, а те, где присутствует оператор if . Переменная key предотвращает исполнение функции, переводящей в нижний регистр (часть с else) - это часть оптимизации PHP, осуществлённой в версии 5.4. Если вызов метода не динамический, то компилятор уже вычислил key , и во время выполнения тратится меньше ресурсов.

Class Foo { public function BAR() { } } $a = new Foo; $b = "bar"; $a->bar(); /* static call: good */ $a->$b(); /* dynamic call: bad */
Во время компиляции функции/метода происходит немедленный перевод в нижний регистр. Вышеприведённая функция BAR() превращается в bar() компилятором при добавлении метода таблице классов и функций.

В приведённом примере первый вызов статический: компилятор вычислил key для строковой “bar”, а когда приходит время вызова метода, ему нужно делать меньше работы. Второй вызов уже динамический, компилятор ничего не знает о “$b”, не может вычислить key для вызова метода. Затем, во время выполнения, нам придётся перевести строковую в нижний регистр и вычислить её хеш (zend_hash_func()), что не лучшим образом сказывается на производительности.

Что касается __call() , то она не настолько сильно снижает производительность. Тем не менее, в этом случае тратится больше ресурсов, чем при вызове существующей функции.

Управление атрибутами объекта

Вот что происходит:

Как видите, когда создаётся несколько объектов одного класса, движок перенаправляет каждый атрибут на тот же указатель, что и в случае с атрибутами класса. На протяжении своей жизни класс хранит не только свои, статические, атрибуты, но также и атрибуты объектов. В случае с внутренними классами - в течение всего времени работы PHP. Создание объекта не подразумевает создания его атрибутов, так что это довольно быстрый и экономичный подход. Только когда объект собирается поменять один из своих атрибутов, движок создаёт для этого новый, предполагая, что вы меняете атрибут $a объекта Foo #2:

Так что, создавая объект, мы «всего лишь» создаём структуру zend_object весом 32 байта:

Typedef struct _zend_object { zend_class_entry *ce; HashTable *properties; zval **properties_table; HashTable *guards; /* protects from __get/__set ... recursion */ } zend_object;
Эта структура добавляется к хранилищу объектов. А им, в свою очередь, является структура zend_object_store . Это глобальный реестр объектов движка Zend - место, где собираются все объекты и хранятся в одном экземпляре:

ZEND_API zend_object_value zend_objects_new(zend_object **object, zend_class_entry *class_type TSRMLS_DC) { zend_object_value retval; *object = emalloc(sizeof(zend_object)); (*object)->ce = class_type; (*object)->properties = NULL; (*object)->properties_table = NULL; (*object)->guards = NULL; /* Add the object into the store */ retval.handle = zend_objects_store_put(*object, (zend_objects_store_dtor_t) zend_objects_destroy_object, (zend_objects_free_object_storage_t) zend_objects_free_object_storage, NULL TSRMLS_CC); retval.handlers = &std_object_handlers; return retval; }
Далее движок создаёт вектор признаков нашего объекта:

ZEND_API void object_properties_init(zend_object *object, zend_class_entry *class_type) { int i; if (class_type->default_properties_count) { object->properties_table = emalloc(sizeof(zval*) * class_type->default_properties_count); for (i = 0; i < class_type->default_properties_count; i++) { object->properties_table[i] = class_type->default_properties_table[i]; if (class_type->default_properties_table[i]) { #if ZTS ALLOC_ZVAL(object->properties_table[i]); MAKE_COPY_ZVAL(&class_type->default_properties_table[i], object->properties_table[i]); #else Z_ADDREF_P(object->properties_table[i]); #endif } } object->properties = NULL; } }
Как видите, мы разместили в памяти таблицу/вектор (как в языке С) для zval* , основанный на объявленных свойствах класса объекта. В случае непоточнобезопасного PHP мы просто добавляем к признаку refcount, а если используется поточнобезопасный Zend (ZTS, Zend thread safety), то нужно полностью скопировать zval . Это один из многочисленных примеров, подтверждающих низкую производительность и высокую ресурсоёмкость режима ZTS по сравнению с не ZTS PHP.

Вероятно, у вас возникли два вопроса:

  • Чем отличаются properties_table и properties в структуре zend_object ?
  • Если мы поместили атрибуты нашего объекта в С-вектор, то как вернуть их обратно? Каждый раз просматривать вектор (что снижает производительность)?

Ответ на оба вопроса даёт zend_property_info .

Typedef struct _zend_property_info { zend_uint flags; const char *name; int name_length; ulong h; int offset; const char *doc_comment; int doc_comment_len; zend_class_entry *ce; } zend_property_info;
Каждый объявленный атрибут (свойство) нашего объекта имеет соответствующую информацию о свойстве, добавляемую в поле property_info в zend_class_entry . Делается это во время компиляции объявленных в классе атрибутов:

Class Foo { public $a = "foo"; protected $b; private $c; } struct _zend_class_entry { /* ... ... */ HashTable function_table; HashTable properties_info; /* here are the properties infos about $a, $b and $c */ zval **default_properties_table; /* and here, we"ll find $a, $b and $c with their default values */ int default_properties_count; /* this will have the value of 3: 3 properties */ /* ... ... */
Properties_infos представляет собой таблицу, сообщающую объекту о существовании запрашиваемого атрибута. И если он существует, то передаёт его индексный номер в массиве object->properties . Потом мы проверяем видимость и доступ к scope (public/protected/private).

Если же атрибут не существует и нам нужно записать в него, то можно попытаться вызвать __set() . В случае неудачи создаём динамический атрибут, который будет храниться в поле object->property_table .

Property_info = zend_get_property_info_quick(zobj->ce, member, (zobj->ce->__set != NULL), key TSRMLS_CC); if (EXPECTED(property_info != NULL) && ((EXPECTED((property_info->flags & ZEND_ACC_STATIC) == 0) && property_info->offset >= 0) ? (zobj->properties ? ((variable_ptr = (zval**)zobj->properties_table) != NULL) : (*(variable_ptr = &zobj->properties_table) != NULL)) : (EXPECTED(zobj->properties != NULL) && EXPECTED(zend_hash_quick_find(zobj->properties, property_info->name, property_info->name_length+1, property_info->h, (void **) &variable_ptr) == SUCCESS)))) { /* ... ... */ } else { zend_guard *guard = NULL; if (zobj->ce->__set && /* class has a __set() ? */ zend_get_property_guard(zobj, property_info, member, &guard) == SUCCESS && !guard->in_set) { Z_ADDREF_P(object); if (PZVAL_IS_REF(object)) { SEPARATE_ZVAL(&object); } guard->in_set = 1; /* prevent circular setting */ if (zend_std_call_setter(object, member, value TSRMLS_CC) != SUCCESS) { /* call __set() */ } guard->in_set = 0; zval_ptr_dtor(&object); /* ... ... */
Пока вы не пишете в объект, его потребление памяти не меняется. После записи он занимает уже больше места (пока не будет уничтожен), поскольку содержит все записанные в него атрибуты.

Объекты, ведущие себя как ссылки благодаря хранилищу объектов

Объекты не являются ссылками. Это демонстрируется на маленьком скрипте:

Function foo($var) { $var = 42; } $o = new MyClass; foo($o); var_dump($o); /* this is still an object, not the integer 42 */
Все сейчас скажут, что «в PHP 5 объекты являются ссылками», об этом упоминает даже официальный мануал. Технически это совершенно неверно. Тем не менее, объекты могут вести себя так же, как и ссылки. Например, когда вы передаёте переменную, являющуюся объектом функции, эта функция может модифицировать тот же объект.

Так происходит потому, что zval , передаваемый в виде функции, передаёт не сам объект, а его уникальный идентификатор, используемый для поиска в общем хранилище объектов. А результат получается тот же самый. Можно разместить в памяти три разных zval , и все они могут содержать один и тот же дескриптор объекта.

Object(MyClass)#1 (0) { } /* #1 is the object handle (number), it is unique */

Zend_object_store обеспечивает однократное занесение объектов в память. Единственный способ записать в хранилище заключается в создании нового объекта с ключевым словом new , функцией unserialize() , reflection API или ключевым словом clone . Никакие другие операции не позволят дуплицировать или создать новый объект в хранилище.

Typedef struct _zend_objects_store { zend_object_store_bucket *object_buckets; zend_uint top; zend_uint size; int free_list_head; } zend_objects_store; typedef struct _zend_object_store_bucket { zend_bool destructor_called; zend_bool valid; zend_uchar apply_count; union _store_bucket { struct _store_object { void *object; zend_objects_store_dtor_t dtor; zend_objects_free_object_storage_t free_storage; zend_objects_store_clone_t clone; const zend_object_handlers *handlers; zend_uint refcount; gc_root_buffer *buffered; } obj; struct { int next; } free_list; } bucket; } zend_object_store_bucket;

Что такое $this?

Понять устройство $this не так уж сложно, но с этим инструментом связаны куски кода в нескольких местах движка: в компиляторе, в коде получения переменных во время выполнения и т.д. $this появляется и исчезает по мере необходимости, автоматически присваивая себе текущий объект - в общем, «волшебная» штука. А внутренний код прекрасно позволяет ей управлять.

Во-первых, компилятор не позволит записывать в $this . Для этого он проверят каждое осуществляемое вами присваивание, и если обнаруживает присвоение $this , то возникает фатальная ошибка.

/* ... ... */ if (opline_is_fetch_this(last_op TSRMLS_CC)) { zend_error(E_COMPILE_ERROR, "Cannot re-assign $this"); } /* ... ... */ static zend_bool opline_is_fetch_this(const zend_op *opline TSRMLS_DC) { if ((opline->opcode == ZEND_FETCH_W) && (opline->op1_type == IS_CONST) && (Z_TYPE(CONSTANT(opline->op1.constant)) == IS_STRING) && ((opline->extended_value & ZEND_FETCH_STATIC_MEMBER) != ZEND_FETCH_STATIC_MEMBER) && (Z_HASH_P(&CONSTANT(opline->op1.constant)) == THIS_HASHVAL) && (Z_STRLEN(CONSTANT(opline->op1.constant)) == (sizeof("this")-1)) && !memcmp(Z_STRVAL(CONSTANT(opline->op1.constant)), "this", sizeof("this"))) { return 1; } else { return 0; } }
Как управляется $this ? Его использование возможно только внутри метода, во время вызова которого компилятор генерирует OPCode INIT_METHOD_CALL . Движок знает, кто вызывает метод, в случае с $a->foo() это $a . После чего извлекается значение $a и сохраняется в общем пространстве. Далее происходит вызов метода с помощью OPCode DO_FCALL . На этом этапе снова извлекается сохранённое значение (объект вызывает метод) и присваивается глобальному внутреннему $this -указателю - EG(This) .

If (fbc->type == ZEND_USER_FUNCTION || fbc->common.scope) { should_change_scope = 1; EX(current_this) = EG(This); EX(current_scope) = EG(scope); EX(current_called_scope) = EG(called_scope); EG(This) = EX(object); /* fetch the object prepared in previous INIT_METHOD opcode and affect it to EG(This) */ EG(scope) = (fbc->type == ZEND_USER_FUNCTION || !EX(object)) ? fbc->common.scope: NULL; EG(called_scope) = EX(call)->called_scope; }
Теперь, когда метод вызван, если в его теле вы используете $this для действия с переменной или вызова метода (например, $this->a = 8), то это приведёт к OPCode ZEND_ASSIGN_OBJ , который, в свою очередь, обратно извлечёт $this из EG(This) .

Static zend_always_inline zval **_get_obj_zval_ptr_ptr_unused(TSRMLS_D) { if (EXPECTED(EG(This) != NULL)) { return &EG(This); } else { zend_error_noreturn(E_ERROR, "Using $this when not in object context"); return NULL; } }
В том случае, если вы использовали $this для вызова метода (например, $this->foo()) или передали другому вызову функции ($this->foo($this);), то движок попытается извлечь $this из текущей символьной таблицы, как он это делает для каждой стандартной переменной. Но здесь осуществляется специальная подготовка в ходе создания кадра стека текущей функции:

If (op_array->this_var != -1 && EG(This)) { Z_ADDREF_P(EG(This)); if (!EG(active_symbol_table)) { EX_CV(op_array->this_var) = (zval **) EX_CV_NUM(execute_data, op_array->last_var + op_array->this_var); *EX_CV(op_array->this_var) = EG(This); } else { if (zend_hash_add(EG(active_symbol_table), "this", sizeof("this"), &EG(This), sizeof(zval *), (void **) EX_CV_NUM(execute_data, op_array->this_var))==FAILURE) { Z_DELREF_P(EG(This)); } } }
Когда мы вызываем метод, движок изменяет область видимости:

If (fbc->type == ZEND_USER_FUNCTION || fbc->common.scope) { /* ... ... */ EG(scope) = (fbc->type == ZEND_USER_FUNCTION || !EX(object)) ? fbc->common.scope: NULL; /* ... ... */ }
EG(scope) относится к типу zend_class_entry . Это класс, которому принадлежит запрашиваемый вами метод. И он будет использоваться для любой операции с объектом, которую вы будете выполнять в теле метода после проверки видимости движком:

Static zend_always_inline int zend_verify_property_access(zend_property_info *property_info, zend_class_entry *ce TSRMLS_DC) { switch (property_info->flags & ZEND_ACC_PPP_MASK) { case ZEND_ACC_PUBLIC: return 1; case ZEND_ACC_PROTECTED: return zend_check_protected(property_info->ce, EG(scope)); case ZEND_ACC_PRIVATE: if ((ce==EG(scope) || property_info->ce == EG(scope)) && EG(scope)) { return 1; } else { return 0; } break; } return 0; }
Вот так можно получать доступ к приватным членам объектов, не принадлежащим вам, но являющимся дочерними по отношению к вашей текущей области видимости:

Class A { private $a; public function foo(A $obj) { $this->a = "foo"; $obj->a = "bar"; /* yes, this is possible */ } } $a = new A; $b = new A; $a->foo($b);
Эта особенность стала причиной большого количества баг-репортов от разработчиков. Но так устроена объектная модель в PHP - на самом деле, мы задаём область видимости на основе не объекта, а класса. В случае с нашим классом “Foo”, вы можете работать с любым приватным Foo любого другого Foo, как показано выше.

О деструкторе

Деструкторы опасны, не полагайтесь на них, поскольку PHP их не вызывает даже в случае фатальной ошибки:

Class Foo { public function __destruct() { echo "byebye foo"; } } $f = new Foo; thisfunctiondoesntexist(); /* fatal error, function not found, the Foo"s destructor is NOT run */
А что насчёт порядка вызова деструкторов в том случае, если они всё-таки вызываются? Ответ хорошо виден в коде:

Void shutdown_destructors(TSRMLS_D) { zend_try { int symbols; do { symbols = zend_hash_num_elements(&EG(symbol_table)); zend_hash_reverse_apply(&EG(symbol_table), (apply_func_t) zval_call_destructor TSRMLS_CC); } while (symbols != zend_hash_num_elements(&EG(symbol_table))); zend_objects_store_call_destructors(&EG(objects_store) TSRMLS_CC); } zend_catch { /* if we couldn"t destruct cleanly, mark all objects as destructed anyway */ zend_objects_store_mark_destructed(&EG(objects_store) TSRMLS_CC); } zend_end_try(); } static int zval_call_destructor(zval **zv TSRMLS_DC) { if (Z_TYPE_PP(zv) == IS_OBJECT && Z_REFCOUNT_PP(zv) == 1) { return ZEND_HASH_APPLY_REMOVE; } else { return ZEND_HASH_APPLY_KEEP; } }
Здесь продемонстрированы три стадии вызова деструктора:

  • Цикличный просмотр глобальной символьной таблицы в обратном направлении и вызов деструкторов для объектов, у которых refcount = 1.
  • Затем направление цикличности меняется, а деструкторы вызываются уже для всех остальных объектов, с refcount > 1.
  • Если на одном из предыдущих этапов возникает проблема, то вызов оставшихся деструкторов прерывается.
К чему это приводит:

Class Foo { public function __destruct() { var_dump("destroyed Foo"); } }
class Bar { public function __destruct() { var_dump("destroyed Bar"); } }

Пример первый:

$a = new Foo; $b = new Bar; "destroyed Bar" "destroyed Foo"
Тот же пример:

$a = new Bar; $b = new Foo; "destroyed Foo" "destroyed Bar"
Пример второй:

$a = new Bar; $b = new Foo; $c = $b; /* increment $b"s object refcount */ "destroyed Bar" "destroyed Foo"
Пример третий:

Class Foo { public function __destruct() { var_dump("destroyed Foo"); die();} } /* notice the die() here */ class Bar { public function __destruct() { var_dump("destroyed Bar"); } } $a = new Foo; $a2 = $a; $b = new Bar; $b2 = $b; destroyed Foo
Эта процедура была выбрана не просто так. Но если она вас не устраивает, то лучше уничтожайте свои объекты самостоятельно. Это единственный способ контролировать вызовы __destruct() . Если вы предоставите PHP делать это за вас, то не возмущайтесь потом результатами его работы. У вас всегда есть возможность уничтожать свои объекты вручную, чтобы полностью контролировать очерёдность.

PHP не вызывает деструкторы в случае возникновения какой-либо фатальной ошибки. Дело в том, что в этом случае Zend работает нестабильно, а вызов деструкторов приводит к выполнению пользовательского кода, который может получить доступ к ошибочным указателям и, в результате, к падению PHP. Уж лучше сохранять стабильность системы - поэтому вызов деструкторов и блокируется. Возможно, в PHP 7 что-то и поменяется.

Что касается рекурсий, то в PHP они слабо защищены, да и относится это только к __get() и __set() . Если вы уничтожаете свой объект где-то в стековом кадре деструктора, то окажетесь в бесконечном рекурсивном цикле, который сожрёт все ресурсы вашего стека процесса (обычно 8 кбайт, ulimit –s) и сломает PHP.

Class Foo { public function __destruct() { new Foo; } /* you will crash */ }
Суммируя вышесказанное: не доверяйте деструкторам критически важный код, например, управление механизмом блокировки (lock mechanism), поскольку PHP может и не вызвать деструктор или вызвать его в неконтролируемой последовательности. Если всё-таки важный код обрабатывается деструктором, то как минимум самостоятельно контролируйте жизненный цикл объектов. PHP вызовет деструктор, когда refcount вашего объекта упадёт до нуля, а это значит, что объект больше не используется и его можно безопасно уничтожить.

Заключение

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

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

Понять что такое объект легче всего на примере, но тут сначала придётся набраться немного терпения и осилить теорию.

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

Теперь давайте создадим класс.

Class TestClass { // Это тело класса. }

В PHP мы используем слово class , чтобы объявить класс. Имя класса чувствительно к регистру.

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

Class TestClass { public $message = "Hi!"; function sayHello() { echo "Hello!"; } }

Перед объявлением переменной мы видим слово public , это определение области видимости для нашего свойства. По английски public значит общественный, так что из названия понятно, что это свойство класса общедоступно. Об области видимости подробнее мы поговорим позже. Пока терпение и грызём текущую теорию.

Теперь в нашем классе TestClass есть переменная $message со значением "Hi!", эта переменная является свойством класса, и функция sayHello, которая является методом класса.

Итак, мы имеем класс. Теперь на основании этого создадим объект при помощи оператора new .

Class TestClass { public $message = "Hi!"; function sayHello() { echo "Hello!"; } } $govorilka = new TestClass();

Наш объект является как бы копией класса.

Следующий шаг - это использование объекта. Для доступа к свойствам и методам объекта используется оператор -> .

Чтобы получить доступ к свойству $message объекта $govorilka нужно просто написать $govorilka->message. Обратите внимание, что перед названием свойства message мы уже не ставим знак $ .

Доступ к методу осуществляется также, но нужно не забывать о скобках в конце метода, ведь мы имее дело с обычной функцией.

Вот пример обращения к свойству и методу созданного нами объекта:

message; echo "
"; $govorilka->sayHello();

Мы можем менять значения свойств.

message = "Здравствуйте."; echo $govorilka->message; echo "
"; $govorilka->sayHello();

На основании одного класса можно создать сколько угодно объектов.

message = "Здравствуйте."; echo $govorilka->message; echo "
"; $govorilka->sayHello(); echo "
"; // Создаём второй объект $govorilka2 = new TestClass(); echo $govorilka2->message;

Как мы видим, изменение свойтва объекта $govorilka не привело к изменению свойства класса TestClass, и второй объект соответствует классу, экземпляром которого он является.

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

Пока что не совсем понятно, зачем всё это нам надо, эти классы, объекты... Давайте создадим более реалистичный класс и на его примере будем дальше работать.

type \"$this->name\". Описание: $this->description. Цена: $this->price"; } } $Goods = new Goods(); $Goods->printGoods();

Обратите внимание на псевдопеременную $this , это зарезервированная в PHP переменная, которая используется для доступа к свойствам класса внутри его кода.

То есть, чтобы обратиться к свойству $name класса Goods из его же метода мы использовали конструкцию $this->name.

Теперь давайте немного увеличим пример, усложнением это назвать трудно. Итак:

type. Название: \"$this->name\". Описание: $this->description. Цена: $this->price."; } } $Goods = new Goods(); $Goods->type = "книга"; $Goods->name = "Война и мир"; $Goods->description = "Толстая книга из нескольких томов"; $Goods->price = "543.26"; $Goods->printGoods();

Конечно, нет смысла всё делать таким образом. Обычно объект получает уникальный идентификатор товара и берёт все данные из базы MySQL.

id = $id; $this->type = "книга"; $this->name = "Война и мир"; $this->description = "Толстая книга из нескольких томов"; $this->price = "543.26"; } function printGoods() { echo " ID товара: $this->id. Тип товара: $this->type. Название: \"$this->name\". Описание: $this->description. Цена: $this->price."; } } $Goods = new Goods(124); $Goods->printGoods();

В этом примере слишком много нового.

Первое, обратите внимание на название метода __construct() . Это зарезервированное слово в PHP и такой метод запустится автоматически при создании объекта. Такие методы называются конструкторами.

Когда мы создавали новый объект Goods(124), мы передали конструктору число 124. Конструктор, по легенде:), получил из базы данных соответствующий товар и определил свойства объекта в соответствии с информацией из базы данных.

Свойства объекта у нас имеют область видимости protected , то есть защищённую. К такому свойству мы не можем обратиться напрямую из клиентского кода.

Клиентский код - это код, который использует класс, объект или функцию. Это важный термин, запомните его.

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

price;

Этот пример вернёт ошибку: "Cannot access protected property Goods". Так и должно быть, мы обращаемся туда, куда нет доступа.

Резюме

Итак, что мы узнали на этой странице:

  • Что такое класс. Зачем нужен класс. Как создать класс при помощи зарезервированного слова class .
  • Что такое свойства и методы класса.
  • Как при помощи оператора new создать объект какого-то класса.
  • Как из объекта получить доступ к свойствам и методам класса при помощи конструкции -> .
  • Как изменять свойства класса из объекта.
  • Область видимости свойств и методов. Как ограничить область видимости при помощи слова protected .
  • Как обратиться к свойству метода в классе при помощи псевдопеременной $this .
  • Создание конструкторов при помощи метода __construct() .

Это только первый шаг в освоении объектов. По большому счёту ещё не совсем понятно, зачем всё это нужно. Но картина с объектами станет ясной несколько позже, пока наберитесь терпения.

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

Из Зандстры

стр. 51 - проверка передаваемого методу аргумента на соответствие типу класса (можно и проверить на массив).

стр. 56 - оператор instanceof (англ. экземпляр), проверяет соответствие левого аргумента типу объекта в правом аргументе.

В этом уроке вы ознакомитесь с основами объектно-ориентированного программирования в PHP. Вы узнаете о принципах ООП вообще и научитесь писать простенькие скрипты на PHP.

Добро пожаловать в первый из серии уроков по ООП на PHP! Ознакомившись со всеми уроками данной серии, вы узнаете об основных принципах и концепциях ООП и научитесь быстро и легко создавать полезные приложения на PHP.

В этом уроке я начну вводить вас в курс дела и расскажу вам об основных понятиях ООП. Вы узнаете:

  • что такое ООП
  • как ООП поможет вам создавать лучшие PHP скрипты
  • некоторые основные понятия, такие как классы, объекты, методы, переменные класса
  • с чего начать написание PHP скрипта

Вы готовы погрузиться в мир объектов PHP? Тогда вперед!

Что такое объектно-ориентированное программирование?

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

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

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

Зачем использовать ООП?

Процедурный и объектно-ориентированный - это два разных способа сделать одно и то же. Нельзя сказать, что один из них лучше другого - каждый пишет, как ему нравится, так что вы даже можете легко комбинировать эти два подхода в одном скрипте.

Однако, вот некоторые преимущества ООП для разработчиков:

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

Некоторые основные понятия

Перед тем, как начать писать скрипты, необходимо хорошо разобраться с такими понятиями, как класс, объект, переменная класса и метод.

Классы

Класс - это каркас для объекта. Это кусок кода, который определяет:

  • Типы данных, которые будут содержать созданные объекты класса
  • Функции, которые будут содержать эти объекты.

Когда вы создаете приложение на ООП, вы обычно будете создавать несколько классов, которые будут представлять различные типы сущностей вашего приложения. Например, для создания форума вы можете создать классы Forum, Topic, Post и Member.

Объекты

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

Для сравнения с реальными сущностями:

  • Класс - это каркас для автомобиля: он определяет, как автомобиль будет выглядеть и действовать, но это все же абстрактная сущность
  • Объект - это настоящий автомобиль, созданный из каркаса: у него есть настоящие свойства (например, скорость) и поведение (например, ускорение или торможение).

На заметку: Объект часто называют сущностью класса, а процесс создания объекта класса - реализацией.

Переменные класса

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

Методы

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

Как создать класс в PHP?

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

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

Class ClassName { // (определение класса) }

К примеру, если вы создаете класс Member для вашего форума, вы напишите так:

Class Member { // (определение класса) }

Это достаточно просто. Естественно, класс этот ничего не сделает, пока вы не добавите в него переменные и методы. Тем не менее, приведенный выше код создает валидный класс на PHP, который можно использовать.

Правило хорошего тона: каждый класс помещайте в отдельный файл с названием, совпадающим с именем класса. Например, поместите класс Member в файл Member.php и храните его в папке, допустим, classes.

Как создавать объекты в PHP?

Создать объект можно с помощью ключевого слова new:

New ClassName()

Этот код создаст объект класса ClassName. Вам впоследствии понадобится использовать этот объект, поэтому его нужно хранить в переменной. Например, создадим объект класса Member и сохраним его в переменной $member:

$member = new Member();

Мы также можем создать еще один объект того же класса:

$member2 = new Member();

Несмотря на то что мы создали эти два объекта от одного и того же класса, переменные $member и $member2 не зависят друг от друга.

Создаем переменные класса

Теперь, когда мы уже умеем создавать классы и объекты классов, давайте посмотрим, как создавать переменные класса. Есть 3 идентификатора доступа для переменных класса, которые можно добавлять в класс:

  • Открытые переменные класса (public): доступны - т.е. их можно прочитать и/или изменять - в любом месте скрипта, независимо от того, где находится этот код - внутри класса или за его пределами
  • Частные переменные класса (private): доступны только методам класса. Лучше всего делать переменные класса именно частными, чтобы отделить объекты от остальной части кода.
  • Защищенные переменные класса (protected): доступны методам собственного класса, а также методам наследуемых классов (мы поговорим о наследовании позже).

Чтобы создать переменную класса, напишите ключевое слово public, private или protected, а затем введите имя переменной:

Class ClassName { public $propertyName; private $propertyName; protected $propertyName; }

Давайте добавим переменную класса public нашему классу Member для хранения имени пользователя:

Class Member { public $username = ""; }

Обратите внимание на то, что мы инициализировали нашу переменную класса, его значение - пустая строка, “”. Это значит, что при создании нового пользователя значение его имени по умолчанию будет равняться пустой строке. Так же, как и в случае с обычными переменными в PHP, переменные класса не обязательно инициализировать, но лучше все-таки не лениться. Если вы не инициализируете переменную класса, то по умолчанию ее значение равно null.

Доступ к переменным класса

Для получения доступа к переменной того или иного объекта используется оператор ->:

$object->propertyName

Давайте попробуем. Напишем скрипт, который объявляет класс Member и переменную класса, создает объект этого класса, а затем задает значение переменной класса и выводит его на экран:

username = "Fred"; echo $member->username; // Выведет "Fred" ?>

Запустите данный код, он выведет на экран строку “Fred”, значение переменной класса $member->username. Как видите, вы оперируете переменной объекта так же, как обычной переменной - вы можете задать ей значение и прочитать его.

Добавление методов в класс

Что скажете насчет создания методов? Как я ранее упоминал, методы - это обычные функции, являющиеся частью класса. Так что вы, возможно, не удивитесь тому, что и создаются они с помощью того же ключевого слова function. Единственное отличие от создания обычных функций заключается в том, что вы также можете добавить один из идентификаторов доступа (public, private, protected) в ее объявлении. В этом методы схожи с переменными класса:

Class ClassName { public function methodName() { // (код) } private function methodName() { // (код) } protected function methodName() { // (код) } }

На заметку: так же, как и в случае с переменными класса, методы public могут быть вызваны откуда угодно, методы private могут вызываться только в пределах класса, а методы protected - из самого класса и его наследника.

Давайте попробуем добавить в наш класс некоторые методы и переменные класса:

  • переменная класса private $loggedIn для идентификации пользователя, т.е. зашел он или нет,
  • метод login(), который будет осуществлять вход на форум, устанавливая значение переменной класса $loggedIn в true,
  • метод logout(), который будет осуществлять выход из форума, устанавливая значение переменной класса $loggedIn в false,
  • метод isLoggedIn(), который будет возвращать значение переменной класса $loggedIn.

Вот наш код:

loggedIn = true; } public function logout() { $this->loggedIn = false; } public function isLoggedIn() { return $this->loggedIn; } } ?>

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

Например, метод login() может получить доступ к переменной класса $loggedIn объекта через $this->loggedIn.

Кстати, наша переменная класса - частная (private), поэтому ее нельзя вызывать из любой части скрипта, а только из методов login(), logout() и isLoggedIn(). Это хороший подход, так как внутренняя часть объекта (например, то, как именно записывается, авторизовался ли пользователь или нет) находится отдельно от остального кода. По возможности старайтесь использовать именно переменные класса private, чтобы ваши объекты были автономными, мобильными и защищенными.

На заметку: переменная класса $username в нашем примере - public. Я это сделал только для того, чтобы продемонстрировать, как можно получать доступ к переменным класса объекта. В реальных проектах скорее нужно сделать эту переменную частной и создать специальные переменные класса public для задания значений имени пользователя, если это необходимо.

Использование методов

Чтобы вызвать метод объекта, воспользуйтесь оператором ->, с которым вы уже успели подружиться.

$object->methodName()

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

loggedIn = true; } public function logout() { $this->loggedIn = false; } public function isLoggedIn() { return $this->loggedIn; } } $member = new Member(); $member->username = "Fred"; echo $member->username . " is " . ($member->
"; $member->login(); echo $member->username . " is " . ($member->isLoggedIn() ? "logged in" : "logged out") . "
"; $member->logout(); echo $member->username . " is " . ($member->isLoggedIn() ? "logged in" : "logged out") . "
"; ?>

Данный скрипт отобразит следующее:

Fred is logged out Fred is logged in Fred is logged out

Вот, как он работает:

  1. После описания класса Member мы создали его объект и сохранили в переменной $member. Также мы дали переменной класса $username данного объекта значение “Fred”.
  2. Затем мы вызвали метод $member->isLoggedIn() для того, чтобы определить, залогинился ли пользователь или нет. Данный метод просто-напросто возвращает значение переменной класса $loggedIn. Так как значение по умолчанию этой переменной класса - false, значит результатом вызова $member->isLoggedIn() будет ложь, поэтому отобразится сообщение "Fred is logged out".
  3. Затем вызовем метод login(). Он установит в true значение переменной класса $loggedIn.
  4. Теперь, при вызове метода $member->isLoggedIn() вернется истина, и выведется сообщение "Fred is logged in".
  5. Вызовем метод logout(), который устанавливает в false значение свойства $loggedIn.
  6. В третий раз вызовем метод $member->isLoggedIn(). Сейчас он вернет false, потому что значение свойства $loggedIn опять установлено в ложь. Так, снова выведется сообщение "Fred is logged out".

На заметку: на случай, если вы в первые увидели такое: ?:, - это тернарный оператор. Это упрощенная версия блоков if … else. Узнать о такого рода операторах можно .

Выводы

В этом уроке вы познакомились с основами ООП в PHP. Вы узнали о таких вещах, как:

  • что такое ООП и почему его полезно применять
  • понятия классов, объектов, переменных класса и методов
  • как создавать классы и объекты
  • как создавать и использовать переменные классов
  • понятия идентификаторов доступа public, private, protected
  • как создавать и применять методы классов

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




Close