[webshake] Продвинутый курс «ООП в PHP» - Часть 1

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

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

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

Разумеется, что это за продвинутый курс, если в нём не будет уроков о работе с базой данных. Здесь вы узнаете, как правильно работать с MySQL, используя технологию ORM и паттерн ActiveRecord.

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

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

Программа курса

Уровень 1. Основы ООП

  1. Классы и объекты в PHP как основа ООП
  2. Инкапсуляция в PHP
  3. Наследование в PHP
  4. Интерфейсы в PHP
  5. Трейты в PHP
  6. Полиморфизм в PHP
  7. Изучаем абстрактные классы в PHP
  8. Статические методы и свойства в PHP
  9. Объектно-ориентированный подход в PHP

Уровень 2. Архитектура приложения

  1. Неймспейсы и автозагрузка в PHP
  2. Архитектура MVC - Model, View, Controller
  3. Controller в MVC
  4. Фронт-контроллер и роутинг в PHP
  5. View в MVC
  6. Создаём базу данных для будущего блога
  7. Класс для работы с базой данных
  8. Делаем вывод статей на сайте из базы данных
  9. ORM - Object Relational Mapping
  10. Реализуем Active Record в PHP
  11. Паттерн Singleton в PHP
  12. PHP Reflection API
  13. Обновление с помощью Active Record
  14. Вставка с помощью Active Record
  15. Удаление в Active Record
  16. CRUD-операции
  17. M в MVC
  18. Работа с исключениями в PHP

Уровень 3. Пишем блог на PHP

  1. Пишем регистрацию на сайте на PHP
  2. Система активации пользователей по email на PHP
  3. Пишем систему авторизации на PHP
  4. Добавляем статьи в блог на PHP
  5. Редактируем статьи в блоге на PHP
  6. Задание для самостоятельной работы – доводим блог до ума

Уровень 4. PHP для продвинутых

  1. Command Line Interface в PHP
  2. Планировщик заданий cron
  3. Стандарты PSR
  4. Менеджер пакетов Composer
  5. Взаимодействие сервисов и REST API
  6. Учимся писать безопасный код на PHP
  7. Пишем свой фреймворк на PHP

Уровень 5. Готовимся к собеседованию

  1. Готовимся к собеседованию на должность Junior PHP Developer

Классы и объекты в PHP как основа ООП

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

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

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

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

Что вообще за объекты такие?

Давайте на минутку забудем о программировании и подумаем: что такое объекты. Да это всё, что нас окружает. Взгляните на комнату вокруг. Вот же: стул, стол, цветок, ручка, листок бумаги - всё это является объектами. Вы, в том числе, тоже объект. Вы можете взять ручку и начать что-то писать на листе бумаги. Это - взаимодействие между объектами. А ещё, если присмотреться, то можно понять, что все объекты созданы по какому-то определенному шаблону. Ну то есть вы смотрите на разные ручки: одна - желтая, другая - синяя. Но вы смотрите на них и можете безошибочно их классифицировать как ручки. И вы понимаете, что для того, чтобы сделать ручку, нужен какой-то чертёж, по которому её будут собирать. И то же самое со столом, стулом, и даже Вами - ваша ДНК содержит “шаблон”, по которому вы в итоге собрались.

Ровно то же самое происходит и в объектно-ориентированном программировании. Виртуальные объекты создаются на основе специальных шаблонов, называемых классами . Класс - это своего рода “чертёж”, на основе которого будут созданы объекты. Объект же, как из этого следует - это экземпляр какого-то класса. То есть сущность, созданная по этому шаблону.

Классы в PHP

Итак, представим, что мы хотим создать некоторую упрощенную модель котика в PHP. Для этого нам сперва надо сделать его “чертёж”, а именно - создать класс. Мы смотрим на котиков и понимаем что это котики. Это происходит по тому, что все котики имеют какие-то общие признаки. По этим признакам мы можем отличать одни классы объектов от других.

Классы в PHP создаются с помощью слова class , за которым следуем имя класса. Имя класса должно быть написано в CamelCase и начинаться с большой буквы. Затем с новой строки открывается фигурная скобка, далее идёт тело класса, и, наконец, закрывающая фигурная скобка.

Итак, самый простейший пример класса котика выглядит следующим образом:

class Cat
{
    // тело класса
}

Сам по себе этот код сейчас ничего не выведет. Это - просто шаблон. Точно так же, как если бы мы просто описали функцию.

Объекты в PHP

Итак, мы сделали некоторый шаблон, который описывает котиков. Пора бы уже и котика по нему собрать. А именно - создать объект.

Объекты создаются с помощью ключевого слова new:

<?php
class Cat
{
}

$cat1 = new Cat();

var_dump($cat1);

Так мы создали объект с типом Cat и вывели его с помощью var_dump().

object(Cat)[1]

Как видим, в переменной лежит объект (object). И больше пока ничего.

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

<?php

class Cat
{
}

$cat1 = new Cat();
$cat2 = new Cat();
$cat3 = new Cat();

var_dump($cat1);
var_dump($cat2);
var_dump($cat3);

Результат этого кода:

object(Cat)[1]
object(Cat)[2]
object(Cat)[3]

Видим, что это действительно объекты класса Cat, однако их идентификаторы разные: 1, 2 и 3 (в PHP каждый отдельный объект имеет собственный идентификатор).

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

Инкапсуляция в PHP

В этом уроке мы узнаем о первом из трёх китов ООП - инкапсуляции . Инкапсуляция (лат. in capsula; от capsula «коробочка») — размещение в оболочке, изоляция, закрытие чего-либо с целью исключения влияния на окружающее. О том, как это используется в объектно-ориентированном программировании, вы узнаете по ходу этого урока.

Свойства объектов

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

  • имя;
  • цвет;
  • вес.

Давайте теперь создадим более похожий на реального котика класс:

<?php
class Cat
{
    public $name;
    public $color;
    public $weight;
}

Всё это: $name, $color, $weight - свойства будущих объектов этого класса. Перед именем свойства всегда ставится модификатор доступа . В нашем случае - это public. Это слово говорит о том, что данное свойство будет доступно всем, кто работает с объектами данного класса. Есть и другие модификаторы доступа, но о них чуть ниже.

И снова сам по себе этот код сейчас ничего не выведет. Это опять - просто шаблон.

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

<?php
class Cat
{
    public $name;
    public $color;
    public $weight;
}

$cat1 = new Cat();

var_dump($cat1);

Так мы создали объект с типом Cat и вывели его с помощью var_dump().

object(Cat)[1]
  public 'name' => null
  public 'color' => null
  public 'weight' => null

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

...

$cat1->name = 'Снежок';
$cat1->color = 'white';
$cat1->weight = 3.5;

Оператор -> (стрелочка, состоящая из двух знаков - “тире” и “больше”) используется для доступа к свойствам объекта. В данном коде мы обратились к каждому свойству отдельно и присвоили им значения. Если теперь мы выведем $cat1 с помощью var_dump(), то получим следующее:

object(Cat)[1]
  public 'name' => string 'Снежок' (length=12)
  public 'color' => string 'white' (length=5)
  public 'weight' => float 3.5

Как видим, это уже не ерунда какая-то, а белый Снежок, который весит три с половиной кило.

Теперь мы можем обратиться к свойству этого кота и узнать его имя.

echo $cat1->name;

И получим в результате “Снежок”.

Можем создать несколько котов и задать им разные свойства:

<?php

class Cat
{
    public $name;
    public $color;
    public $weight;
}

$cat1 = new Cat();
$cat1->name = 'Снежок';
$cat1->color = 'white';
$cat1->weight = 3.5;

$cat2 = new Cat();
$cat2->name = 'Барсик';
$cat2->color = 'black';
$cat2->weight = 6.2;

var_dump($cat1);
var_dump($cat2);

Результат получится вполне ожидаемый:

object(Cat)[1]
  public 'name' => string 'Снежок' (length=12)
  public 'color' => string 'white' (length=5)
  public 'weight' => float 3.5
object(Cat)[2]
  public 'name' => string 'Барсик' (length=12)
  public 'color' => string 'black' (length=5)
  public 'weight' => float 6.2

Два разных объекта со своими значениями свойств.

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

То, что внутри объектов есть свойства - это уже проявление инкапсуляции. У объекта есть свойства, он их внутри себя содержит - вот и “капсула”.

Методы объектов

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

Методы объявляются следующим образом:

class Cat
{
    public $name;
    public $color;
    public $weight;

    public function sayHello()
    {
        echo 'Мяу!';
    }
}

public - модификатор доступа к методу, говорящий о том, что его могут вызвать все, кто пользуется объектом, sayHello - имя метода, а далее идут аргументы (в нашем случае их нет), а далее - тело метода, в котором мы просто выводим строку ‘Мяу!’;

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

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

$cat1 = new Cat();
$cat1->name = 'Снежок';
$cat1->color = 'white';
$cat1->weight = 3.5;

$cat1->sayHello();

Этот код выведет строку ‘Мяу!’. Вот так вот, с нами поздоровался виртуальный кот!

Переменная $this

Да только методы - это не такие уж и простые функции. Внутри методов доступна специальная переменная $this, и в ней хранится… наш текущий созданный объект. БДЫЩЬ! Мозг взорвался :slight_smile:

На деле всё не так уж и сложно. Мы можем с помощью этой переменной обращаться к другим методам и свойствам данного объекта. Например, давайте научим кота здороваться по-человечески. Пусть он будет называть своё имя. Для этого нам нужно переписать метод sayHello() следующим образом:

class Cat
{
    public $name;
    public $color;
    public $weight;

    public function sayHello()
    {
        echo 'Привет! Меня зовут ' . $this->name . '.';
    }
}

И теперь, когда мы создадим новый объект кота, и попросим его с нами поздороваться, то $this->name вернёт значение свойства name у текущего объекта.

<?php

class Cat
{
    public $name;
    public $color;
    public $weight;

    public function sayHello()
    {
        echo 'Привет! Меня зовут ' . $this->name . '.';
    }
}

$cat1 = new Cat();
$cat1->name = 'Снежок';
$cat1->sayHello();

$cat2 = new Cat();
$cat2->name = 'Барсик';
$cat2->sayHello();

Данный код выведет следующее:

Привет! Меня зовут Снежок.
Привет! Меня зовут Барсик.

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

И методы, и переменная $this - тоже инкапсуляция ! Но и на этом ещё не всё :slight_smile:

Модификаторы доступа

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

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

Например, давайте изменим модификатор для свойства name:

class Cat
{
    private $name;
    public $color;
    public $weight;

    public function sayHello()
    {
        echo 'Привет! Меня зовут ' . $this->name . '.';
    }
}

Давайте теперь попытаемся изменить это свойство у объекта:

$cat1 = new Cat();
$cat1->name = 'Снежок';

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

Однако, мы можем написать публичный метод, который позволит задать данное свойство с помощью него. Назовём его setName(). Он будет брать переданную в него строку и устанавливать это значение в свойство name.

class Cat
{
    private $name;
    public $color;
    public $weight;

    public function sayHello()
    {
        echo 'Привет! Меня зовут ' . $this->name . '.';
    }

    public function setName(string $name)
    {
        $this->name = $name;
    }
}

Теперь давайте зададим имя коту с помощью этого метода:

$cat1 = new Cat();
$cat1->setName('Снежок');
$cat1->sayHello();

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

echo $cat1->name;

то мы бы снова получили ошибку доступа.

Как вы могли заметить с помощью метода setName мы позволяем задать в свойство name только строку. Ни число, ни массив, ни что-то ещё, а именно - строку. Это хороший подход - это позволяет использовать различного рода валидации. Ведь внутри метода вы можете выполнить какие-то проверки, прежде чем положить значение в свойство. Это позволяет избежать ситуаций, когда в свойства можно засунуть любую ерунду. Такие методы как setName(), задающие значения свойствам объекта называются сеттерами .

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

class Cat
{
    private $name;
    public $color;
    public $weight;

    public function sayHello()
    {
        echo 'Привет! Меня зовут ' . $this->name . '.';
    }

    public function setName(string $name)
    {
        $this->name = $name;
    }

    public function getName(): string
    {
        return $this->name;
    }
}

Теперь мы можем просто получить имя кота извне:

$cat1 = new Cat();
$cat1->setName('Снежок');
echo $cat1->getName();

Такие методы, в свою очередь, именуются геттерами .

Модификаторы доступа - это ещё одно проявление инкапсуляции.

Конструктор

А теперь давайте возьмём и сломаем одного кота :slight_smile:

Для этого мы не будем давать ему имя, и вызовем метод getName().

$cat1 = new Cat();
echo $cat1->getName();

Что произойдёт? Правильно - ошибка!

Ведь мы описали, что getName() всегда должна отдавать строку. А в нашем объекте возвращается null.

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

Метод-конструктор должен называться __construct. Именно так и никак иначе.

Давайте создадим конструктор для нашего кота, который будет иметь обязательный аргумент $name с типом строка.

class Cat
{
    private $name;

    public function __construct(string $name)
    {
        $this->name = $name;
    }

    public function sayHello()
    {
        echo 'Привет! Меня зовут ' . $this->name . '.';
    }

    public function setName(string $name)
    {
        $this->name = $name;
    }

    public function getName(): string
    {
        return $this->name;
    }
}

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

Теперь чтобы создать кота с именем Снежок мы должны передать аргумент при создании нового объекта:

$cat1 = new Cat('Снежок');

И вот что сейчас произошло: аргумент, переданный в круглые скобки, был передан в метод __construct(). Там это значение установилось в свойство объекта name.

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

echo $cat1->getName();

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

$cat1 = new Cat();

Мы получим ошибку.
Ошибка об отсутствии нужного количества аргументов конструктора

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

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

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

Это был первый кит ООП - инкапсуляция. До встречи на следующем уроке, а пока за домашку.

  • Дополните метод sayHello(), чтобы котики после того, как назвали своё имя, говорили о том, какого они цвета.
  • Сделайте свойство color приватным и добавьте в конструктор ещё один аргумент, через который при создании котика будет задаваться его цвет.
  • Сделайте геттер, который будет позволять получить свойство color.
  • Подумайте, стоит ли давать возможность менять котикам цвета после их создания? Если вам хватило совести сказать да - добавьте ещё и сеттер для этого свойства. Это вам в наказание - хорошие люди котов не перекрашивают.
  • Создайте теперь белого Снежка и рыжего Барсика и попросите их рассказать о себе.

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

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

Котики бывают двух полов: мальчики и девочки. Все мы прекрасно понимаем, чем они отличаются. Однако, у них есть и общие черты – независимо от пола и у тех и у тех есть четыре лапы, есть хвост, голова, усы и далее по списку. То есть есть что-то общее, а есть что-то, что их отличает. Так сказать, детали реализации.

В природе таких примеров уйма – это деление существ по классам и царствам, разделение по половому признаку, разделение по расовой принадлежности, и так далее. Любую вещь можно отнести к какому-то классу, а по каким-то другим признакам – отличить её от других вещей.

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

Предположим, на сайте есть простые записи в блоге. У них есть заголовок и текст. Администратор их может редактировать, добавлять что-то новое. А есть посты-уроки, как тот, что вы читаете прямо сейчас. И они должны иметь те же свойства и уметь делать всё то же самое, что и обычные посты, но вдобавок ко всему, у них должна быть домашка.

Давайте создадим для простых записей в блоге класс Post. Пусть, с некоторыми упрощениями, он выглядит так:

class Post
{
    private $title;
    private $text;

    public function __construct(string $title, string $text)
    {
        $this->title = $title;
        $this->text = $text;
    }

    public function getTitle()
    {
        return $this->title;
    }

    public function setTitle($title): void
    {
        $this->title = $title;
    }

    public function getText()
    {
        return $this->text;
    }

    public function setText($text): void
    {
        $this->text = $text;
    }
}

Неужели, для того чтобы сделать класс урока с домашкой нам нужно копировать весь этот код в новый класс Lesson, а затем добавлять новое свойство homework и добавлять геттер и сеттер?

Оказывается – нет. Благодаря наследованию, разумеется. Как это работает? Да проще простого!

Класс может унаследовать от другого класса свойства и методы. Делается это при помощи ключевого слова extends (англ. - расширять). Вот так:

class Lesson extends Post
{
    //тут уже тело класса Lesson
}

Класс Lesson называют классом-наследником, или дочерним классом. Класс Post – родительский класс.
В качестве родительского класса при помощи слова extends можно указать только один класс. Однако, у класса Lesson, в свою очередь, тоже могут быть наследники. Они унаследуют все свойства и методы всех своих родителей.

При этом доступными внутри объектов класса-наследника будут только свойства или методы, объявленные в родительском классе как public или protected . Свойства и методы, с модификатором доступа private не будут унаследованы дочерними классами.

То есть, если мы хотим в классе Lesson объявить метод, который будет работать со свойствами title и text, то мы должны определить эти свойства не как private , а как protected :

class Post
{
    protected $title;
    protected $text;
    ...

Теперь мы можем работать с ними и в классе Lesson:

class Lesson extends Post
{
    private $homework;

    public function __construct(string $title, string $text, string $homework)
    {
        $this->title = $title;
        $this->text = $text;
        $this->homework = $homework;
    }

    public function getHomework(): string
    {
        return $this->homework;
    }

    public function setHomework(string $homework): void
    {
        $this->homework = $homework;
    }
}

Вот таким нехитрым образом мы расширили класс Lesson, унаследовав его от класса Post. Как видим, мы избежали копирования кода, что не может не радовать.

При этом в объектах класса Lesson нам так же доступны все protected- и public-методы, объявленные в родительском классе. Давайте убедимся, в этом.

$lesson = new Lesson('Заголовок', 'Текст', 'Домашка');
echo 'Название урока: ' . $lesson->getTitle();

Мы получим следующее:

Название урока: Заголовок

Как видим, всё прекрасно работает.

Модификаторы доступа

Давайте теперь вернёмся к модификаторам доступа и до конца проясним ситуацию, как каждый модификатор влияет на методы и свойства:

  • private – доступны только внутри объектов этого класса, недоступны в объектах классов-наследников;
  • protected – доступны внутри объектов этого класса и всем объектам классов-наследников. При этом недоступны извне;
  • public – доступны как внутри объектов класса, так и снаружи – можем напрямую обращаться к ним извне. Доступны объектам классов-наследников.

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

Вызов родительского метода

Если присмотреться к классам Post и Lesson, то можно заметить некоторое дублирование кода в конструкторе. Мы и там и там выполняем одинаковые действия для свойств title и text. Было бы неплохо от этого избавиться, воспользовавшись в Lesson уже готовым функционалом из Post.

Для этого мы можем вызвать родительский конструктор в конструкторе дочернего класса. Делается это при помощи ключевого слова parent и двойного двоеточия.

class Lesson extends Post
{
    private $homework;

    public function __construct(string $title, string $text, string $homework)
    {
        parent::__construct($title, $text);
        $this->homework = $homework;
    }
    ...

Что именно произойдёт? В момент вызова конструктора класса Lesson (при создании нового объекта), сначала произойдёт вызов метода __construct из родительского класса, а затем задастся свойство homework. При этом этот метод из родительского класса отработает для свойств класса-наследника. Можно представить, что мы просто скопировали сюда содержимое этого метода из класса Post и вставили его сюда. Именно так и происходит, когда этот код выполняется.

Давайте проверим что всё работает:

$lesson = new Lesson('Заголовок', 'Текст', 'Домашка');
var_dump($lesson);

Результат:

object(Lesson)[1]
  private 'homework' => string 'Домашка' (length=14)
  protected 'title' => string 'Заголовок' (length=18)
  protected 'text' => string 'Текст' (length=10)

Как видим, всё успешно отработало и мы получили то, что хотели.
Вызывать с помощью parent:: можно не только конструктор, но и любой другой метод.

Как видим, тема наследования тесно связана с темой инкапсуляции. Одно без другого не работает. Уберёте одно – и другое сломается. На этом с наследованием всё, делайте домашку, а потом переходите к изучению полиморфизма.

Полный код, получившийся в ходе урока:

<?php

class Post
{
    protected $title;
    protected $text;

    public function __construct(string $title, string $text)
    {
        $this->title = $title;
        $this->text = $text;
    }

    public function getTitle()
    {
        return $this->title;
    }

    public function setTitle($title): void
    {
        $this->title = $title;
    }

    public function getText()
    {
        return $this->text;
    }

    public function setText($text): void
    {
        $this->text = $text;
    }
}

class Lesson extends Post
{
    private $homework;

    public function __construct(string $title, string $text, string $homework)
    {
        parent::__construct($title, $text);
        $this->homework = $homework;
    }

    public function getHomework(): string
    {
        return $this->homework;
    }

    public function setHomework(string $homework): void
    {
        $this->homework = $homework;
    }
}

$lesson = new Lesson('Заголовок', 'Текст', 'Домашка');
var_dump($lesson);

Домашнее задание

  • Создайте ещё один класс, являющийся наследником класса Lesson - PaidLesson (платный урок).
  • Объявите в нем свойство price (цена), а также геттеры и сеттеры для этого свойства. Добавьте в конструкторе параметр, через который это свойство будет устанавливаться при создании объекта.
  • Создайте объект этого класса со следующими свойствами:
    заголовок: Урок о наследовании в PHP
    текст: Лол, кек, чебурек
    домашка: Ложитесь спать, утро вечера мудренее
    цена: 99.90
  • Выведите этот объект с помощью var_dump()

Интерфейсы в PHP

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

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

Прямоугольник.

class Rectangle
{
    private $x;
    private $y;

    public function __construct(float $x, float $y)
    {
        $this->x = $x;
        $this->y = $y;
    }

    public function calculateSquare(): float
    {
        return $this->x * $this->y;
    }
}

Квадрат.

class Square
{
    private $x;

    public function __construct(float $x)
    {
        $this->x = $x;
    }

    public function calculateSquare(): float
    {
        return $this->x ** 2;
    }
}

Круг.

class Circle
{
    private $r;

    public function __construct(float $r)
    {
        $this->r = $r;
    }

    public function calculateSquare(): float
    {
        $pi = 3.1416;
        return $pi * ($this->r ** 2);
    }
}

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

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

const PI =  3.1416;

Константы принято задавать в самом начале класса и называть их CAPS-ом с подчеркушками. Вот примеры того, как могут называться константы: DB_NAME, COUNT_OF_OBJECTS.

Для того, чтобы обратиться к константе, нужно использовать конструкцию self::ИМЯ_КОНСТАНТЫ, или ИмяКласса::ИМЯ_КОНСТАНТЫ. Ключевое слово self – это обращение к текущему классу (как $this – обращение к текущему объекту, не путайте эти понятия). Константы принадлежат классу, а не его объектам.

Давайте вынесем число Пи в константу.

class Circle
{
    const PI = 3.1416;

    private $r;

    public function __construct(float $r)
    {
        $this->r = $r;
    }

    public function calculateSquare(): float
    {
        return self::PI * ($this->r ** 2);
    }
}

Теперь мы можем использовать её и в других методах. Или даже в других классах, обратившись к ней через Circle::PI.

Интерфейсы

Окей, разобрались с константами и имеем в итоге 3 объекта, описывающих геометрические фигуры и реализацию для вычисления их площадей. Если присмотреться, то мы видим, что во всех классах определён метод calculateSquare(), возвращающий float. Можно сказать, что у них есть что-то общее.

Допустим, мы хотели бы, чтобы у нас были фигуры, которые умеют считать свою площадь. То есть, говоря чуть более абстрактно, какие-то наши классы обязаны реализовать какой-то внешний интерфейс, а именно – иметь метод calculateSquare(), который всегда возвращает float.
Для этой задачи в PHP есть интерфейсы. Это такие «контракты», которые класс должен соблюдать, если он на это «подписался». А говоря языком программистов, классы могут реализовывать интерфейсы .

Интерфейс – это описание public методов, которые представляют собой только название метода, описание их аргументов и возвращаемый тип. Тело метода в интерфейсе не описывается.
Давайте создадим интерфейс для нашего случая.

interface CalculateSquare
{
    public function calculateSquare(): float;
}

Чтобы обязать класс реализовать этот интерфейс нужно использовать слово implements после имени класса.

class Circle implements CalculateSquare
{
    ...
}

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

class Circle implements CalculateSquare, Interface2, Interface3
{
    ...
}

IDE PhpStorm автоматически понимает, что наш класс реализует интерфейс и рисует слева от методов специальные иконки. Если по ним кликнуть, то нас перекинет на интерфейс.
Подсветка интерфейсов в PhpStorm

Ну и в интерфейсе если кликнуть на такую иконку, то нам откроется список мест, где этот интерфейс реализован.
Реализации интерфейса

Если же мы напишем, что класс реализует какой-то интерфейс, но не реализуем его, то получим ошибку. Об этом нам даже подскажет IDE. Давайте удалим метод calculateSquare() из класса Circle. IDE любезно подчеркнёт красным строку, в которой мы говорим, что класс реализует интерфейс.
Напоминание о необходимости реализации интерфейса

Если же мы попробуем запустить этот код, то и вовсе словим фатальную ошибку.
Ошибка при нереализованном интерфейсе

Так что давайте этот метод вернём обратно =)

Что ещё стоит сказать об интерфейсах – один интерфейс может содержать требования по реализации нескольких методов. Они просто перечисляются один за другим, вот так:
interface CalculateSquare

{
    public function calculateSquare(): float;

    public function getId(): int;

    ...
}

Но мы пока ограничимся одним методом calculateSquare().

Окей, так для чего это всё?

В программировании зачастую требуется проверить, что перед нами сейчас какой-то конкретный тип объектов, то есть что перед нами экземпляр какого-то класса, либо что этот объект реализует какой-то интерфейс. Для этого используется конструкция instanceof . С её помощью можно понять, является ли объект экземпляром какого-то класса, или реализует интерфейс. Эта конструкция возвращает true или false.

$circle1 = new Circle(2.5);
var_dump($circle1 instanceof Circle);

Результат:

boolean true

Всё верно, объект $circle1 является экземпляром класса Circle. Давайте теперь проверим, является ли он экземпляром класса Rectangle.

$circle1 = new Circle(2.5);
var_dump($circle1 instanceof Rectangle);

Результат:

boolean false

И снова всё верно, он не является экземпляром класса Rectangle.

А теперь давайте проверим, является ли он объектом, класс которого реализует интерфейс CalculateSquare.

$circle1 = new Circle(2.5);
var_dump($circle1 instanceof CalculateSquare);

И мы получим:

boolean true

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

class Square implements CalculateSquare
...
class Rectangle implements CalculateSquare
...
class Circle implements CalculateSquare

Давайте теперь насоздаём объектов этих классов и положим их в массив:

$objects = [
    new Square(5),
    new Rectangle(2, 4),
    new Circle(5)
];

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

$objects = [
    new Square(5),
    new Rectangle(2, 4),
    new Circle(5)
];

foreach ($objects as $object) {
    if ($object instanceof CalculateSquare) {
        echo 'Объект реализует интерфейс CalculateSquare. Площадь: ' . $object->calculateSquare();
        echo '<br>';
    }
}

Результат:

Объект реализует интерфейс CalculateSquare. Площадь: 25
Объект реализует интерфейс CalculateSquare. Площадь: 8
Объект реализует интерфейс CalculateSquare. Площадь: 78.54

Давайте теперь уберём из класса Rectangle упоминание о том, что он реализует этот интерфейс.
class Rectangle

{
...

И снова попробуем запустить код.

Объект реализует интерфейс CalculateSquare. Площадь: 25
Объект реализует интерфейс CalculateSquare. Площадь: 78.54

Как видим, проверка успешно отработала и объект класса Rectangle был пропущен.

Полный код, полученный в ходе урока:

<?php

interface CalculateSquare
{
    public function calculateSquare(): float;
}

class Circle implements CalculateSquare
{
    const PI = 3.1416;

    private $r;

    public function __construct(float $r)
    {
        $this->r = $r;
    }

    public function calculateSquare(): float
    {
        return self::PI * ($this->r ** 2);
    }
}

class Rectangle
{
    private $x;
    private $y;

    public function __construct(float $x, float $y)
    {
        $this->x = $x;
        $this->y = $y;
    }

    public function calculateSquare(): float
    {
        return $this->x * $this->y;
    }
}

class Square implements CalculateSquare
{
    private $x;

    public function __construct(float $x)
    {
        $this->x = $x;
    }

    public function calculateSquare(): float
    {
        return $this->x ** 2;
    }
}

$objects = [
    new Square(5),
    new Rectangle(2, 4),
    new Circle(5)
];

foreach ($objects as $object) {
    if ($object instanceof CalculateSquare) {
        echo 'Объект реализует интерфейс CalculateSquare. Площадь: ' . $object->calculateSquare();
        echo '<br>';
    }
}

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

Домашнее задание

  • Познакомьтесь самостоятельно с функцией get_class().
  • Дополните информацию об объекте, для которого считается площадь – пишите что это объект такого-то класса.
  • Для объектов, которые не реализуют интерфейс CalculateSquare пишите:
    Объект класса ТУТ_НАЗВАНИЕ_КЛАССА не реализует интерфейс CalculateSquare.

Трейты в PHP

Как мы знаем, в PHP класс может наследоваться только от одного класса. Но как быть, если мы хотим иметь какой-либо функционал в разных классах, которые не являются наследниками друг друга? Для этого придумали трейты. Трейты в PHP – это такой механизм, который позволяет внутри классов избегать повторного использования кода.

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

<?php

class Man
{
    public function sayYourClass(): string
    {
        return 'My class is Man';
    }
}

class Box
{
    public function sayYourClass(): string
    {
        return 'My class is Box';
    }
}

$man = new Man();
$box = new Box();

echo $man->sayYourClass();
echo $box->sayYourClass();

Результат:

My class is Man
My class is Box

В PHP можно получить имя класса с помощью конструкции ИмяКласса::class. Например:

echo Box::class;

Выведет:

Box

Если мы находимся внутри класса, например, в каком-то его методе, то мы можем ИмяКласса заменить словом self – текущий класс.

<?php

class Man
{
    public function sayYourClass(): string
    {
        return 'My class is ' . self::class;
    }
}

class Box
{
    public function sayYourClass(): string
    {
        return 'My class is ' . self::class;
    }
}

$man = new Man();
$box = new Box();

echo $man->sayYourClass();
echo $box->sayYourClass();

Результат останется прежним.

My class is Man
My class is Box

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

trait SayYourClassTrait
{
    public function sayYourClass(): string
    {
        return 'My class is ' . self::class;
    }
}

Ничего сложного. Теперь мы можем просто использовать этот трейт в двух наших классах. Для этого используется конструкция use.

class Man
{
    use SayYourClassTrait;
}

class Box
{
    use SayYourClassTrait;
}

$man = new Man();
$box = new Box();

echo $man->sayYourClass();
echo $box->sayYourClass();

И снова увидим нужный результат:

My class is Man
My class is Box

Код из трейта SayYourClass просто подставился в классы, где мы его использовали с помощью слова use. В self будет лежать класс, в котором сейчас исполняется этот код. Вот так всё просто.

Трейты также довольно плотно пересекаются с темой интерфейсов.

Давайте добавим интерфейс, который будет обязывать классы иметь метод sayYourClass().

interface ISayYourClass
{
    public function sayYourClass(): string;
}

Теперь наши классы могут его реализовать – этот метод реализован в трейте, а трейт используется в классе.

Давайте посмотрим на получившийся код.

interface ISayYourClass
{
    public function sayYourClass(): string;
}

trait SayYourClassTrait
{
    public function sayYourClass(): string
    {
        return 'My class is ' . self::class;
    }
}

class Man implements ISayYourClass
{
    use SayYourClassTrait;
}

class Box implements ISayYourClass
{
    use SayYourClassTrait;
}

$man = new Man();
$box = new Box();

echo $man->sayYourClass();
echo $box->sayYourClass();

В PHP класс может наследоваться только от одного класса, помните? Так вот с помощью интерфейсов и трейтов мы можем это ограничение немного обойти. Мы теперь можем добавлять некоторый функционал в классы, которые не имеют какого-то общего поведения в целом. Но они при этом объединены одним интерфейсом. А проверять, реализует ли объект интерфейс, мы уже умеем – с помощью конструкции instanceof.

Таким образом, мы можем обходить ограничения наследования:

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

Полиморфизм в PHP

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

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

class A
{
    public function sayHello()
    {
        return 'Hello, I am A';
    }
}

$a = new A();

var_dump($a instanceof A); // true

Давайте создадим ещё один класс, который будет наследником класса A, и выполним ту же проверку для объекта этого класса.

class B extends A
{
}

$b = new B();

var_dump($b instanceof B); // true

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

class B extends A
{
}

$b = new B();

var_dump($b instanceof A); // тоже true!

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

class A
{
    public function sayHello()
    {
        return 'Hello, I am A';
    }
}

class B extends A
{
}

$a = new A();
$b = new B();

var_dump($a instanceof B); // false

Согласитесь, это вполне логично.

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

Как мы помним, методы объектов родительских классов у нас доступны и в дочерних – они наследуются. Соответственно мы можем быть уверены, что эти же методы есть и у дочерних объектов. Конкретно в нашем примере – у объектов класса B будет метод sayHello(), унаследованный от A.

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

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

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

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

Переопределение методов

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

class A
{
    public function sayHello()
    {
        return 'Hello, I am A';
    }
}

class B extends A
{
}

$b = new B();

echo $b->sayHello(); // Hello, I am A

Однако, мы можем переопределить этот метод в классе B. Для этого мы описываем метод с таким же названием и описываем в нём свою логику:

class A
{
    public function sayHello()
    {
        return 'Hello, I am A';
    }
}

class B extends A
{
    public function sayHello()
    {
        return 'Hello, I am B';
    }
}

$b = new B();

echo $b->sayHello(); // 'Hello, I am B

Мы также можем вызвать родительский метод в дочернем при помощи слова parent и двойного двоеточия.

<?php

class A
{
    public function sayHello()
    {
        return 'Hello, I am A';
    }
}

class B extends A
{
    public function sayHello()
    {
        return parent::sayHello() . '. It was joke, I am B :)';
    }
}

$b = new B();

echo $b->sayHello(); // Hello, I am A. It was joke, I am B :)

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

И ещё один примерчик, тоже искуственный.

class A
{
    public function method1()
    {
        return $this->method2();
    }

    protected function method2()
    {
        return 'A';
    }
}

class B extends A
{
    protected function method2()
    {
        return 'B';
    }
}

$b = new B();

echo $b->method1();

Как думаете, что выведет этот код?

A или B?

Не знаю, что вы ответили, но он в любом случае выведет B. Внутри метода method1() будет вызван тот метод method2(), который определён для класса, в котором его выполняют. То есть $this не привязан напрямую к классу A. Когда мы вызываем method1() у экземпляра класса B, то $this внутри него будет связан с классом B, и вызовется метод method2(), определенный именно в классе B.

И это – тоже полиморфизм. $this привязывается к объекту, в котором код выполняется, а не там, где он был описан.

Изучаем абстрактные классы в PHP

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

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

abstract class AbstractClass
{
    // тело абстрактного класса
}

Давайте попробуем создать объект этого класса:

abstract class AbstractClass
{
    // тело абстрактного класса
}

$object = new AbstractClass();

Мы получим ошибку.
ошибка при создании объекта абстрактного класса

Создать объект абстрактного класса нельзя – от него можно только наследоваться. Примерно как с интерфейсами – их мы можем только реализовывать в других классах.

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

abstract class AbstractClass
{
    abstract public function getValue();
}

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

abstract class AbstractClass
{
    abstract public function getValue();

    public function printValue()
    {
        echo 'Значение: ' . $this->getValue();
    }
}

То есть в классах-наследниках будет доступен уже реализованный метод printValue(), который без проблем можно переопределить. Он вызывает метод getValue(), который в самом классе пока не реализован, но мы можем его использовать в других методах.

Дочерние классы будут обязаны реализовать метод getValue(). Давайте опишем класс-наследник от этого абстрактного класса.

abstract class AbstractClass
{
    abstract public function getValue();

    public function printValue()
    {
        echo 'Значение: ' . $this->getValue();
    }
}

class ClassA extends AbstractClass
{

}

Такой код вызовет ошибку – класс ClassA не реализовал в себе метод getValue().

Ошибка из-за нереализованности абстрактного метода

Давайте это исправим.

abstract class AbstractClass
{
    abstract public function getValue();

    public function printValue()
    {
        echo 'Значение: ' . $this->getValue();
    }
}

class ClassA extends AbstractClass
{
    private $value;

    public function __construct(string $value)
    {
        $this->value = $value;
    }

    public function getValue()
    {
        return $this->value;
    }
}

$objectA = new ClassA('kek');
$objectA->printValue();

Этот код вполне нормально отработает, и выведет на экран “kek”.

Домашнее задание

Создайте абстрактный класс Human.

abstract class HumanAbstract
{
    private $name;

    public function __construct(string $name)
    {
        $this->name = $name;
    }

    public function getName(): string
    {
        return $this->name;
    }

    abstract public function getGreetings(): string;
    abstract public function getMyNameIs(): string;

    public function introduceYourself(): string
    {
        return $this->getGreetings() . '! ' . $this->getMyNameIs() . ' ' . $this->getName() . '.';
    }
}

Отнаследуйте от него 2 класса: RussianHuman и EnglishHuman.

Реализуйте в них методы getGreetings(), которые будут возвращать приветствие на разных языках, вроде «Привет».

Реализуйте в них методы getMyNameIs(), которые будут возвращать на разных языках слова «Меня зовут».

В итоге метод introduceYourself должен возвращать строку, вроде «Привет! Меня зовут Иван.»

Создайте объекты этих классов и заставьте их поздороваться.

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

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

Определяются статические свойства и методы с помощью ключевого свойства static.

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

Давайте рассмотрим самый простой пример статического метода.

class A {
    public static function test(int $x)
    {
        return 'x = ' . $x;
    }
}

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

echo A::test(5);

Результат:

x = 5

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

class User
{
    private $role;

    private $name;

    public function __construct(string $role, string $name)
    {
        $this->role = $role;
        $this->name = $name;
    }
}

Мы с помощью конструктора можем создавать пользователей с разными ролями (модератор, администратор, простой пользователь) и именами.

Чтобы создать администратора, нам нужно сделать следующее:

$admin = new User('admin', 'Иван');

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

class User
{
    private $role;

    private $name;

    public function __construct(string $role, string $name)
    {
        $this->role = $role;
        $this->name = $name;
    }

    public static function createAdmin(string $name)
    {
        return new self('admin', $name);
    }
}

Этот метод возвращает новый объект текущего класса (благодаря слову self), и передаёт ему всегда в аргумент $role значение ‘admin’. Использование такого метода будет выглядеть следующим образом.

$admin = User::createAdmin('Иван');
var_dump($admin);

Вывод будет следующим:

object(User)[1]
  private 'role' => string 'admin' (length=5)
  private 'name' => string 'Иван' (length=8)

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

Отличие от методов объектов

В отличие от методов объектов, в статических методах нет слова $this – оно указывает только на текущий объект. Если объекта нет – нет и $this!

Статические свойства

А что на счёт статических свойств? Для чего можно использовать их?
Давайте создадим класс со статическим свойством:

class A
{
    public static $x;
}

Мы можем читать и писать в это свойство, не создавая объектов этого класса:

A::$x = 5;
var_dump(A::$x); // 5

Кроме того, эти же свойства будут доступны и у объектов этого класса:

A::$x = 5;
$a = new A();
var_dump($a::$x); // 5

И даже внутри этих объектов мы сможем достучаться до этих свойств, при помощи слова self::

class A
{
    public static $x;

    public function getX()
    {
        return self::$x;
    }
}

A::$x = 5;
$a = new A();
var_dump($a->getX()); // 5

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

class A
{
    public static $x;

    public static function getX()
    {
        return self::$x;
    }
}

A::$x = 5;
$a = new A();
var_dump($a::getX());

Применение статических свойств

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

class Human
{
    private static $count = 0;

    public static function getCount()
    {
        return self::$count;
    }
}

Давайте выведем значение этой переменной.

echo 'Людей уже ' . Human::getCount(); // 0

Что нужно сделать, чтобы при создании нового объекта класса Human, это число увеличивалось? Правильно, просто увеличивать это значение в конструкторе – он ведь вызывается каждый раз при создании объекта.

class Human
{
    private static $count = 0;

    public function __construct()
    {
        self::$count++;
    }

    public static function getCount()
    {
        return self::$count;
    }
}

Давайте проверим, что всё работает:

$human1 = new Human();
$human2 = new Human();
$human3 = new Human();
echo 'Людей уже ' . Human::getCount(); // 3

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

Объектно-ориентированный подход в PHP

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

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

Мы создаём объекты, добавляем в них свойства и методы. Всё что с модификатором public – это интерфейс, через который мы можем взаимодействовать с объектом. Через этот интерфейс мы можем изменять внутреннее состояние объекта, добиться от него каких-то действий, или получить от него что-то. Аналогично и в жизни – у человека, например, есть органы чувств – это тоже интерфейс. Сказав что-то человеку в его ухо, можно неплохо так изменить его внутреннее состояние. Можно помахать человеку рукой, в интерфейс «глаз». В ответ он может return-уть в ответ тем же, а может и нет, зависит от его внутреннего состояния.

Как вы понимаете, возможность разделить логику программы по отдельным классам – это большой плюс. Это позволяет разделить зоны ответственности, каждый класс содержит свою логику, и вы будете знать, что класс Article – содержит в себе функционал, который отвечает за статьи на сайте, а класс User – за пользователей. И если вам что-то нужно будет изменить, вы будете знать где это искать. К примеру, нужно добавить для статей возможность за них голосовать. Вы будете добавлять этот функционал в классе Article, а не где-то ещё.

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

class User
{
    private $name;

    public function __construct(string $name)
    {
        $this->name = $name;
    }

    public function getName(): string
    {
        return $this->name;
    }
}

class Article
{
    private $title;
    private $text;
    private $author;

    public function __construct(string $title, string $text, User $author)
    {
        $this->title = $title;
        $this->text = $text;
        $this->author = $author;
    }

    public function getTitle(): string
    {
        return $this->title;
    }

    public function getText(): string
    {
        return $this->text;
    }

    public function getAuthor(): User
    {
        return $this->author;
    }
}

Заметили? В конструктор мы передаём объект класса User, а не что-либо ещё. Это тайп-хинтинг – мы про него уже говорили. Он позволяет указывать в аргументах типы передаваемых переменных, однако до этого мы использовали только скалярные типы – строки, числа. Но в PHP можно указывать в качестве типов и объекты!

Давайте проверим, что всё работает. Создадим пользователя и создадим новую публикацию от его имени.

$author = new User('Иван');
$article = new Article('Заголовок', 'Текст', $author);

var_dump($article);

Все успешно отработало и мы получили следующий вывод:

object(Article)[2]
  private 'title' => string 'Заголовок' (length=18)
  private 'text' => string 'Текст' (length=10)
  private 'author' => 
    object(User)[1]
      private 'name' => string 'Иван' (length=8)

Видим объект класса Article, у которого в свойстве author лежит объект класса User.
Теперь, если мы захотим получить имя автора статьи мы сможем сделать это следующим образом:

$author = new User('Иван');
$article = new Article('Заголовок', 'Текст', $author);

echo 'Имя автора: ' . $article->getAuthor()->getName();

Результат:

Имя автора: Иван

Как это работает? Да всё очень просто. Мы с помощью метода getAuthor() получили от статьи объект с типом User, а далее сразу же у этого объекта вызвали метод getName() и получили строковое значение поля name. Вот это и есть объектно-ориентированный подход – взаимодействие между объектами.

Ещё про тайп-хинтинг

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

<?php

class User
{
    private $name;

    public function __construct(string $name)
    {
        $this->name = $name;
    }

    public function getName(): string
    {
        return $this->name;
    }
}

class Cat
{
    private $name;

    public function __construct(string $name)
    {
        $this->name = $name;
    }

    public function getName(): string
    {
        return $this->name;
    }
}

class Article
{
    private $title;
    private $text;
    private $author;

    public function __construct(string $title, string $text, User $author)
    {
        $this->title = $title;
        $this->text = $text;
        $this->author = $author;
    }

    public function getTitle(): string
    {
        return $this->title;
    }

    public function getText(): string
    {
        return $this->text;
    }

    public function getAuthor(): User
    {
        return $this->author;
    }
}

$author = new User('Иван');
$cat = new Cat('Барсик');
$article = new Article('Заголовок', 'Текст', $cat);

Это, разумеется, приведет к ошибке. Третий аргумент должен быть с типом User, а передан Cat. Ахтунг!

Ошибка при передаче объекта неправильного типа

Тайп-хинтинг позволяет избежать ошибок .

А теперь давайте вспомним оператор instanceof. Помните, что при проверке объектов дочерних классов на родительский, он возвращал true? То же актуально и для тайп-хинтинга. Давайте создадим класс Admin, который будет наследником класса User. И попробуем передать его в качестве автора статьи:

class Admin extends User
{
}

$author = new Admin('Пётр');
$article = new Article('Заголовок', 'Текст', $author);

Такой код корректно отработает, потому что Admin – это тоже User. Обратное, разумеется, неверно. Если мы в конструкторе статьи разрешим в качестве автора передавать только объекты класса Admin, то если туда передадим объект класса User, то скрипт упадёт с ошибкой, потому что не всякий User является Admin-ом. Что, согласитесь, вполне логично.

Заключение

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

Неймспейсы и автозагрузка в PHP

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

На самом деле, в PHP всё довольно просто с правилами по реализации большинства частей приложения. Для этого есть уже придуманные умными людьми стандарты – PSR (PHP Standards Recommendations). В них описано, как нужно писать ту или иную составляющую вашей программы.

В этом уроке мы затронем стандарт PSR-4. В нём говорится о том, что каждый класс должен храниться в отдельном файле и находиться в пространстве имён. Давайте обо всём по порядку.

Пусть у нас есть классы User и Article. Нам нужно сохранить их в разных файлах. Для этого давайте создадим рядом с папкой www папку src, а внутри неё папку MyProject. Внутри папки MyProject создадим папку Models, а в ней создадим ещё 2 папки – Articles и Users. И уже в этих папках создадим файлы Article.php и User.php. Должно получиться следующее:

Архитектура проекта

Давайте теперь опишем в этих двух файлах наши классы.
src/MyProject/Models/Articles/Article.php

<?php

class Article
{
    private $title;
    private $text;
    private $author;

    public function __construct(string $title, string $text, User $author)
    {
        $this->title = $title;
        $this->text = $text;
        $this->author = $author;
    }

    public function getTitle(): string
    {
        return $this->title;
    }

    public function getText(): string
    {
        return $this->text;
    }

    public function getAuthor(): User
    {
        return $this->author;
    }
}

src/MyProject/Models/Users/User.php

<?php

class User
{
    private $name;

    public function __construct(string $name)
    {
        $this->name = $name;
    }

    public function getName(): string
    {
        return $this->name;
    }
}

Первую часть сделали – теперь у нас каждый класс лежит в отдельном файле. Давайте теперь перейдём в наш index.php, лежащий в директории www и запишем в него логику для работы с этими классами.

<?php

$author = new User('Иван');
$article = new Article('Заголовок', 'Текст', $author);
var_dump($article);

Давайте теперь попробуем запустить этот скрипт в браузере.
Разумеется, мы получим ошибку.

Ошибка из-за ненайденного класса

Нашему скрипту не удалось найти класс User. Давайте подключим файлы с нужными нам классами в начале index.php

<?php

require __DIR__ . '/../src/MyProject/Models/Users/User.php';
require __DIR__ . '/../src/MyProject/Models/Articles/Article.php';

$author = new User('Иван');
$article = new Article('Заголовок', 'Текст', $author);
var_dump($article);

Если мы сейчас запустим этот скрипт, то всё у нас прекрасно отработает и мы увидим результат var_dump().

Результат успешной работы скрипта

Итак, с первым пунктом про хранение классов в отдельных файлах мы разобрались.

Пространства имён - namespaces

Теперь вернёмся к пространствам имён – неймспейсам. Тут всё довольно просто – класс можно поместить в отдельное именованное пространство и в дальнейшем использовать его по этому полному имени. Для того чтобы указать это пространство для конкретного класса используется слово namespace, за которым следует само имя. Указывается оно в файле с классом, перед определением класса. На примере класса User это будет выглядеть следующим образом:
src/MyProject/Models/Users/User.php

<?php

namespace MyProject\Models\Users;

class User
{
    private $name;

    public function __construct(string $name)
    {
        $this->name = $name;
    }

    public function getName(): string
    {
        return $this->name;
    }
}

Теперь мы можем говорить, что класс User находится в неймспейсе MyProject\Models\Users.

Давайте проделаем аналогичные действия с классом Article.
src/MyProject/Models/Articles/Article.php

<?php

namespace MyProject\Models\Articles;

class Article
{
    private $title;
    private $text;
    private $author;

    public function __construct(string $title, string $text, User $author)
    {
        $this->title = $title;
        $this->text = $text;
        $this->author = $author;
    }

    public function getTitle(): string
    {
        return $this->title;
    }

    public function getText(): string
    {
        return $this->text;
    }

    public function getAuthor(): User
    {
        return $this->author;
    }
}

Теперь, чтобы в файле index.php работать с данными классами, мы должны указать полное имя класса – это имя класса с указанием его неймспейса. Делается это следующим образом.
www/index.php

<?php

require __DIR__ . '/../src/MyProject/Models/Users/User.php';
require __DIR__ . '/../src/MyProject/Models/Articles/Article.php';

$author = new \MyProject\Models\Users\User('Иван');
$article = new \MyProject\Models\Articles\Article('Заголовок', 'Текст', $author);
var_dump($article);

Если мы сейчас запустим скрипт, то снова столкнёмся с ошибкой.

Ошибка из-за разных неймспейсов

Но на этот раз, она уже другая. А именно – третий аргумент, переданный в конструктор класса Article должен быть объектом класса MyProject\Models\Articles\User, а передан объект класса MyProject\Models\Users\User. Заметили ошибку? Неймспейс не тот. Дело в том, что если в файле с классом указан неймспейс, то все классы, которые указываются в данном файле будут искаться в том же неймспейсе. Так как у нас класс User находится в другом неймспейсе, то мы должны явно это указать. Вот так:
src/MyProject/Models/Articles/Article.php

<?php

namespace MyProject\Models\Articles;

class Article
{
    private $title;
    private $text;
    private $author;

    public function __construct(string $title, string $text, \MyProject\Models\Users\User $author)
    {
        $this->title = $title;
        $this->text = $text;
        $this->author = $author;
    }

    public function getTitle(): string
    {
        return $this->title;
    }

    public function getText(): string
    {
        return $this->text;
    }

    public function getAuthor(): \MyProject\Models\Users\User
    {
        return $this->author;
    }
}

Либо же указать в начале файла о каком классе идёт речь, когда мы используем в коде только слово User. Делается это с помощью слова use после указания текущего неймспейса, но перед описанием класса.

<?php

namespace MyProject\Models\Articles;

use MyProject\Models\Users\User;

class Article
{
    private $title;
    private $text;
    private $author;

    public function __construct(string $title, string $text, User $author)
    {
        $this->title = $title;
        $this->text = $text;
        $this->author = $author;
    }

    public function getTitle(): string
    {
        return $this->title;
    }

    public function getText(): string
    {
        return $this->text;
    }

    public function getAuthor(): User
    {
        return $this->author;
    }
}

Теперь, когда мы будем использовать класс User, то автоматически будет использоваться класс из неймспейса MyProject\Models\Users\User.

Давайте снова запустим скрипт, и убедимся, что всё у нас теперь работает.

object(MyProject\Models\Articles\Article)[2]
  private 'title' => string 'Заголовок' (length=18)
  private 'text' => string 'Текст' (length=10)
  private 'author' => 
    object(MyProject\Models\Users\User)[1]
      private 'name' => string 'Иван' (length=8)

Получили работающий скрипт. Ура! Вот в принципе и всё, что вам нужно знать о неймспейсах. В наших будущих программах мы всегда будем создавать классы внутри неймспейсов.

Автозагрузка

Однако, давайте снова посмотрим на наш файл index.php. представьте, что у нас теперь большой проект и в нём больше 100 классов. Нам придётся сто раз писать require с указанием каждого файла. Утомительно, да? Однако, можно автоматизировать этот процесс, написав функцию автозагрузки классов. Она будет вызываться каждый раз, когда впервые будет встречаться новый класс.

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

Итак, давайте сделаем эту функцию автозагрузки. Давайте я сначала приведу пример кода, а затем объясню, как это работает. Наш файл index.php принимает следующий вид:

<?php

function myAutoLoader(string $className)
{
    require_once __DIR__ . '/../src/' . $className . '.php';
}

spl_autoload_register('myAutoLoader');

$author = new \MyProject\Models\Users\User('Иван');
$article = new \MyProject\Models\Articles\Article('Заголовок', 'Текст', $author);
var_dump($article);

А теперь по порядку о том, что же происходит.

  • Функция spl_autoload_register() принимает первым аргументом имя функции, в которую будет передаваться имя класса, каждый раз, когда этот класс ещё не был загружен. Поэтому мы создаём новую функцию myAutoLoader() и указываем это имя в качестве аргумента функции spl_autoload_register().
  • Теперь по поводу функции myAutoLoader. Каждый раз, когда в коде будет встречаться класс, который ещё не был подключён, в неё первым аргументом будет передаваться полное имя класса (вместе с неймспейсом). И мы должны на основе этого полного имени подключить нужный файл. Так как у нас пути до файлов и их неймспейсы совпадают, то мы просто склеиваем строку из следующих составляющих: текущая директория + поднимаемся на уровень выше + переходим в папку src + полное имя класса + добавляем расширение .php.

Всё! Теперь все классы будут подгружаться автоматически. Давайте запустим скрипт и убедимся, что всё работает.

Давайте добавим отладочную информацию внутри функции myAutoLoader(), чтобы проверить что всё именно так и работает. Добавим var_dump() с выводом переменной $className.

function myAutoLoader(string $className)
{
    var_dump($className);
    require_once __DIR__ . '/../src/' . $className . '.php';
}

Снова запустим скрипт и посмотрим на вывод.

string 'MyProject\Models\Users\User' (length=20)
string 'MyProject\Models\Articles\Article' (length=26)
object(MyProject\Models\Articles\Article)[2]
  private 'title' => string 'Заголовок' (length=18)
  private 'text' => string 'Текст' (length=10)
  private 'author' => 
    object(MyProject\Models\Users\User)[1]
      private 'name' => string 'Иван' (length=8)

Мы видим, что в эту функцию попал сначала класс MyProject\Models\Users\User, а затем MyProject\Models\Articles\Article. И для этих классов мы сделали require нужных файлов и они успешно подгрузились.

На этом давайте var_dump уберём.

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

<?php

spl_autoload_register(function (string $className) {
    require_once __DIR__ . '/../src/' . $className . '.php';
});

$author = new \MyProject\Models\Users\User('Иван');
$article = new \MyProject\Models\Articles\Article('Заголовок', 'Текст', $author);
var_dump($article);

В таком случае, функция называется анонимной – у неё нет имени. Она просто передаётся в качестве аргумента и имя ей не нужно.

Запустите код ещё раз, и убедитесь, что всё работает как нужно.
Вот такими вот нехитрыми действиями мы сделали автозагрузку классов. PHP – прекрасный язык, не правда ли?

6 Likes

А возможно так же сюда PHP для начинающих и MySQL с нуля

1 Like

PHP вроде бесплатный, если нет, то добавим

1 Like

Спасибо тебе и всей вашей teamE, там PHP для начинающих с 12 урока обрыв денежный

Почему может не работать автогенератор путей, пишет что не может найти файл путь задаю верно все проверил несколько раз

Что это за автогенератор путей?