<? phpukraine ЕКОСИСТЕМА
⌕Пошук по платформі
SYMFONY · ЩО НОВОГО

Що нового в Symfony 8.1

  1. 01Мінорний реліз у межах обіцянки зворотної сумісності: більшості застосунків вистачить composer update і розбору deprecations.
  2. 02Найбільше змін у Console: кілька команд в одному класі, argument resolvers як у контролерах і новий API для тестів.
  3. 03Для контролерів є #[Serialize], #[RateLimit] і #[MapRequestHeader], а ядро й бандли переїхали в DependencyInjection, тож консольному застосунку HttpKernel більше не потрібен.
СТАТУС Активна підтримка
Реліз
29 травня 2026
Останній патч
8.1.8
Активна підтримка до
31.01.2027
Безпека до
31.01.2027
НОВІ МОЖЛИВОСТІ · 6

Нові можливості

1.1 · Console

Кілька команд в одному класі

Повʼязані команди часто ділять ті самі залежності: репозиторій, API-клієнт, логер. Досі на кожну команду був окремий клас з однаковим конструктором. Тепер їх можна зібрати в один клас, як дії контролера, і autoconfiguration зареєструє кожен позначений метод як незалежну команду.

+ SYMFONY 8.1
final class UserCommands
{
    public function __construct(private UserRepository $users) {}

    #[AsCommand('app:user:create', description: 'Створює користувача')]
    public function create(OutputInterface $output): int
    {
        // ...
        return Command::SUCCESS;
    }

    #[AsCommand('app:user:delete', description: 'Видаляє користувача')]
    public function delete(OutputInterface $output): int
    {
        // ...
        return Command::SUCCESS;
    }
}
− SYMFONY 8.0
#[AsCommand('app:user:create')]
final class CreateUserCommand
{
    public function __construct(private UserRepository $users) {}

    public function __invoke(OutputInterface $output): int { /* ... */ }
}

#[AsCommand('app:user:delete')]
final class DeleteUserCommand
{
    public function __construct(private UserRepository $users) {}

    public function __invoke(OutputInterface $output): int { /* ... */ }
}
1.2 · Console

Argument resolvers у командах

Команда отримувала сирий ID і сама шукала сутність у репозиторії. У 8.1 консоль використовує той самий механізм value resolvers, що й контролери: #[MapEntity] знаходить сутність, вбудовані resolvers розбирають backed enum, дати, UUID і ULID. Сервіси можна просити параметрами __invoke(), і там працюють #[Autowire] та #[Target].

+ SYMFONY 8.1
use Symfony\Bridge\Doctrine\Attribute\MapEntity;

public function __invoke(
    ReportGenerator $reports,
    #[Argument, MapEntity] User $user,
): int {
    // php bin/console app:report 42 знайде User з id 42
}
− SYMFONY 8.0
public function __construct(
    private UserRepository $users,
    private ReportGenerator $reports,
) {}

public function __invoke(#[Argument] int $userId): int
{
    $user = $this->users->find($userId);
    // ...
}
1.3 · HttpKernel

#[Serialize] для контролерів

В API-контролерах роками повторювалась одна схема: інжект serializer, виклик serialize() і JsonResponse з потрібним статусом. З атрибутом #[Serialize] метод просто повертає обʼєкт або масив, формат береться із запиту (JSON за замовчуванням), а Content-Type ставиться сам. Якщо Serializer не знає запитаного формату, клієнт отримує 415.

+ SYMFONY 8.1
use Symfony\Component\HttpKernel\Attribute\Serialize;

#[Serialize(code: 201, context: ['groups' => ['read']])]
public function __invoke(): ProductCreated
{
    return new ProductCreated(101);
}
− SYMFONY 8.0
public function __invoke(SerializerInterface $serializer): JsonResponse
{
    $json = $serializer->serialize(new ProductCreated(101), 'json');

    return JsonResponse::fromJsonString($json, 201);
}
1.4 · HttpKernel

#[RateLimit] для контролерів

Раніше в контролер інжектили фабрику лімітера, будували ключ, споживали токен і кидали виняток вручну. #[RateLimit] посилається на лімітер з framework.rate_limiter і робить це за вас: ключ за замовчуванням складається з IP клієнта, методу і шляху, а задати свій можна через Expression. Атрибут повторюваний і ставиться також на весь клас контролера.

+ SYMFONY 8.1
use Symfony\Component\HttpKernel\Attribute\RateLimit;

#[RateLimit('api')]
public function index(): JsonResponse { /* ... */ }

// дорогий ендпоінт: лише POST, 5 токенів за запит
#[RateLimit('api', methods: ['POST'], tokens: 5)]
public function export(): JsonResponse { /* ... */ }
− SYMFONY 8.0
public function index(Request $request, RateLimiterFactoryInterface $apiLimiter): JsonResponse
{
    if (!$apiLimiter->create($request->getClientIp())->consume()->isAccepted()) {
        throw new TooManyRequestsHttpException();
    }

    // ...
}
1.5 · DependencyInjection

Застосунки без HTTP

Консольні утиліти, воркери і споживачі черг не обробляють HTTP, але заради контейнера, бандлів і конфігів тягнули HttpKernel та FrameworkBundle. Тепер AbstractKernel і KernelTrait живуть у DependencyInjection і дають той самий життєвий цикл контейнера, що й MicroKernelTrait, а для консолі вистачає ConsoleBundle. HttpKernel\Kernel сам розширює AbstractKernel, тому наявні застосунки працюють без змін.

+ SYMFONY 8.1
use Symfony\Component\DependencyInjection\Kernel\AbstractKernel;
use Symfony\Component\DependencyInjection\Kernel\KernelTrait;

class Kernel extends AbstractKernel
{
    use KernelTrait;
}

// config/bundles.php
return [
    Symfony\Component\Console\ConsoleBundle::class => ['all' => true],
];
− SYMFONY 8.0
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;

class Kernel extends BaseKernel
{
    use MicroKernelTrait;
}
1.6 · Tui

Компонент Tui

Console створена для команд, аргументів і виводу, а перемальовувати екран на кожне натискання вона не вміє. Tui бере це на себе: поля вводу і редактори, списки, вкладки, прогрес-бари, Markdown, стилі в дусі CSS і неблокуючий цикл подій на Fibers і Revolt. Компонент ставиться окремо (symfony/tui) і поки experimental, тож обіцянка зворотної сумісності на нього не поширюється.

+ SYMFONY 8.1
use Symfony\Component\Tui\Tui;
use Symfony\Component\Tui\Widget\InputWidget;
use Symfony\Component\Tui\Widget\TextWidget;

$tui = new Tui();

$input = new InputWidget();
$input->setPrompt('Імʼя: ');
$input->onSubmit(fn () => $tui->stop());

$tui->add(new TextWidget('Введіть імʼя і натисніть Enter:'));
$tui->add($input);
$tui->setFocus($input);
$tui->run();

echo 'Привіт, '.$input->getValue()."!\n";
СИНТАКСИС · 2

Синтаксис

2.1 · HttpKernel

#[MapRequestHeader]

Для query-параметрів і тіла запиту атрибути вже були, а заголовок доводилось діставати з Request. #[MapRequestHeader] бере назву з імені аргументу ($acceptLanguage читає accept-language) або з опції name. Тип array повертає всі значення, AcceptHeader дає розібраний обʼєкт, а без обовʼязкового заголовка Symfony відповідає 400.

+ SYMFONY 8.1
use Symfony\Component\HttpKernel\Attribute\MapRequestHeader;

public function index(
    #[MapRequestHeader] string $acceptLanguage,
    #[MapRequestHeader(name: 'x-request-id')] ?string $requestId,
): Response {
    // ...
}
− SYMFONY 8.0
public function index(Request $request): Response
{
    $requestId = $request->headers->get('x-request-id');
    // ...
}
2.2 · HttpKernel

#[Cache] з умовою if

Коли кешувати треба було не завжди, скажімо крім режиму попереднього перегляду, заголовки ставили вручну в тілі контролера. Тепер опція if приймає вираз ExpressionLanguage або замикання, у виразах доступні request і args, а кілька #[Cache] з різними умовами можна поставити на один метод. Замикання в атрибутах потребують PHP 8.5, на 8.4 пишіть вирази.

+ SYMFONY 8.1
use Symfony\Component\HttpKernel\Attribute\Cache;

#[Cache(public: true, maxage: 3600, if: "not request.query.has('preview')")]
#[Cache(public: false, maxage: 0, if: "request.query.has('preview')")]
public function show(Article $article): Response
{
    return $this->render('article/show.html.twig', ['article' => $article]);
}
− SYMFONY 8.0
public function show(Request $request, Article $article): Response
{
    $response = $this->render('article/show.html.twig', ['article' => $article]);

    if (!$request->query->has('preview')) {
        $response->setPublic();
        $response->setMaxAge(3600);
    }

    return $response;
}
ФУНКЦІЇ · 2

Нові функції

3.1 · Console

runCommand() і ExecutionResult

Щоб перевірити stdout і stderr окремо, треба було вмикати capture_stderr_separately, а порядок виводу при цьому губився. CommandTester::run() повертає ExecutionResult з getOutput(), getErrorOutput(), getDisplay() і statusCode, а відповіді на інтерактивні питання йдуть тим самим викликом. У KernelTestCase додали runCommand(), який сам знаходить команду в застосунку.

+ SYMFONY 8.1
$result = static::runCommand('app:create-user', ['username' => 'Wouter']);

$this->assertCommandIsSuccessful($result);
$this->assertStringContainsString('Wouter', $result->getOutput());

$result->getErrorOutput(); // лише stderr
− SYMFONY 8.0
$application = new Application(self::bootKernel());
$tester = new CommandTester($application->find('app:create-user'));
$tester->execute(['username' => 'Wouter']);

$tester->assertCommandIsSuccessful();
3.2 · HttpKernel

Файли і variadic у #[MapRequestPayload]

DTO, де поруч зі звичайними полями є файл, через #[MapRequestPayload] не заповнювався, і файл брали окремим аргументом. Тепер для multipart/form-data Symfony обʼєднує поля і файли, включно з вкладеними масивами, ще до десеріалізації. Масив DTO замість опції type можна оголосити як variadic-параметр.

+ SYMFONY 8.1
class ProductDto
{
    public ?string $name = null;
    public ?UploadedFile $image = null;
}

public function upload(#[MapRequestPayload] ProductDto $data): Response
{
    // $data->image уже UploadedFile
}

// [{"value": 50}, {"value": 23}] стане двома обʼєктами Price
public function createPrices(#[MapRequestPayload] Price ...$prices): Response
{
    // ...
}
− SYMFONY 8.0
public function upload(
    #[MapRequestPayload] ProductDto $data,
    #[MapUploadedFile] ?UploadedFile $image = null,
): Response {
    // ...
}
ПРОДУКТИВНІСТЬ · 2

Продуктивність

4.1 · VarExporter

DeepCloner

Глибоку копію зазвичай робили через unserialize(serialize()), і це повільно та дорого за памʼяттю, бо рядки й масиви перестають ділитися через copy-on-write. DeepCloner відтворює граф напряму і copy-on-write зберігає, за бенчмарками з анонсу він швидший у 4-15 разів. Symfony вже використовує його сам: при компіляції контейнера, у Form і в ArrayAdapter кешу.

+ SYMFONY 8.1
use Symfony\Component\VarExporter\DeepCloner;

$copy = DeepCloner::deepClone($order);

// один прототип, багато копій: граф аналізується один раз
$cloner = new DeepCloner($prototype);
$first = $cloner->clone();
$second = $cloner->clone();
− SYMFONY 8.0
$copy = unserialize(serialize($order));
4.2 · Messenger

Messenger: --fetch-size і --no-reset=N

Воркер брав з транспорту по одному повідомленню, тобто робив мережевий запит на кожне. Опція --fetch-size просить кілька за виклик, і транспорти, що це підтримують, її враховують: SQS до 10 повідомлень, Redis через COUNT, Doctrine через LIMIT. А --no-reset тепер приймає число, і сервіси скидаються раз на N повідомлень, а не після кожного.

+ SYMFONY 8.1
php bin/console messenger:consume async --fetch-size=8 --no-reset=100
− SYMFONY 8.0
php bin/console messenger:consume async

# або без скидання сервісів узагалі
php bin/console messenger:consume async --no-reset
БЕЗПЕКА · 2

Безпека

5.1 · Uid

Uuid47Transformer

UUIDv7 зручні як первинні ключі, бо впорядковані за часом, але з такого ID в API видно, коли створено запис. Uuid47Transformer маскує мітку часу ключованим SipHash-2-4 і віддає ідентифікатор, схожий на UUIDv4, а з тим самим секретом повертає його в UUIDv7. У FrameworkBundle сервіс уже зареєстровано з kernel.secret як ключем, потрібне розширення sodium.

+ SYMFONY 8.1
use Symfony\Component\Uid\Uuid47Transformer;

public function __construct(private Uuid47Transformer $uuids) {}

// UuidV7 з бази стає UuidV4 для API
$publicId = $this->uuids->encode($order->getId());

// і назад, з тим самим секретом
$id = $this->uuids->decode($publicId);
5.2 · FrameworkBundle

getAllowedEnvs()

Помилка в APP_ENV (prodution замість prod) чи значення, що лишилось з CI, давали тихий старт у невідомому середовищі, де prod-налаштування безпеки можуть не діяти. Тепер у Kernel можна перевизначити getAllowedEnvs(), і з чужим середовищем ядро одразу кине виняток. Порожній масив, як і раніше, дозволяє будь-яке.

+ SYMFONY 8.1
class Kernel extends BaseKernel
{
    use MicroKernelTrait;

    private function getAllowedEnvs(): array
    {
        return ['prod', 'dev', 'test'];
    }
}
DEPRECATED

Застаріло

Ще працює, але дає попередження і зникне в одній із наступних версій.

Іменовані аліаси без #[Target]Збіг autowiring-аліасу лише за назвою параметра, як $imageStorage, застарів. Додайте #[Target], у 9.0 старий спосіб приберуть.
getDefault*Name() і getDefaultPriority()Статичні методи для індексу і пріоритету в tagged iterators і locators замінює атрибут #[AsTaggedItem].
Запис у властивості Request і ResponseПрисвоєння на кшталт $request->query = ... чи $response->headers = ... застаріло. Передавайте значення в конструктор або викликайте initialize().
BundleInterface, FileLocator і Extension з HttpKernelКласи переїхали в DependencyInjection (Kernel\BundleInterface, Kernel\FileLocator, Extension\Extension), старі імена поки працюють.
Bundle::registerCommands()Замість перевизначення методу реєструйте команди атрибутом #[AsCommand] або тегом console.command.
ConstraintValidatorInterface::validate() і initialize()Валідатор, що реалізує інтерфейс напряму, переходить на validateInContext(). Нащадків ConstraintValidator це не стосується.
Hydrator і Instantiator з VarExporterЇх замінює deepclone_hydrate(), яка створює обʼєкт і заповнює властивості одним викликом.
Нестрогий розбір дат у SerializerДату, що не відповідає формату за замовчуванням, DateTimeNormalizer ще розбирає конструктором, але з deprecation. У 9.0 тут буде NotNormalizableValueException.
BREAKING

Ламає сумісність

Перевірте це до оновлення: такий код перестане працювати або поводитиметься інакше.

Недекодовані повідомлення MessengerПовідомлення, яке не вдалося декодувати, більше не видаляється з черги, а проходить retry і потрапляє у failure transport.
max_ttl у CachingHttpClientВідповіді кешуються щонайбільше на 24 години, навіть якщо сервер дозволив довше. Інший ліміт задає опція max_ttl.
Placeholder обовʼязкового ChoiceTypeУ select для required-поля placeholder тепер має атрибут hidden. Стару поведінку повертає 'placeholder_attr' => [].

Як оновитися

0 / 6