Найти в Дзене
ZDG

ООП в PHP: Особенности реализации

Предыдущая часть: ООП в Python

Продолжаем рассматривать реализацию ООП в различных языках. Сегодня будет PHP. Учтите, что на сегодняшний день актуальная версия это 7 и выше.

1. Объект

Здесь мы рассматриваем динамическое создание безымянного объекта. В PHP это можно сделать так:

$a = (object) ['id' => 1, 'title' => 'My Title'];

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

  • массив: $a['title']
  • объект: $a->title

Если, например, в Javascript вы можете обратиться к свойству объекта через синтаксис массива, то есть a.title и a['title'] это одно и то же, в PHP это не так. Если вы создали объект, пусть даже из массива, то теперь это объект, и обращаться к его свойствам можно только через синтаксис объекта: $a->title.

Мы также можем создавать в объекте безымянные функции, что сильно похоже на JavaScript:

Но использовать их напрямую, вот так:

$a->test('Test');

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

$t = $a->test;
$t('Test');

Но это, конечно, неудобно.

Начиная с PHP 7 есть более удобный способ создавать объекты со всеми пирогами:

-2

Здесь создаётся экземпляр объекта безымянного класса (new class), свойства и методы которого объявлены "на лету". Синтаксис тоже как у класса. А классы мы сейчас и рассмотрим.

2. Классы

-3

Здесь описан простейший класс 'A', у которого есть свойство $title и метод test(). Свойства класса описываются через var, а методы через function.

Свойства класса можно инициализировать прямо при объявлении:

-4

Чтобы создать объект из класса 'A', мы пишем: new A().

3. Наследование

-5

Класс 'B' отнаследован от класса 'A', и конечно, унаследовал свойство $title.

4. Множественное наследование

Множественного наследования в PHP нет, но есть его аналог – т.н. трейты (traits). Это, можно сказать, ДНК кода, которую можно внедрять в гены классов.

-6

Трейт объявляется так же, как класс, он имеет такие же свойства и методы, просто вместо слова class используется слово trait. Далее трейт можно включить в код другого класса с помощью use. В данном примере класс B включает в себя трейт A. Включить можно несколько разных трейтов. Получается аналог множественного наследования: класс наследует свойства и методы всех трейтов, которые он в себя включил.

5. Конструктор

-7

Конструктором является метод со специальным именем __construct(). В этом примере мы передаём в конструктор класса A параметр, который присваивается свойству $title.

6. Статические свойства и методы

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

-8

При этом писать var, как раньше, уже не нужно. Вместо static var нужно писать просто static. Это упрощает код, но в то же время вызывает некоторую путаницу и нелогичность. Мы разберёмся с этим в заключении.

Статический доступ к свойству класса выглядит так:

A::$title

И аналогично к методу:

A::test()

Обратите внимание, что когда мы пишем доступ к свойству объекта $title:

$a->title

то не используем символ '$' для свойства $title. Но его надо использовать, когда мы пишем доступ к статическому свойству класса:

A::$title

-9

7. Инкапсуляция

По умолчанию все свойства и методы классов публичные. Чтобы сделать их приватными, нужно использовать ключевое слово private перед свойством или методом:

-10

Чтобы явно указать, что свойство или метод публичные, нужно написать public. PHP также может похвастаться защищёнными свойствами и методами – для этого пишем protected. Обратите внимание, что и в этих случаях писать var больше не нужно.

-11

8. Абстрактные классы и методы

-12

Чтобы сделать класс или метод абстрактным, нужно написать перед ним abstract. В данном примере есть абстрактный класс A с абстрактным методом test(), и класс B, который наследуется от A и реализует у себя метод test().

9. Полиморфизм, Перезапись методов

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

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

function feed_dog(Dog $dog) { ... }

То есть указываем перед именем параметра $dog его тип – Dog. Теперь в функцию можно передавать параметры только типа Dog.

-13

Теперь возвращаемся к проблеме типизированных языков :) У нас есть классы Dog и Cat, которые имеют одинаковый набор свойств. Мы могли бы передать в функцию feed_dog() тип Cat, и она бы ничего не заметила. Но она ждёт только Dog.

Мы делаем класс Pet с общими свойствами, которые есть у Dog и Cat, и наследуем классы Dog и Cat от Pet. Также мы переделываем функцию, чтобы она теперь ждала тип Pet. И теперь мы можем передавать в неё и Dog, и Cat, потому что они – из семейства Pet. Полиморфизм работает (также см. Интерфейсы).

Перезапись методов

Для перезаписи метода в классе-потомке нужно просто сделать такой же метод, как в классе-родителе. Для доступа к методу класса-родителя используем parent:

-14

Перегрузка методов

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

-15

Если мы не передадим в функцию третий параметр $c, он в ней всё равно окажется с автоматически присвоенным значением null.

10. Интерфейсы

-16

11. this

Для доступа к экземпляру объекта изнутри него используем $this:

-17

Кроме того, для доступа к статическим свойствам и методам класса изнутри мы можем использовать self и static:

-18

В большинстве случаев доступ через self и static ничем не отличается, но разница между ними очень серьёзная. Поэтому стоит напрячь мозг, чтобы её понять.

Если мы вызовём у класса A метод test(), то он вызовет метод my_test() того же класса, используя два варианта: self::my_test() и static::my_test(). Результат будет одинаковый: два раза вызовется метод my_test() класса A.

Теперь отнаследуем класс B от класса A. И через класс B снова вызовем метод test(). Так как у класса B нет собственного метода test(), то снова будет вызван метод класса A. Но хотя код в нём тот же самый, результат его работы будет другой. При вызове self::my_test() будет вызван метод my_test() класса A, а при вызове static::my_test() будет вызван метод my_test() класса B.

Почему так происходит?

Когда мы пишем self, то это всегда именно тот класс, в котором написан данный код. Поэтому self в методе класса A всегда будет указывать только на класс A.

Когда мы пишем static, то ссылка на класс получается с учётом наследования. Когда мы вызывали метод из класса A с помощью static, мы получили тот же результат, что и с self. Так как класс A ни от чего не отнаследован, self и static у него совпадают.

Но когда мы вызываем отнаследованный метод класса A из класса B, то, так как этот метод находится в A, self по-прежнему показывает на A, а вот static показывает на тот класс, из которого мы вызвали метод, то есть на B. Поэтому static::my_test() уже указывает на метод my_test() класса B, и мы получаем другой результат.

12. Геттеры и сеттеры

Реализуются с помощью специальных методов __get() и __set():

-19

Мы объявили класс A, у которого есть свойства $test и $vars. Когда мы обращаемся к существующему свойству объекта, такому как $a->test, то геттеры и сеттеры не задействуются. То есть мы можем присвоить или прочитать свойство как обычно. Если же мы обратимся к несуществующему свойству объекта, например,

$a->test2

то в этом случае будет вызван метод-геттер __get() или метод-сеттер __set(). По умолчанию эти методы ничего не делают, но мы можем их переписать. В данном примере сеттер складывает все неизвестные свойства и их значения в массив $vars, а геттер наоборот, ищет по имени значение элемента в массиве $vars.

Таким образом, мы можем создавать в объекте $a любые новые свойства и получать их значения. По факту все они будут храниться в массиве $vars. Написали так:

$a->test2 = 5;

И в массиве $vars создастся элемент с ключом 'test2' и значением 5.

Написали так:

$b = $a->test2;

И в массиве $vars будет найдено значение с ключом 'test2' (если оно там есть).

Заключение

Как можно было видеть выше, не всё в языке PHP идеально с точки зрения синтаксиса. Где-то что-то пишем, а где-то не пишем. История развития языка была долгой и непростой, и местами он действительно заслуживает критики. Зато в результате он имеет практически все возможности, которые должны быть в ООП-языке высокого уровня.

В некоторых примерах выше свойства классов создавались через var и были по умолчанию публичными.

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

-20

Читайте также: