Глиф Учебники

Урок 10. ООП: классы и объекты

Курс «PHP» · урок 10 из 10 · 4 минуты чтения

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

Класс и объект

PHP
<?php
class Product
{
    public string $name;
    public int $price;

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

    public function label(): string
    {
        return "{$this->name} — {$this->price} руб.";
    }
}

$bread = new Product('Хлеб', 2);
echo $bread->label() . "\n";
$bread->price = 3;
echo $bread->label() . "\n";
Вывод
Хлеб — 2 руб.
Хлеб — 3 руб.
$bread = new Product('Хлеб', 2); class Product чертёж свойства public string $name; public int $price; методы __construct label() new внутри label() $this — это $bread $bread объект name 'Хлеб' price 2 $bread->label() → «Хлеб — 2 руб.»
Класс Product — чертёж со свойствами и методами; new Product('Хлеб', 2) создаёт по нему объект $bread, а при вызове $bread->label() внутри метода $this — это $bread.

Имена классов по обычаю пишут с заглавной буквы: Product, UserRepository.

Короткий конструктор, private и readonly

С PHP 8.0 свойства можно объявить прямо в параметрах конструктора, а с PHP 8.1 — сделать неизменяемыми (readonly):

PHP
<?php
class BankAccount
{
    private int $balance = 0;

    public function __construct(public readonly string $owner)
    {
    }

    public function deposit(int $amount): void
    {
        if ($amount <= 0) {
            throw new InvalidArgumentException('Сумма должна быть больше нуля');
        }
        $this->balance += $amount;
    }

    public function getBalance(): int
    {
        return $this->balance;
    }
}

$account = new BankAccount('Анна');
$account->deposit(100);
$account->deposit(50);
echo $account->owner . ': ' . $account->getBalance() . "\n";

try {
    $account->deposit(-5);
} catch (InvalidArgumentException $e) {
    echo 'Ошибка: ' . $e->getMessage() . "\n";
}
Вывод
Анна: 150
Ошибка: Сумма должна быть больше нуля

Здесь три важные идеи.

Видимость. Свойство $balance объявлено private: снаружи класса его не прочитать и не изменить, запись $account->balance = 1000000 остановит программу ошибкой Cannot access private property. Менять баланс можно только методом deposit, а он проверяет сумму. Так класс сам следит, чтобы его данные оставались правильными.

СловоКому доступно
publicвсем
protectedсамому классу и его наследникам
privateтолько самому классу
public — доступно всем deposit() getBalance() $owner protected — классу и наследникам private — только самому классу $balance меняет только метод deposit с проверкой суммы Код снаружи класса $account->deposit(100); метод public — вызвать можно $account->getBalance() баланс читают через метод $account->balance = 1000000 ошибка Cannot access private property
public открыт всему коду, protected — классу и наследникам, private — только самому классу: снаружи можно вызвать deposit() и getBalance(), а запись в $balance даёт ошибку.

readonly. Свойство $owner задаётся один раз в конструкторе. Попытка изменить его позже даст ошибку Cannot modify readonly property.

Исключения. throw сообщает об ошибке и прерывает метод. Код, который может её вызвать, заключают в try, а в catch решают, что делать. Если исключение никто не поймал, программа остановится с сообщением Uncaught InvalidArgumentException. В PHP много готовых классов исключений: InvalidArgumentException, RuntimeException, PDOException и другие.

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

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

PHP
<?php
class Animal
{
    public function __construct(protected string $name)
    {
    }

    public function speak(): string
    {
        return "{$this->name} издаёт звук";
    }
}

class Dog extends Animal
{
    public function speak(): string
    {
        return "{$this->name} лает";
    }
}

$animals = [new Animal('Ёжик'), new Dog('Шарик')];
foreach ($animals as $animal) {
    echo $animal->speak() . "\n";
}
Вывод
Ёжик издаёт звук
Шарик лает

Dog получил от Animal конструктор и свойство $name (оно protected, поэтому наследнику доступно) и заменил метод speak своим. Вызвать метод родителя из наследника можно через parent::speak().

class Animal __construct(protected string $name) speak() → «… издаёт звук» extends class Dog от Animal: $name, __construct speak() → «… лает» свой $animal->speak() new Animal('Ёжик') Ёжик издаёт звук конструктор и $name у Dog — от Animal, а метод speak() — свой new Dog('Шарик') Шарик лает
Dog расширяет Animal: конструктор и свойство $name он получает от родителя, а метод speak() заменяет своим, поэтому Ёжик «издаёт звук», а Шарик «лает».

Интерфейсы

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

PHP
<?php
interface Notifier
{
    public function send(string $to, string $text): void;
}

class EmailNotifier implements Notifier
{
    public function send(string $to, string $text): void
    {
        echo "Письмо для $to: $text\n";
    }
}

function remind(Notifier $notifier, string $to): void
{
    $notifier->send($to, 'Завтра собрание в 10:00');
}

remind(new EmailNotifier(), 'anna@example.com');
Вывод
Письмо для anna@example.com: Завтра собрание в 10:00

Завтра можно добавить класс SmsNotifier implements Notifier, и функция remind будет работать с ним без единой правки.

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

static принадлежит классу целиком, а не отдельному объекту. Обращаются к нему через :::

PHP
<?php
class Temperature
{
    public static function toFahrenheit(float $celsius): float
    {
        return $celsius * 9 / 5 + 32;
    }
}

echo Temperature::toFahrenheit(25) . "\n";
Вывод
77

Внутри класса к своим статическим членам обращаются через self::, к константам класса — так же: self::MAX_SIZE.

Перечисления: enum

С PHP 8.1 для набора фиксированных значений есть enum:

PHP
<?php
enum Status: string
{
    case Draft = 'draft';
    case Published = 'published';
}

$status = Status::from('published');
echo $status->name . "\n";
var_dump($status === Status::Published);
Вывод
Published
bool(true)

Status::from превращает строку из базы в значение перечисления, ->name и ->value дают имя и значение. Опечатка вроде Status::Publised остановит программу ошибкой, как только PHP дойдёт до этой строки, а с простыми строками 'publised' такая опечатка жила бы незамеченной.

Классы по файлам

Обычно каждый класс лежит в своём файле с тем же именем: класс Product — в Product.php. Чтобы не писать require для каждого, используют автозагрузку (PHP сам подключает файл класса, когда класс понадобился) и пространства имён (namespace — приставки к именам классов, чтобы классы разных библиотек не путались). Их удобнее всего настроить через Composer (справка Composer).

Частые ошибки

Коротко

Проверьте себя

1. Что означает $this внутри метода?
2. Свойство объявлено private. Где его можно прочитать?
3. Что выведет программа?
PHP
<?php
class Counter
{
    private int $count = 0;

    public function add(): static
    {
        $this->count++;
        return $this;
    }

    public function get(): int
    {
        return $this->count;
    }
}

echo (new Counter())->add()->add()->get();
4. Когда вызывается метод __construct?