Статика: удобный способ сломать ООП

Сцепить. Скрыть. Заморозить.

Статика сцепляет код с конкретным классом: вместо зависимости по контракту — жёсткий вызов Settings::get(), который нельзя подменить. Скрывает зависимости: глядя на конструктор, не видно, что сервис ещё и от статики зависит. Заморозить — поведение нельзя изменить в рантайме: у статического вызова нет объекта, которому можно передать другую реализацию, поэтому код со статикой внутри не мокается и не тестируется.


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

Статические методы и свойства — это методы и данные, которые принадлежат классу, а не объекту. Один экземпляр на всё приложение, доступ без new: Config::get('app.name'), Cache::get('key'). Звучит удобно — и в момент написания так и есть.

Боль из практики: вы пишете сервис, который внутри обращается к Settings::get('delivery_price'). Работает. Через полгода в тестах поведение начинает «плыть»: один тест поменял настройку, второй её увидел — потому что статическое состояние живёт между вызовами и его никто не сбрасывает. Или процесс задеплоен с очередями в несколько воркеров — и два воркера одновременно пишут в один статический счётчик. Всё это следы одной причины: статика делает состояние глобальным.

Суть темы в трёх словах — статика сцепляет код, скрывает зависимости и замораживает поведение. Разберём каждое.

Как это работает

Статика сцепляет

Обычный объект создаётся, живёт, умирает — его состояние локально. Статическое свойство — одно на всю программу:

php
class Cart
{
    public static array $items = [];
}

Cart::$items[] = 'Товар';

Кто ещё пользуется этой корзиной? Все. Какой пользователь? Никакой — корзина одна на весь запрос, а в фоне и на все запросы сразу. Статика превращает локальную проблему в глобальную: изменили её в одном месте — изменили везде.

Статика скрывает зависимости

Это самая коварная часть. Посмотрите на конструктор:

php
class OrderService
{
    public function __construct(
        private Mailer $mailer
    ) {}

    public function place(Order $order): void
    {
        // зависимости почты — на виду, в конструкторе
        $this->mailer->send($order);

        // а вот эта зависимость спрятана внутри
        $recipient = UserStorage::getCurrent(); // ← сюрприз!
    }
}

Глядя на конструктор OrderService, вы не видите, что сервис зависит ещё и от UserStorage. Статический вызов — зависимость, которую нельзя найти в сигнатуре. А что нельзя увидеть — нельзя подменить, задокументировать и проверить.

Сравните с честной версией:

php
public function __construct(
    private Mailer $mailer,
    private UserStorage $users // зависимость видна — и управляема
) {}

Статика замораживает поведение

Полиморфизм работает через объект: интерфейсу можно передать любую реализацию. Статическому вызову передать нельзя:

php
// Мокаем легко
$this->mock(Notifier::class)->...;

// А как замокать это?
Notifier::send($message);

У статического вызова нет «объекта, который можно подменить». Тест заходит в тупик: либо запускается настоящая отправка, либо приходится строить обходные конструкции. Итог: код со статикой внутри тестируется плохо, а нетестируемый код со временем перестают трогать — и он гниёт.

Где статика всё-таки уместна

Не рубить с плеча: у статики есть законные места.

php
// Чистые функции без состояния — можно
Str::slug('Привет мир');        // вход → выход, ничего не запоминает
Carbon::parse('2024-01-01');

// Фабрики и константы — можно
Money::of(100);                 // создаёт объект
PDO::FETCH_ASSOC;               // константа класса

Критерий один: нет состояния — нет проблемы. Если статический метод что-то запоминает между вызовами (static $cache = [], static $instance) — вы уже на пути к глобальному состоянию.

Нюанс Laravel: фасады (Cache::get()) выглядят статически, но работают иначе — они проксируют вызов к объекту, который берут из сервис-контейнера. Подменить его в тесте можно (Cache::shouldReceive()), зависимости настоящие и управляемые. Это не нарушение принципа, а его обход через контейнер. Поэтому фасады — исключение из правила, а не опровержение.

Плохо / Хорошо

ПлохоХорошо
Сервис вызывает Settings::get() внутри методовНастройки приходят через конструктор
Статическое свойство-кэш «на всякий случай»Кэш — инжектируемый сервис с явным контрактом
Счётчик или флаг в static свойствеСостояние — в объекте или в хранилище (БД, Redis)
Helper::formatDate() с состоянием внутриЧистая функция: вход → выход, ничего не помнит

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

  1. Синглтоны через статику. Settings::getInstance() — это глобальная переменная с хорошим макияжем. В Laravel синглтоны делают через контейнер: $this->app->singleton(...) — подменяемо и тестируемо.
  2. Статика в тестах не сбрасывается. Один тест поменял статическое свойство — следующий тест унаследовал его состояние. Отсюда «флакающие» тесты, которые проходят по одному и падают вместе.
  3. static::$instance в многопоточности. В long-running процессах (Swoole, Octane, воркеры очередей) статическое состояние живёт между запросами — и превращается в источник гонок и утечек.
  4. Фасад — не индульгенция. Если внутри вашей обёртки над фасадом копится своё статическое состояние — фасад чистый, а ваш код уже нет.

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

php
// Плохо: скрытая зависимость + глобальное состояние
class Settings
{
    public static array $values = [];
}
Settings::$values['delivery_price'] = 100;

// Хорошо: зависимость через конструктор, состояние — в сервисе
class SettingsService
{
    public function __construct(private array $values) {}

    public function get(string $key): mixed
    {
        return $this->values[$key] ?? null;
    }
}

// Статике можно — когда нет состояния
Str::slug('Привет мир');   // чистая функция
Money::of(100);            // фабрика

// Синглтон в Laravel — через контейнер, а не через статику
$this->app->singleton(SettingsService::class, fn () => new SettingsService(config('settings')));

Три вывода

  1. Статика — это глобальное состояние и скрытые зависимости: сцепляет код там, где нужна слабая связность.
  2. Нет состояния — статике можно; есть состояние — выносите в объект или инжектируемый сервис.
  3. Синглтоны и подменяемые зависимости в Laravel решаются контейнером (singleton, фасады), а не статическими свойствами.