Статические переменные в PHP: использование static внутри функций
Введение
Ключевое слово static в теле функции или метода позволяет переменной сохранять значение между вызовами. В отличие от обычной локальной переменной, которая уничтожается при выходе из функции, статическая продолжает существовать и доступна при следующем обращении.
function counter() {
static $count = 0;
return ++$count;
}
echo counter(); // 1
echo counter(); // 2При первом вызове $count инициализируется нулём. При последующих вызовах объявление static $count = 0 не выполняется повторно, а переменная сохраняет ранее присвоенное значение.
Согласно статистике Exakat, статические переменные встречаются в 30% PHP-проектов. Это объясняется удобством: разработчик получает кэш или ленивую инициализацию без создания дополнительного класса или обращения к глобальной области видимости.
Статические переменные внутри функций не следует путать со статическими свойствами классов — это разные механизмы с разными правилами работы. Официальная документация PHP разделяет эти контексты: область видимости переменных для статики в функциях и ключевое слово static в ООП для свойств и методов классов.
Если вы хотите глубже разобраться в различных контекстах использования ключевого слова static в PHP, рекомендую статью «Разница между self::, static:: и parent::», где подробно разбирается позднее статическое связывание и выбор между self:: и static:: при работе с классами.
С выходом PHP 8.1 поведение статических переменных в унаследованных методах изменилось: теперь они разделяются между родительским и дочерним методом, аналогично статическим свойствам. С PHP 8.3 инициализация статической переменной поддерживает произвольные выражения, включая вызовы функций, тогда как ранее допускались только константные значения.
Примеры использования статических переменных
Рассмотрим пять типовых сценариев применения статических переменных. Для каждого примера приведён код и анализ его уместности с учётом описанных ранее ограничений.
1. Мемоизация
Классический пример — кэширование результата ресурсоёмкой операции, чтобы не выполнять её повторно при каждом вызове функции.
function getExpensiveData(): array {
static $cache = null;
if ($cache === null) {
$cache = fetchDataFromExternalApi();
}
return $cache;
}Первый вызов выполняет запрос к API и сохраняет результат. Последующие вызовы возвращают кэшированное значение. Это допустимый подход для CLI-скриптов и одноразовых утилит. В веб-приложении с модульными тестами лучше использовать явный кэш-сервис, который можно подменить в тестовом окружении.
2. Ленивая инициализация сервиса
Функция создаёт необходимый ей сервис при первом обращении.
function logMessage(string $message): void {
static $logger = null;
if ($logger === null) {
$logger = new FileLogger('/var/log/app.log');
}
$logger->write($message);
}Этот подход удобен для небольших хелперов, но затрудняет тестирование: невозможно подменить логгер на mock-объект. В продакшене предпочтительнее передавать логгер как аргумент или получать его из контейнера зависимостей.
3. Отслеживание глубины рекурсии
Статическая переменная хранит состояние, изменяющееся на каждом уровне рекурсивного вызова.
function flattenArray(array $array): array {
static $depth = 0;
$result = [];
$depth++;
if ($depth > 10) {
$depth--;
return $result;
}
foreach ($array as $item) {
if (is_array($item)) {
$result = array_merge($result, flattenArray($item));
} else {
$result[] = $item;
}
}
$depth--;
return $result;
}Официальная документация PHP приводит аналогичный пример рекурсивной функции со статическим счётчиком. Альтернатива — передача глубины дополнительным параметром, что делает состояние явным и упрощает отладку.
4. Защита от повторного входа
Флаг, предотвращающий рекурсивный вызов функции, пока она не завершила выполнение.
function processNode(Node $node): void {
static $processing = false;
if ($processing) {
return;
}
$processing = true;
foreach ($node->getChildren() as $child) {
processNode($child);
}
$processing = false;
}Паттерн полезен при обходе графов с потенциальными циклическими ссылками. Статическая переменная оправдана, поскольку состояние действительно должно быть приватным для функции и не требуется снаружи.
5. Однократная инициализация системы
Функция выполняет тяжёлую настройку только при первом вызове.
function initSystem(): void {
static $initialized = false;
if ($initialized) {
return;
}
setupDatabaseConnection();
loadConfiguration();
registerAutoloader();
$initialized = true;
}Сценарий близок к ленивой инициализации, но не возвращает сервис, а выполняет побочные действия. Альтернативой может быть явный вызов метода инициализации при старте приложения или использование глобальной переменной, доступной для проверки из других частей кода.
Таблица принятия решений
Представленные ниже критерии помогают определить, уместно ли использование статической переменной в конкретной ситуации.
| Критерий | Статическая переменная допустима | Лучше выбрать альтернативу |
|---|---|---|
| Время жизни процесса | Запрос завершается (CLI-скрипт, PHP-FPM) | Долгоживущий процесс (RoadRunner, Swoole, FrankenPHP) |
| Тестирование | Код не покрывается модульными тестами | Код подлежит автоматическому тестированию |
| Типизация | Тип значения прост и очевиден | Требуется строгая типизация |
| Область видимости состояния | Состояние должно быть приватным для функции | Состояние должно быть доступно извне для инспекции или сброса |
| Сложность инициализации | Инициализация тривиальна (константа или простой вызов) | Инициализация требует сложной логики с возможными ошибками |
| Параллелизм | Однопоточное выполнение | Многопоточная или асинхронная среда |
Если хотя бы один критерий указывает на правую колонку, стоит рассмотреть замену статической переменной на свойство класса, значение в DIC-контейнере или аргумент, передаваемый по ссылке.
Недостатки и риски статических переменных
Примеры выше демонстрируют удобство статических переменных, однако у этого инструмента есть серьёзные ограничения. Рассмотрим их подробнее.
Проблемы с тестированием. Статическая переменная сохраняет состояние между вызовами функции в рамках одного процесса. При запуске модульных тестов это состояние не сбрасывается автоматически, и результаты одного теста влияют на другой. Единственный способ сбросить значение — использовать рефлексию или запускать каждый тест в изолированном процессе, что замедляет выполнение тестового набора.
function getCachedConfig() {
static $config = null;
if ($config === null) {
$config = loadConfigFromFile();
}
return $config;
}В данном примере невозможно протестировать функцию с разными конфигурациями в одном тестовом классе без сброса статической переменной через ReflectionProperty или аналогичный механизм.
Утечки памяти в долгоживущих процессах. В традиционной модели PHP-FPM каждый HTTP-запрос выполняется в отдельном процессе, который завершается после отправки ответа. В этом окружении статическая переменная существует только в течение жизни запроса и автоматически уничтожается вместе с процессом. Однако в архитектурах с долгоживущими воркерами (RoadRunner, Swoole, FrankenPHP) один процесс живет часами и обрабатывает тысячи запросов. Воркер не умирает после ответа клиенту. Статическая переменная превращается в «глобальную помойку» в рамках воркера: она накапливает данные всех пользователей, прошедших через этот процесс, что ведет к неконтролируемому росту памяти и утечкам конфиденциальных данных.
Отсутствие типизации. В отличие от свойств класса, статические переменные в функциях не поддерживают объявление типа. Невозможно написать static private int $count = 0. Переменная может изменить тип в процессе работы, что снижает предсказуемость кода и лишает разработчика преимуществ строгой типизации.
Сложность отладки. Статические переменные существуют исключительно в локальной области видимости функции. Их значения не видны в отладчике среди глобальных переменных или свойств объектов. Для инспекции состояния требуется либо добавить отладочный вывод внутрь функции, либо использовать специализированные инструменты вроде Xdebug с ручным исследованием области видимости конкретного вызова.
Совместное использование в унаследованных методах. С PHP 8.1 статические переменные в методах, унаследованных без переопределения, разделяются между родительским и дочерним классами. Это поведение, описанное в официальной документации, может привести к неожиданным побочным эффектам:
class ParentClass {
public static function counter() {
static $count = 0;
return ++$count;
}
}
class ChildClass extends ParentClass {}
echo ParentClass::counter(); // 1
echo ChildClass::counter(); // 2 (до PHP 8.1 было бы 1)Если логика подразумевает независимые счётчики для каждого класса, такое поведение требует явного переопределения метода в дочернем классе.
Альтернативы. В большинстве случаев статическую переменную можно заменить на свойство класса (обычное или статическое), значение в DIC-контейнере, аргумент по ссылке или глобальную переменную. Все эти альтернативы делают состояние явным и доступным для внешнего наблюдения и управления (подробнее о методе сброса см. «Как сбросить значение статической переменной в тестах?»).
Изменения в PHP 8.1 и 8.3
Поведение статических переменных претерпело существенные изменения в последних версиях языка. Понимание этих изменений необходимо для корректной работы кода на современных версиях PHP.
Наследование методов со статическими переменными
До версии 8.1 статические переменные в унаследованных методах вели себя независимо: каждый класс получал собственную копию статической переменной при первом вызове метода. Начиная с PHP 8.1 поведение изменилось — теперь статическая переменная разделяется между родительским и дочерним классами, если дочерний класс не переопределяет метод. Это делает поведение статических переменных в методах аналогичным статическим свойствам класса.
class ParentClass {
public static function counter() {
static $count = 0;
return ++$count;
}
}
class ChildClass extends ParentClass {}
echo ParentClass::counter(); // 1
echo ParentClass::counter(); // 2
echo ChildClass::counter(); // 3 (до PHP 8.1 было бы 1)
echo ChildClass::counter(); // 4 (до PHP 8.1 было бы 2)Если требуется независимый счётчик для дочернего класса, необходимо явно переопределить метод:
class ChildClass extends ParentClass {
public static function counter() {
static $count = 0;
return ++$count;
}
}Официальная документация PHP подтверждает это изменение и приводит аналогичный пример.
Динамическая инициализация
До версии 8.3 статические переменные можно было инициализировать только константными выражениями — литералами, константами и простыми арифметическими операциями с ними. Вызов функции или метода при инициализации был запрещён.
Начиная с PHP 8.3 это ограничение снято. Теперь допускается инициализация произвольным выражением, включая вызовы функций, создание объектов и тернарные операторы.
function getConfig() {
static $config = loadConfigurationFile(); // Допустимо с PHP 8.3
return $config;
}
function getTimestamp() {
static $startTime = microtime(true); // Допустимо с PHP 8.3
return microtime(true) - $startTime;
}Это изменение упрощает код, избавляя от необходимости проверять null и выполнять инициализацию в отдельном условии. Сравните со старым подходом:
// До PHP 8.3
function getConfig() {
static $config = null;
if ($config === null) {
$config = loadConfigurationFile();
}
return $config;
}Подробнее о других возможностях PHP 8.3, включая json_validate(), атрибут #[Override] и типизированные константы классов, читайте в статье «PHP 8.3 что нового. Изменения и новый функционал».
Что это значит для вашего кода
При миграции проекта на PHP 8.1 и выше необходимо учитывать изменение поведения при наследовании. Если код опирался на независимость статических переменных в унаследованных методах, его логика может нарушиться. Рекомендуется провести аудит использования статических переменных в классах с наследованием.
Возможность динамической инициализации в PHP 8.3 устраняет один из основных источников шаблонного кода при работе со статическими переменными, делая их использование более лаконичным.
Почему 30% проектов используют статические переменные
Статистика Exakat, показывающая присутствие статических переменных в каждой третьей PHP-кодовой базе, заслуживает отдельного анализа. Цифра в 30% не означает, что треть разработчиков осознанно выбирает этот инструмент как архитектурное решение. Скорее, она отражает накопленные особенности экосистемы и типовые сценарии, в которых статическая переменная оказывается наименее затратным способом решить локальную задачу.
Основные причины широкого распространения этого приёма:
- Наследие процедурного подхода. PHP исторически развивался как язык с сильной процедурной составляющей. Многие проекты, начатые до широкого распространения современных фреймворков и DI-контейнеров, содержат функции-хелперы, в которых статическая переменная была единственным простым способом сохранить состояние между вызовами без захламления глобальной области видимости.
- Избегание создания класса. Для реализации простого кэша или однократной инициализации разработчик должен создать класс, определить в нём статическое свойство, написать метод доступа. Статическая переменная внутри функции решает ту же задачу в несколько строк. В условиях ограниченного времени или при работе над небольшими утилитами выбор часто делается в пользу более короткого пути.
- Отсутствие явных альтернатив в ядре языка. PHP не предоставляет встроенного механизма для мемоизации результатов функции, аналогичного декоратору
@lru_cacheв Python илиmemoizeв других языках. Статическая переменная заполняет этот пробел, пусть и с ограничениями. - Код, не рассчитанный на тестирование. В проектах, где автоматическое тестирование отсутствует или охватывает лишь малую часть кода, недостатки статических переменных просто не проявляются. Разработчик не сталкивается с проблемой сброса состояния между тестами и не видит причин отказываться от удобного инструмента.
- Эпизодическое использование в фреймворках. Даже современные фреймворки иногда прибегают к статическим переменным для решения узких задач, где накладные расходы на DI-контейнер или сервис-локатор неоправданны. Это дополнительно легитимизирует приём в глазах разработчиков, которые ориентируются на код популярных библиотек.
С учётом изменений в PHP 8.1 и 8.3, устранивших часть неудобств (совместное использование в наследовании теперь предсказуемо, инициализация стала лаконичнее), можно ожидать сохранения или даже некоторого роста частоты использования статических переменных. Однако тренд на чистую архитектуру и повсеместное внедрение автоматического тестирования будет оказывать противоположное давление, вытесняя статические переменные в нишу одноразовых скриптов и прототипов.
Часто задаваемые вопросы
Чем static $var в теле функции отличается от static::$var класса?
static $var внутри функции создаёт переменную, сохраняющую значение между вызовами этой конкретной функции. Область видимости ограничена телом функции, и доступ к переменной извне невозможен.
static::$var — это обращение к статическому свойству класса через позднее статическое связывание. Такое свойство принадлежит классу, а не его экземплярам, и доступно из любого места программы при условии соблюдения модификаторов видимости (public, protected, private). Подробнее это различие раскрыто в официальной документации по статическим свойствам классов.
Можно ли типизировать статическую переменную?
Нет. Статические переменные в функциях не поддерживают объявление типа. Конструкция вида static private int $count = 0 синтаксически неверна. Переменная может принимать значения любого типа и менять его в процессе работы. Если типизация критична, следует использовать статическое свойство класса с объявленным типом.
Как сбросить значение статической переменной в тестах?
Штатного способа сбросить статическую переменную извне не существует. В тестах применяют один из следующих подходов:
- Использование рефлексии для доступа к скрытому состоянию функции и его изменения.
- Запуск каждого теста в отдельном процессе (аннотация
@runInSeparateProcessв PHPUnit). - Рефакторинг тестируемого кода с заменой статической переменной на свойство объекта или значение в DI-контейнере, которое легко подменить в тестовом окружении.
Влияет ли область видимости анонимной функции на статическую переменную?
Да. Статическая переменная в анонимной функции привязана к конкретному экземпляру этой функции. Если анонимная функция создаётся заново при каждом вызове внешней функции, статическая переменная также будет переинициализироваться. Официальная документация приводит пример такого поведения.
Как статические переменные работают с рекурсией?
Статическая переменная существует в единственном экземпляре для всех уровней рекурсии. Это позволяет использовать её как счётчик глубины или флаг защиты от повторного входа. При выходе из рекурсивного вызова значение переменной сохраняется для вышестоящего уровня, поэтому необходимо явно управлять её состоянием (например, декрементировать счётчик перед возвратом).
Что делать с ошибкой «Compile Error: Static variable initializers must be constant»?
Эта ошибка возникает при попытке инициализировать статическую переменную неконстантным выражением в версиях PHP до 8.3. Например, следующий код вызовет ошибку на PHP 8.2 и ниже:
function getConfig() {
static $config = loadConfigurationFile(); // Ошибка до PHP 8.3
return $config;
}До PHP 8.3 статические переменные можно было инициализировать только константными выражениями: литералами, именованными константами и простыми арифметическими операциями с ними.
Решение зависит от версии PHP:
- PHP 8.3 и выше. Ошибки не будет — код с вызовом функции при инициализации заработает без изменений.
- PHP 8.2 и ниже. Используйте отложенную инициализацию с проверкой на
null:
function getConfig() {
static $config = null;
if ($config === null) {
$config = loadConfigurationFile();
}
return $config;
}Заключение
Статическая переменная в теле функции — инструмент с узкой, но оправданной областью применения. Преимущество заключается в лёгкости использования: пара строк кода заменяет создание отдельного класса или внедрение зависимости. Это делает её оптимальным выбором для CLI-скриптов, прототипов и одноразовых утилит, где время разработки важнее строгости архитектуры.
В то же время ограничения, связанные с тестированием, типизацией и поведением в долгоживущих процессах, делают статические переменные проблемным решением для продакшена. Если проект подразумевает модульное тестирование, развёртывание в среде с постоянными воркерами или длительную поддержку, используйте явные альтернативы: статические свойства классов, значения в DI-контейнере или параметры, передаваемые по ссылке.
Изменения, внесённые в PHP 8.1 и 8.3, устранили часть неудобств: поведение при наследовании стало предсказуемым, а инициализация — гибкой. Тем не менее ограничения (не тестируемость без рефлексии, отсутствие типизации) остаются в силе и вряд ли будут пересмотрены в будущих релизах языка, поскольку противоречат природе этой конструкции.
Итог: статическая переменная уместна тогда и только тогда, когда время жизни скрипта ограничено одним запросом или одной задачей, а необходимость в тестировании и внешнем наблюдении за состоянием отсутствует. В остальных случаях стоит выбрать явный и контролируемый способ управления состоянием.
Благодарности
Материал подготовлен на основе статьи «Usages of PHP Static variables», опубликованной в блоге Exakat. Оригинальное исследование, включая статистику о распространённости статических переменных в PHP-проектах, принадлежит автору исходной публикации. Русскоязычная версия дополнена актуальными сведениями из официальной документации PHP и расширена разделами о рисках, критериях принятия решений и изменениях в современных версиях языка.