PHP и ООП на практике: от четырёх китов до грабель в проде
Зачем это нужно
Laravel — это библиотека на PHP, и большинство странных багов в проектах растёт не из фреймворка, а из базовых вещей: неявное приведение типов, путаница между интерфейсом и абстрактным классом, трейты, разросшиеся до полноценных сервисов. Разберём базу и покажем на примерах, где она стреляет.
Классическая ситуация: в валидации сравниваем $request->input('is_active') == 1, а в базу уходит true или строка "1" — и логика начинает вести себя непредсказуемо. Или классу через конструктор нужна зависимость, а при создании объекта её не передали — и приложение падает с ArgumentCountError.
Четыре кита ООП простыми словами
Инкапсуляция — данные и методы, которые с ними работают, живут внутри класса, а наружу торчат только «кнопки». Доступ к состоянию контролируем через модификаторы.
Наследование — класс-наследник получает свойства и методы родителя и добавляет своё. Инструмент для отношения «это разновидность того».
Полиморфизм — разные классы отвечают на один и тот же вызов каждый по-своему, а вызывающий код не знает и не хочет знать, какой именно класс перед ним.
Абстракция — выделяем важное и прячем детали: интерфейс говорит «что умеет», реализация — «как именно».
1. Инкапсуляция: public, protected, private
Модификаторы видимости — это способ сказать: «это внутренняя кухня, руками туда не лезть».
phpclass BankAccount
{
private float $balance = 0; // снаружи не изменить напрямую
public function deposit(float $amount): void
{
if ($amount <= 0) {
throw new InvalidArgumentException('Сумма должна быть положительной');
}
$this->balance += $amount;
}
public function getBalance(): float
{
return $this->balance;
}
}
$account = new BankAccount();
$account->deposit(100);
// $account->balance = -9999; // Fatal error: private property
Боль, которую это снимает: если бы $balance был public, любой код мог бы написать $account->balance = -500 — и в отчётах всплыли бы отрицательные остатки. Через метод мы контролируем каждое изменение.
2. Наследование: extends
Наследование — для отношений «это разновидность». Собака — разновидность животного, Controller в Laravel — разновидность базового контроллера.
phpclass Animal
{
public function __construct(protected string $name) {}
public function info(): string
{
return "Меня зовут {$this->name}";
}
}
class Dog extends Animal
{
public function info(): string
{
return parent::info() . ', и я говорю «гав»';
}
}
echo (new Dog('Рекс'))->info(); // Меня зовут Рекс, и я говорю «гав»
Наследник переопределяет методы, а parent:: позволяет не дублировать родительскую логику, а дополнить её.
Грабля: наследование — не способ «переиспользовать код ради галочки». Класс LogService extends DbService — красный флаг: сервис логов не «разновидность» сервиса БД, ему нужно лишь воспользоваться его методами. Для этого есть композиция — передать зависимость через конструктор.
3. Полиморфизм: один вызов — разное поведение
Полиморфизм раскрывается через интерфейс: код зависит от контракта, а не от конкретного класса.
phpinterface Notifier
{
public function send(string $message): void;
}
class EmailNotifier implements Notifier
{
public function send(string $message): void
{
// отправка письма
}
}
class SmsNotifier implements Notifier
{
public function send(string $message): void
{
// отправка SMS
}
}
function notifyUser(Notifier $notifier, string $message): void
{
$notifier->send($message); // какой класс — неважно, контракт один
}
notifyUser(new EmailNotifier(), 'Привет!');
notifyUser(new SmsNotifier(), 'Привет!');
notifyUser ничего не знает о письмах и SMS — он работает с Notifier. Завтра добавим TelegramNotifier, и эта функция не изменится ни на символ. Именно на этом построены Laravel-контракт Authenticatable, Queueable-джобы и подмена реализаций через контейнер.
Дальше по базе
4. Классы и объекты
Класс — чертёж, объект — дом, построенный по чертежу.
phpclass User
{
public string $name;
public int $age;
public function introduce(): string
{
return "Привет, я {$this->name}, мне {$this->age} лет.";
}
}
$user1 = new User();
$user1->name = 'Анна';
$user1->age = 25;
echo $user1->introduce(); // Привет, я Анна, мне 25 лет.
В Laravel почти всё — сервисы, контроллеры, модели — классы. Объекты либо создаются вручную, либо собирает сервис-контейнер.
5. == vs === — самая частая ловушка
== сравнивает значение и приводит типы;=== сравнивает значение и тип.
php$a = 1;
$b = '1';
var_dump($a == $b); // true
var_dump($a === $b); // false
== сравнивает значение и приводит типы;=== сравнивает значение и тип.$a = 1;
$b = '1';
var_dump($a == $b); // true
var_dump($a === $b); // false
Боль, которую это приносит: в проверках прав и статусах строка "0" при нестрогом сравнении превращается в false — и доступ открывается там, где не должен. В Laravel это всплывает в middleware, проверках ролей и флагах из БД, где tinyint 0/1 соседствует со строками.
6. Интерфейс vs абстрактный класс (это про абстракцию)
- Интерфейс — обещание: «я умею делать вот эти методы». Без реализации.
- Абстрактный класс — полуфабрикат: может иметь реализацию, но сам по себе не создаётся.
phpinterface PaymentProcessor
{
public function pay(float $amount): bool;
}
abstract class BasePayment
{
protected string $currency = 'RUB';
protected function logTransaction(float $amount): void
{
// общее логирование для всех платёжек
}
}
class SberPayment extends BasePayment implements PaymentProcessor
{
public function pay(float $amount): bool
{
$this->logTransaction($amount);
// реальная оплата
return true;
}
}
interface PaymentProcessor
{
public function pay(float $amount): bool;
}
abstract class BasePayment
{
protected string $currency = 'RUB';
protected function logTransaction(float $amount): void
{
// общее логирование для всех платёжек
}
}
class SberPayment extends BasePayment implements PaymentProcessor
{
public function pay(float $amount): bool
{
$this->logTransaction($amount);
// реальная оплата
return true;
}
}
Практический смысл: когда проект завязан на интерфейс, а не на конкретный класс, добавить нового платёжного провайдера — значит написать новый класс и поменять одну строку привязки в контейнере. Без интерфейса пришлось бы переписывать всю логику заказов. В Laravel привязка выглядит так:
php$this->app->bind(PaymentProcessor::class, SberPayment::class);
Абстрактный класс уместен там, где есть общая логика для группы классов — базовый контроллер, общий каркас сервиса.
7. Трейты — «копипаста без копипасты»
Трейт — набор методов, который «подмешивается» в разные классы. Случай, когда логика нужна в нескольких местах, но наследование ни при чём.
phptrait HasSlug
{
public function generateSlug(): string
{
return str_slug($this->name); // упрощённо
}
}
class Post
{
use HasSlug;
public string $name = 'Мой пост';
}
class Product
{
use HasSlug;
public string $name = 'Кроссовки';
}
В Laravel SoftDeletes, HasFactory, HasMedia в моделях — именно трейты.
8. Позднее статическое связывание: static:: vs self::
Вопрос в том, какой класс реально используется в цепочке наследования:
self::— всегда класс, где метод объявлен;static::— класс, который реально вызвал метод.
phpclass Base
{
public static function className(): string
{
return self::class; // всегда Base
}
public static function realClassName(): string
{
return static::class; // тот, кто вызвал
}
}
class Child extends Base {}
var_dump(Child::className()); // "Base"
var_dump(Child::realClassName()); // "Child"
На этом построены фабрики и scopes в Laravel: методы должны работать для конкретной модели, а не для базового класса.
Плохо / Хорошо
| Плохо | Хорошо |
|---|---|
public свойства, которые меняют откуда угодно | private/protected + геттеры и методы-мутаторы |
| Наследование «ради переиспользования кода» | Наследование для «это разновидность», иначе — композиция |
if ($status == 1) — пропустит строку "1" или true | if ($status === 1) — строго по типу |
| Вся логика в контроллере, включая запросы к БД | Контроллер принимает запрос и делегирует сервису |
| Интерфейсы и абстрактные классы «на всякий случай» | Интерфейс — контракт, абстрактный класс — общая реализация |
Подводный камень
- Неявное приведение типов.
0 == '' — true, false == [] — true. В проверках — только ===. public по умолчанию. Свойство, открытое для записи извне, ломает инварианты класса — доступ давайте через методы.- Глубокие цепочки наследования. Три-четыре уровня
extends — почти всегда признак того, что логику пора выносить в сервисы и композицию. - Трейты как костыль. Бизнес-логика живёт в сервисе, а не в трейте. Трейт — для мелких утилит: slug, timestamps, логирование.
- Статика и DI. Статические методы не дружат с внедрением зависимостей — зависимости передавайте через конструктор.
0 == '' — true, false == [] — true. В проверках — только ===.public по умолчанию. Свойство, открытое для записи извне, ломает инварианты класса — доступ давайте через методы.extends — почти всегда признак того, что логику пора выносить в сервисы и композицию.Мини-шпаргалка
php// Инкапсуляция
private float $balance = 0;
public function getBalance(): float { return $this->balance; }
// Наследование + переопределение
class Dog extends Animal {
public function info(): string { return parent::info() . ' + своё'; }
}
// Полиморфизм
interface Notifier { public function send(string $m): void; }
function notifyUser(Notifier $n, string $m): void { $n->send($m); }
// Трейт
class Post { use HasTimestamp; }
// static vs self
static::class; // реальный класс
self::class; // класс, где метод объявлен
// Привязка интерфейса в Laravel
$this->app->bind(PaymentInterface::class, SberPayment::class);
// Инкапсуляция
private float $balance = 0;
public function getBalance(): float { return $this->balance; }
// Наследование + переопределение
class Dog extends Animal {
public function info(): string { return parent::info() . ' + своё'; }
}
// Полиморфизм
interface Notifier { public function send(string $m): void; }
function notifyUser(Notifier $n, string $m): void { $n->send($m); }
// Трейт
class Post { use HasTimestamp; }
// static vs self
static::class; // реальный класс
self::class; // класс, где метод объявлен
// Привязка интерфейса в Laravel
$this->app->bind(PaymentInterface::class, SberPayment::class);