Symfony 8.0.0 вышла 27 ноября 2025 года. 29 июля 2026 года проект выпустил версию 8.0.16 — свой последний релиз, и в конце месяца ветка была закрыта: больше никаких багфиксов и исправлений безопасности. Вот и весь жизненный цикл мажорной версии — восемь месяцев, согласно собственной публикации Symfony об окончании поддержки. На странице релиза 8.0 теперь написано «Unmaintained» и предлагается обновиться до 8.1.

Если вы обновились до 8.0 в день релиза, вы получили новую мажорную версию, минимальное требование PHP 8.4 и жесткий дедлайн. Если же вы остались на 7.4, то получили те же самые возможности, но с поддержкой багфиксов до ноября 2028 года и патчами безопасности до ноября 2029 года, судя по странице релизов.

Ничего неожиданного не произошло. Релизный цикл работает ровно так, как и было заявлено. А вот во сколько этот процесс обходится вашей кодовой базе каждые шесть месяцев, в основном зависит от того, сколько ваших файлов вообще знают о существовании Symfony.

Календарь — это контракт 

Процесс релизов Symfony на редкость прозрачен. Минорные версии выходят каждые полгода: одна в мае, другая в ноябре. Мажорные релизы выходят раз в два года, в ноябре нечетных лет. Каждая ветка получает максимум пять минорных версий, от X.0 до X.4, и последняя из них становится LTS-версией с длительной поддержкой.

Сроки поддержки легко укладываются в одну таблицу:


Тип версии Исправления багов Патчи безопасности
| Стандартная (от X.0 до X.3)  | 8 месяцев  | 8 месяцев
| LTS (X.4)  | 3 года  | 4 года

На той же странице четко указано, для кого предназначен каждый из путей. Быстро развивающиеся проекты выбирают стандартные версии: новый релиз каждые шесть месяцев и «двухмесячный период на обновление». Компании, которым нужна стабильность, используют LTS: новый релиз каждые два года и «целый год на обновление».

Посмотрим на текущую ситуацию. Symfony 8.1 вышла в мае 2026 года и поддерживается до января 2027-го. Релиз Symfony 8.2 запланирован на ноябрь 2026 года с поддержкой до июля 2027 года, а 7.4.21 — это текущий LTS. Если вы идете по минорным версиям, вы подписываетесь на постоянные переходы каждые полгода с восьминедельным окном наката каждый раз.

2I5mUbtxKAYcL2avPNZ7X7w9foRL9nhJhcvOKsT1.webp
Стандартные релизы обновляются каждые шесть месяцев. LTS стоит на месте годами.

Что на самом деле удалили в 8.0 

Каждый мажорный релиз выходит бок о бок с LTS. Symfony разрабатывает новую мажорную версию параллельно с последней минорной из старой ветки. Согласно документации по релизному процессу, «обе версии имеют одинаковые новые фичи, но отличаются устаревшим функционалом». Версия 7.4 сохранила deprecated-код. В 8.0 его удалили.

В посте об окончании поддержки приводятся точные цифры: в 8.0 было удалено 13 202 строки устаревшего кода, а минимальная версия PHP была повышена до 8.4. Кроме того, возможности PHP 8.4 использовались для удаления собственных внутренних механизмов Symfony. Нативные ленивые объекты (lazy objects), появившиеся в PHP 8.4 благодаря  ReflectionClass::newLazyGhost()  и  newLazyProxy() , заменили  LazyGhostTrait  и  LazyProxyTrait , которые Symfony раньше поддерживала для ленивых сервисов. В файле UPGRADE-8.0 в разделе VarExporter об этом написано в одну строчку: «Удалены  LazyGhostTrait  и  LazyProxyTrait , используйте вместо них нативные ленивые объекты».

Еще несколько пунктов из этого файла, которые непосредственно затрагивают код приложения:

  • DependencyInjection: «формат конфигурации XML больше не поддерживается».
  • Serializer: «Удалены алиасы классов в пространстве имен  Annotation , используйте атрибуты».
  • Validator: «Значение по умолчанию для опции  $requireTld  ограничения  Url  изменено на  true ».

Взгляните на эти три пункта через призму своего проекта. Первый затронет конфигурационные файлы. Второй коснется каждого места, где в классах используется  use Symfony\Component\Serializer\Annotation\Groups . Третий вообще не вызовет ошибок при сборке:  #[Assert\Url]  без явно переданного аргумента продолжит компилироваться, но начнет отклонять URL вида  http://localhost , которые раньше считались валидными.

То, где именно лежат затронутые файлы, и определяет итоговую цену обновления.

Как депрекейшены попадают из Symfony в ваш CI 

Каждое из этих удалений сначала анонсировалось в коде через одну маленькую функцию. В пакете  symfony/deprecation-contracts функция  trigger_deprecation()  объявлена со следующей сигнатурой:

 function trigger_deprecation(
    string $package,
    string $version,
    string $message,
    mixed ...$args
): void
 

Ее тело состоит всего из одного выражения. Она добавляет префикс  Since <package> <version>:  к сообщению, форматирует его через  vsprintf  при наличии дополнительных аргументов и вызывает  @trigger_error(..., \E_USER_DEPRECATED) .

Оператор  @  здесь принципиален. Он не дает уведомлению попасть в продакшен-логи. При этом он не скрывает его от кастомного обработчика ошибок: как сказано в руководстве по PHP, при зарегистрированном обработчике «настройки error_reporting() не имеют эффекта, и обработчик ошибок будет вызываться в любом случае». Именно поэтому дебаг-панель в dev-окружении и PHPUnit-bridge могут выводить список депрекейшенов, которые никогда не показываются на экране.

Вы можете вызывать эту же функцию и в собственных пакетах, что весьма полезно при переименовании методов портов:

 public function charge(Money $amount): Receipt
{
    trigger_deprecation(
        'acme/billing',
        '2.3',
        'Method "%s()" is deprecated, use "%s()".',
        __METHOD__,
        'capture'
    );

    return $this->capture($amount);
}
 

На принимающей стороне находится PHPUnit-bridge. Он раскладывает все обнаруженные депрекейшены по категориям:  self  (сработали в вашем коде),  direct  (сработали в vendor-пакете, вызванном вашим кодом),  indirect  (вызовы vendor-кода из vendor-кода) и  other . Пороги срабатывания настраиваются через переменную окружения  SYMFONY_DEPRECATIONS_HELPER , и в документации указаны ключи  max[total] ,  max[self] ,  max[direct]  и  max[indirect] :

 <php>
  <env name="SYMFONY_DEPRECATIONS_HELPER"
       value="max[self]=0&amp;max[direct]=0"/>
</php>
 

Такая настройка роняет сборку при наличии депрекейшенов, которые вы в силах исправить, но игнорирует шум внутри сторонних пакетов, на который вы повлиять не можете. Если вам досталось тяжелое наследие с кучей предупреждений, bridge позволяет сгенерировать бейзлайн командой  generateBaseline=true&baselineFile=./tests/allowed.json  и в дальнейшем падать только на новых предупреждениях. На этом строится вся процедура из руководства по мажорному обновлению: сначала обновляемся до последнего минорного релиза, вычищаем все депрекейшены и только потом поднимаем мажорную версию.

Количество  direct  является отражением важного архитектурного показателя: насколько много мест в вашем коде напрямую завязаны на Symfony.

Число, которое предскажет стоимость вашего апгрейда 

Сделаем два замера. Первый займет ровно десять секунд:

 # every PHP file under src/ that imports Symfony
grep -rlE '^use Symfony\\' src | wc -l

# the same, minus the adapter folder
grep -rlE '^use Symfony\\' src \
  | grep -v '^src/Infrastructure/' \
  | wc -l
 

Замените  src/Infrastructure/  на путь, где у вас лежат адаптеры. Второе число — самое важное. Каждый посчитанный им файл — это место, до которого может добраться удаление вроде упразднения алиасов  Annotation  в Serializer, и каждый такой файл находится в непосредственной близости к бизнес-логике. Упавший контроллер чинится в одну строчку. Упавшая сущность — это тоже однострочный фикс, но уже с вопросом от ревьюера: «А не сломалась ли при этом логика домена?».

Команда grep работает честно, но довольно грубо. Она пропустит инлайновые FQCN вроде  new \Symfony\Component\... , а класс  src/Kernel.php  из стандартного скелета попадет в выдачу просто потому, что он наследуется от класса Symfony. Считайте это отправной точкой.

Второй замер превращает этот подсчет в строгое правило. Инструмент Deptrac ( composer require --dev deptrac/deptrac ) с помощью коллекторов разделяет классы по слоям и выдает ошибку, когда слой ссылается на то, что ему видеть запрещено. В справочнике по коллекторам описаны  directory  для локальных папок и  classNameRegex  для поиска по именам классов, чего вполне достаточно, чтобы вынести сам фреймворк Symfony в отдельный слой:

 deptrac:
  paths:
    - ./src
  layers:
    - name: Domain
      collectors:
        - type: directory
          value: src/Domain/.*
    - name: Application
      collectors:
        - type: directory
          value: src/Application/.*
    - name: Adapters
      collectors:
        - type: directory
          value: src/Infrastructure/.*
    - name: Symfony
      collectors:
        - type: classNameRegex
          value: '#^Symfony\\.*#'
  ruleset:
    Domain: ~
    Application:
      - Domain
    Adapters:
      - Application
      - Domain
      - Symfony
 

Запустите проверку командой  vendor/bin/deptrac analyse --config-file=deptrac.yaml  (по умолчанию конфиг инструмента теперь называется  deptrac.php , поэтому YAML-файл лучше указать явно). Правило  Domain: ~  означает, что домен не имеет права зависеть ни от одного другого слоя, включая Symfony. Список нарушений при первом запуске примерно совпадет с числом из вашего grep, но уже с указанием конкретных файлов и строк. Добавьте эту проверку в CI — и это число теперь будет только уменьшаться.

jGSW34vDA9oqIeKk4N5MolRDNPNprA6cIYq90lOH.webp
Код фреймворка — только на внешнем кольце. Ядро никогда его не импортирует.

Фреймворк на границе — рутина на границе 

Возьмем пример с удалением классов из Serializer и проследим его влияние при обоих архитектурных подходах.

При связанной архитектуре сущность  Order  содержит атрибут  #[Groups(['order:read'])] , импортированный из старого пространства имен  Annotation . Обновление до 8.0 затрагивает вашу доменную модель, ровно как и любое будущее изменение в работе метаданных сериализации.

При изолированной (decoupled) архитектуре  App\Domain\Order\Order  — это обычный PHP-класс. HTTP-адаптер маппит его в DTO ответа, и только этот DTO вообще знает о существовании Serializer:

 namespace App\Infrastructure\Http\Order;

use App\Domain\Order\Order;
use Symfony\Component\Serializer\Attribute\Groups;

final class OrderResponse
{
    #[Groups(['order:read'])]
    public string $id;

    #[Groups(['order:read'])]
    public int $totalCents;

    public static function from(Order $o): self
    {
        $r = new self();
        $r->id = $o->id()->toString();
        $r->totalCents = $o->total()->cents();
        return $r;
    }
}
 

Импорт  Attribute\Groups  — это как раз то, что используется в актуальной документации по Serializer. То же самое справедливо и для изменения значения по умолчанию в  Url : повесьте ограничение на входной DTO в слое адаптеров, явно передайте  requireTld , и value object самого домена  WebsiteUrl  сохранит те правила валидации, которые требуются вашему бизнесу. Смена дефолтов в Symfony перестает быть решением, влияющим на домен.

Это не значит, что обновления станут абсолютно бесплатными. Форматы конфигов, бандлы и рецепты все равно развиваются, и адаптеры придется дорабатывать. Меняется сам характер работы: у вас есть изолированная папка, которую можно обновить отдельно от всего остального и прогнать через тесты домена, не зависящие от Symfony, а значит, защищенные от поломок из-за смены API фреймворка.

Главная причина долго сидеть на LTS — ощущение, что каждый минорный апгрейд превращается в лотерею для всей кодовой базы. Если же правило Deptrac соблюдается, а grep за пределами адаптеров выдает около нуля, каждый минорный релиз затрагивает ровно один слой под защитой deprecation-проверок. В этом случае переход на 8.1, 8.2 и далее каждые полгода становится обычной рутиной, а выбор LTS — лишь способом выполнять эту рутину реже.

Запустите grep прямо сейчас. Если второе число перевалило за сотню, вы уже знаете, какую ветку релизов вы можете себе позволить.

Если статья оказалась полезной 

Команда grep из этой статьи — лишь беглая демонстрация идеи, которой я посвятил целую книгу. В Decoupled PHP подробно рассказывается, как вынести Symfony и Laravel на внешние границы реального приложения, спрятав их за портами и адаптерами, чтобы переход на следующий мажорный релиз сводился к правке адаптеров, а не к переписыванию проекта.

AKBwJjMR2Xmts08ebPv0HPSDbsYyWgQNImkiElzO.webp

Книга доступна на Kindle, а также в мягкой и твердой обложке. Вышли издания на английском, немецком и японском языках, скоро появятся версии на португальском и испанском.