PHP и ООП на практике: от четырёх китов до грабель в проде

Зачем это нужно

Laravel — это библиотека на PHP, и большинство странных багов в проектах растёт не из фреймворка, а из базовых вещей: неявное приведение типов, путаница между интерфейсом и абстрактным классом, трейты, разросшиеся до полноценных сервисов. Разберём базу и покажем на примерах, где она стреляет.

Классическая ситуация: в валидации сравниваем $request->input('is_active') == 1, а в базу уходит true или строка "1" — и логика начинает вести себя непредсказуемо. Или классу через конструктор нужна зависимость, а при создании объекта её не передали — и приложение падает с ArgumentCountError.

Четыре кита ООП простыми словами

Инкапсуляция — данные и методы, которые с ними работают, живут внутри класса, а наружу торчат только «кнопки». Доступ к состоянию контролируем через модификаторы.

Наследование — класс-наследник получает свойства и методы родителя и добавляет своё. Инструмент для отношения «это разновидность того».

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

Абстракция — выделяем важное и прячем детали: интерфейс говорит «что умеет», реализация — «как именно».

1. Инкапсуляция: public, protected, private

Модификаторы видимости — это способ сказать: «это внутренняя кухня, руками туда не лезть».

php
class 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 — разновидность базового контроллера.

php
class 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. Полиморфизм: один вызов — разное поведение

Полиморфизм раскрывается через интерфейс: код зависит от контракта, а не от конкретного класса.

php
interface 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. Классы и объекты

Класс — чертёж, объект — дом, построенный по чертежу.

php
class 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

Боль, которую это приносит: в проверках прав и статусах строка "0" при нестрогом сравнении превращается в false — и доступ открывается там, где не должен. В Laravel это всплывает в middleware, проверках ролей и флагах из БД, где tinyint 0/1 соседствует со строками.

6. Интерфейс vs абстрактный класс (это про абстракцию)

  • Интерфейс — обещание: «я умею делать вот эти методы». Без реализации.
  • Абстрактный класс — полуфабрикат: может иметь реализацию, но сам по себе не создаётся.
php
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. Трейты — «копипаста без копипасты»

Трейт — набор методов, который «подмешивается» в разные классы. Случай, когда логика нужна в нескольких местах, но наследование ни при чём.

php
trait 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:: — класс, который реально вызвал метод.
php
class 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" или trueif ($status === 1) — строго по типу
Вся логика в контроллере, включая запросы к БДКонтроллер принимает запрос и делегирует сервису
Интерфейсы и абстрактные классы «на всякий случай»Интерфейс — контракт, абстрактный класс — общая реализация

Подводный камень

  1. Неявное приведение типов. 0 == '' — true, false == [] — true. В проверках — только ===.
  2. public по умолчанию. Свойство, открытое для записи извне, ломает инварианты класса — доступ давайте через методы.
  3. Глубокие цепочки наследования. Три-четыре уровня extends — почти всегда признак того, что логику пора выносить в сервисы и композицию.
  4. Трейты как костыль. Бизнес-логика живёт в сервисе, а не в трейте. Трейт — для мелких утилит: slug, timestamps, логирование.
  5. Статика и DI. Статические методы не дружат с внедрением зависимостей — зависимости передавайте через конструктор.

Мини-шпаргалка

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);

Три вывода

  1. Инкапсуляция — не «модификаторы ради модификаторов», а контроль каждого изменения состояния класса.
  2. Наследование — только для «это разновидность»; в остальных случаях дешевле композиция и интерфейсы.
  3. Полиморфизм через интерфейс освобождает код от знания о конкретных классах — именно поэтому Laravel так легко расширяется.