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

До этого мы хранили данные в массивах: $user = ['name' => 'Иван', 'email' => 'i@x.ru']. Это удобно для двух полей, но когда у сущности 10 полей, валидация, связи и поведение - массив превращается в свалку. Классы решают эту проблему: данные + операции над ними в одном месте, с гарантиями.

Минимальный класс

<?php
final class User
{
    public string $name;
    public string $email;
}

$u = new User();
$u->name = 'Иван';
$u->email = 'i@x.ru';

echo $u->name; // Иван

new User() создаёт объект - экземпляр класса. Полей у User нет волшебных: ты их объявляешь явно, типизированно. Если попробуешь $u->name = 42, PHP бросит TypeError.

Конструктор и promoted properties

Заполнять поля «снаружи» неудобно - легко забыть. Конструктор гарантирует, что объект создаётся в валидном состоянии:

<?php
final class User
{
    public string $name;
    public string $email;

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

$u = new User('Иван', 'i@x.ru');

С PHP 8.0 это пишется в одну строку - constructor property promotion:

<?php
final class User
{
    public function __construct(
        public string $name,
        public string $email,
    ) {}
}

$u = new User('Иван', 'i@x.ru');

Каждый public string $name в сигнатуре конструктора автоматически становится полем и присваивается. На больших классах экономит десятки строк.

Если в конструкторе есть валидация (`if (!filter_var($email, FILTER_VALIDATE_EMAIL))`) или вычисляемые поля - promoted properties можно совмещать с телом конструктора. Promoted поля присваиваются ДО того, как выполнится тело.

Видимость: public, protected, private

<?php
final class BankAccount
{
    public string $owner;       // доступно везде
    protected int $balance = 0; // доступно в классе и наследниках
    private string $secret;     // только внутри этого класса

    public function __construct(string $owner, string $secret)
    {
        $this->owner = $owner;
        $this->secret = $secret;
    }

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

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

$acc = new BankAccount('Иван', 'pin1234');
$acc->deposit(100);
echo $acc->getBalance(); // 100

// $acc->balance = -999;  // Error: protected
// echo $acc->secret;     // Error: private

Правило: поля делай private по умолчанию, наружу выноси только публичные методы (deposit, getBalance). Это инкапсуляция (encapsulation) - детали скрыты, контракт явный. Если завтра ты захочешь логировать каждое изменение баланса, ты добавишь это в deposit(), а не правишь все места, где кто-то делал $acc->balance += ....

Классическая тройка принципов ООП: **инкапсуляция** (ты только что её применил), **наследование** и **полиморфизм** - обе разобраны в [следующем уроке](./14-oop-inheritance.md). Часто называют и четвёртый - **абстракцию** (выделить существенное в контракт, спрятать детали). Держи эту карту в голове: на собеседованиях по ней ходят постоянно.

Геттеры/сеттеры vs publi readonly

В PHP 8.1+ есть readonly - поле, которое можно присвоить только в конструкторе:

<?php
final class User
{
    public function __construct(
        public readonly string $id,
        public readonly string $email,
    ) {}
}

$u = new User('u-1', 'i@x.ru');
echo $u->email;
// $u->email = 'hack@x';  // Error: Cannot modify readonly property

Для immutable-объектов (DTO, value-objects) readonly лучше геттеров: меньше шума, тот же контракт.

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

Принадлежат классу, а не экземпляру:

<?php
final class Counter
{
    private static int $count = 0;

    public static function increment(): int
    {
        return ++self::$count;
    }

    public static function reset(): void
    {
        self::$count = 0;
    }
}

echo Counter::increment(); // 1
echo Counter::increment(); // 2
Counter::reset();
echo Counter::increment(); // 1

Внутри класса - self::$count (или static:: для late static binding, см. урок 15). Снаружи - Counter::increment().

`static` тесно связана со скрытыми зависимостями: код, который трогает `Counter::$count`, нельзя нормально протестировать (тесты влияют друг на друга через общее состояние). Используй статику для констант и фабрик, но не для бизнес-данных.

Фабричный метод

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

<?php
final class Money
{
    private function __construct(
        public readonly int $amount,
        public readonly string $currency,
    ) {}

    public static function fromRubles(int $amount): self
    {
        return new self($amount, 'RUB');
    }

    public static function fromDollars(int $amount): self
    {
        return new self($amount, 'USD');
    }

    public static function fromString(string $value): self
    {
        // "100 RUB" → Money
        [$amount, $currency] = explode(' ', $value);
        return new self((int) $amount, $currency);
    }
}

$rub = Money::fromRubles(1000);
$usd = Money::fromDollars(50);
$parsed = Money::fromString('200 EUR');

private __construct() запрещает new Money(...) снаружи - клиент обязан пройти через фабрику. Это полезно, когда «голый» конструктор не имеет смысла без контекста.

Сравнение объектов

<?php
$a = new User('Иван', 'i@x.ru');
$b = new User('Иван', 'i@x.ru');

var_dump($a == $b);  // true - все поля равны
var_dump($a === $b); // false - это разные объекты
var_dump($a === $a); // true - один и тот же объект

== сравнивает поля, === - идентичность ссылки. Для value-объектов часто пишут метод equals(self $other): bool, чтобы не зависеть от сравнения по ссылке.

$this и контекст

Внутри метода $this - текущий объект. Без явного $this-> PHP не найдёт поле:

<?php
final class Counter
{
    private int $value = 0;

    public function increment(): void
    {
        // ОШИБКА: $value не существует в локальной области
        // $value++;

        // Правильно:
        $this->value++;
    }
}

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

Как это в Symfony

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

<?php
// упрощённый аналог Symfony\Component\Security\Core\User\InMemoryUser
final class InMemoryUser implements UserInterface
{
    public function __construct(
        private readonly string $username,
        private readonly ?string $password,
        private readonly array $roles = ['ROLE_USER'],
    ) {}

    public function getUsername(): string { return $this->username; }
    public function getRoles(): array { return $this->roles; }
}

Тот же readonly, promoted properties, private поля + публичные геттеры. Если ты освоил эту главу - ты уже читаешь Symfony-код, просто там этих классов 5000.

Типичные ошибки

  1. Public-поля у бизнес-сущностей. class Order { public int $status; } → завтра кто-то пишет $order->status = -1 и ломает всё. Делай private + метод cancel()/pay().
  2. Конструктор с побочными эффектами. __construct не должен ходить в БД, в файлы, в сеть. Объект должен создаваться предсказуемо и быстро. Внешние данные передавай извне.
  3. Объект как контейнер геттеров и сеттеров. Если у класса только getX/setX - это анемичная модель, и она ничем не лучше массива. Класс полезен, когда у него есть поведение (deposit, cancel, addItem), а не только данные.

Best practices

  • По умолчанию делай поля private или readonly - открывай доступ только через методы с поведением (deposit, cancel), не через геттеры/сеттеры к данным.
  • Помечай классы final если не проектируешь иерархию - это защищает инварианты и упрощает рефакторинг.
  • Конструктор только для инициализации: никаких обращений к БД, сети, файлам из __construct. Объект должен создаваться предсказуемо и быстро. __construct - лишь первый из семнадцати магических методов PHP; остальные (перегрузка свойств, __clone, сериализация) разобраны в уроке про магические методы.
  • Используй constructor property promotion для DTO и value objects (PHP 8.0+): меньше шума, тот же контракт.
  • Статику ограничивай константами и фабриками - не храни в статических свойствах бизнес-данные, это скрытое глобальное состояние.

Мини-задание

  • Напиши class Email { public function __construct(public readonly string $value) } с валидацией в конструкторе (бросать InvalidArgumentException, если значение не email)
  • Класс class Cart с приватным массивом $items, методами addItem(string $name, int $qty), total(): int, count(): int
  • Класс class Logger со статическим методом info(string $msg): void, который кладёт сообщения в static $messages = [], и static all(): array
  • Фабричный метод Money::zero(string $currency): self

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