Классы и объекты
До этого мы хранили данные в массивах: $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 в сигнатуре конструктора автоматически становится полем и присваивается. На больших классах экономит десятки строк.
Видимость: 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 += ....
Геттеры/сеттеры 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().
Фабричный метод
Часто конструктор перегружают альтернативными способами создания. В 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.
Типичные ошибки
- Public-поля у бизнес-сущностей.
class Order { public int $status; }→ завтра кто-то пишет$order->status = -1и ломает всё. Делайprivate+ методcancel()/pay(). - Конструктор с побочными эффектами.
__constructне должен ходить в БД, в файлы, в сеть. Объект должен создаваться предсказуемо и быстро. Внешние данные передавай извне. - Объект как контейнер геттеров и сеттеров. Если у класса только
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