Статика: удобный способ сломать ООП
Сцепить. Скрыть. Заморозить.
Статика сцепляет код с конкретным классом: вместо зависимости по контракту — жёсткий вызов Settings::get(), который нельзя подменить. Скрывает зависимости: глядя на конструктор, не видно, что сервис ещё и от статики зависит. Заморозить — поведение нельзя изменить в рантайме: у статического вызова нет объекта, которому можно передать другую реализацию, поэтому код со статикой внутри не мокается и не тестируется.
Зачем это нужно
Статические методы и свойства — это методы и данные, которые принадлежат классу, а не объекту. Один экземпляр на всё приложение, доступ без new: Config::get('app.name'), Cache::get('key'). Звучит удобно — и в момент написания так и есть.
Боль из практики: вы пишете сервис, который внутри обращается к Settings::get('delivery_price'). Работает. Через полгода в тестах поведение начинает «плыть»: один тест поменял настройку, второй её увидел — потому что статическое состояние живёт между вызовами и его никто не сбрасывает. Или процесс задеплоен с очередями в несколько воркеров — и два воркера одновременно пишут в один статический счётчик. Всё это следы одной причины: статика делает состояние глобальным.
Суть темы в трёх словах — статика сцепляет код, скрывает зависимости и замораживает поведение. Разберём каждое.
Как это работает
Статика сцепляет
Обычный объект создаётся, живёт, умирает — его состояние локально. Статическое свойство — одно на всю программу:
phpclass Cart
{
public static array $items = [];
}
Cart::$items[] = 'Товар';
Кто ещё пользуется этой корзиной? Все. Какой пользователь? Никакой — корзина одна на весь запрос, а в фоне и на все запросы сразу. Статика превращает локальную проблему в глобальную: изменили её в одном месте — изменили везде.
Статика скрывает зависимости
Это самая коварная часть. Посмотрите на конструктор:
phpclass OrderService
{
public function __construct(
private Mailer $mailer
) {}
public function place(Order $order): void
{
// зависимости почты — на виду, в конструкторе
$this->mailer->send($order);
// а вот эта зависимость спрятана внутри
$recipient = UserStorage::getCurrent(); // ← сюрприз!
}
}
Глядя на конструктор OrderService, вы не видите, что сервис зависит ещё и от UserStorage. Статический вызов — зависимость, которую нельзя найти в сигнатуре. А что нельзя увидеть — нельзя подменить, задокументировать и проверить.
Сравните с честной версией:
phppublic 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() с состоянием внутри | Чистая функция: вход → выход, ничего не помнит |
Подводный камень
- Синглтоны через статику.
Settings::getInstance()— это глобальная переменная с хорошим макияжем. В Laravel синглтоны делают через контейнер:$this->app->singleton(...)— подменяемо и тестируемо. - Статика в тестах не сбрасывается. Один тест поменял статическое свойство — следующий тест унаследовал его состояние. Отсюда «флакающие» тесты, которые проходят по одному и падают вместе.
static::$instanceв многопоточности. В long-running процессах (Swoole, Octane, воркеры очередей) статическое состояние живёт между запросами — и превращается в источник гонок и утечек.- Фасад — не индульгенция. Если внутри вашей обёртки над фасадом копится своё статическое состояние — фасад чистый, а ваш код уже нет.
Мини-шпаргалка
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')));
Три вывода
- Статика — это глобальное состояние и скрытые зависимости: сцепляет код там, где нужна слабая связность.
- Нет состояния — статике можно; есть состояние — выносите в объект или инжектируемый сервис.
- Синглтоны и подменяемые зависимости в Laravel решаются контейнером (
singleton, фасады), а не статическими свойствами.