In addition to the main features announced in previous posts of this series, Symfony 8.1 includes many smaller improvements that make day-to-day work easier. This post highlights the first batch.

Convert Between UUIDv7 and UUIDv4

Nicolas Grekas
Contributed by Nicolas Grekas in #63593

UUIDv7 identifiers are time-ordered, making them ideal as database primary keys. However, that property also leaks record creation times when you expose those identifiers in APIs. The new Uuid47Transformer class lets you store UUIDv7 internally while emitting UUIDv4-looking identifiers at your application boundaries:

1
2
3
4
5
6
7
8
9
10
11
12
use Symfony\Component\Uid\Uuid47Transformer;
use Symfony\Component\Uid\UuidV7;

// the secret must be at least 16 bytes; longer secrets are hashed automatically
$transformer = new Uuid47Transformer($secret);

$uuid = new UuidV7();
// returns a UuidV4 instance that hides the timestamp information
$external = $transformer->encode($uuid);

// returns the original UuidV7 instance (when using the same secret)
$original = $transformer->decode($external);

The conversion masks the UUIDv7 timestamp with a keyed SipHash-2-4 digest, making it reversible only with the same secret. When using FrameworkBundle, the transformer is registered as a service automatically (using kernel.secret as the key), so you can inject it anywhere by type-hinting Uuid47Transformer.

Convert Scalar Types During Denormalization

Jeroen Spee
Contributed by Jeroen Spee in #52173

Some data formats represent all values as strings (e.g. HTTP query strings or form data). When deserializing XML and CSV contents, Symfony already casts those strings to the int, float or bool types expected by the target properties. In Symfony 8.1, you can enable this behavior for any format via the new ENABLE_TYPE_CONVERSION context option:

1
2
3
4
5
6
7
8
9
10
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
// ...

// all values are strings, as in an HTTP query string
$data = ['age' => '39', 'sportsperson' => '1'];

// 'age' is cast to int and 'sportsperson' to bool to match the Person property types
$person = $serializer->denormalize($data, Person::class, context: [
    AbstractObjectNormalizer::ENABLE_TYPE_CONVERSION => true,
]);

Set the option to false to disable the conversion, even for the xml and csv formats. The option is also available in the serializer context builders.

Configurable Default Action in HTML Sanitizer

Adrien Roches
Contributed by Adrien Roches in #57653

When the HTML sanitizer finds a tag that is not part of the configuration, it drops the tag and all its children. In Symfony 8.1, you can change this behavior with the new default_action option, which accepts drop (the current default), block (remove the tag but keep its children) and allow:

1
2
3
4
5
6
7
8
9
10
11
# config/packages/html_sanitizer.yaml
framework:
    html_sanitizer:
        sanitizers:
            app.post_sanitizer:
                # ...

                # remove unconfigured tags, but keep processing their children
                default_action: 'block'
                # remove <figure> tags and their children entirely
                drop_elements: ['figure']

Reset the Kernel Between FrankenPHP Requests

Nicolas Grekas
Contributed by Nicolas Grekas in #64055

In FrankenPHP worker mode, the same kernel instance handles every request for the lifetime of the worker process. This is great for performance, but any state kept by services that don't implement ResetInterface may leak across requests.

Symfony 8.1 adds an opt-in feature: defining the FRANKENPHP_RESET_KERNEL environment variable makes the runtime clone the kernel after each request, so the next one starts with a fresh kernel and container:

1
2
# define this env var where you configure your FrankenPHP workers
FRANKENPHP_RESET_KERNEL=1

The default behavior doesn't change: the kernel is still reused across requests unless you set this variable. Resetting the kernel has a measurable throughput cost on "hello world" benchmarks, but it's still several times faster than the classic non-worker mode and it restores full per-request isolation.

Null-Safe Array Access in Expressions

Alex Rothberg
Contributed by Alex Rothberg in #62754

The ExpressionLanguage component supports the null-safe operator for property access (foo?.bar) and method calls (foo?.getBar()). Symfony 8.1 completes the feature with null-safe array access using the same ?.[...] syntax as JavaScript optional chaining:

1
2
3
4
5
6
7
8
// before: this throws an exception when getItems() returns null
$expressionLanguage->evaluate('fruit.getItems()[0]', ['fruit' => $fruit]);

// now: this returns null instead of throwing an exception
$expressionLanguage->evaluate('fruit.getItems()?.[0]', ['fruit' => $fruit]);

// you can combine it with the other null-safe operators
$expressionLanguage->evaluate('order?.getItems()?.[0]?.getName()', ['order' => $order]);

Outline-Style Console Blocks

Guillaume Van Der Putten
Contributed by Guillaume Van Der Putten in #63546

The success(), error(), warning() and similar SymfonyStyle methods fill the entire line with a background color, which can be hard to read on terminals with custom color schemes or high-contrast accessibility settings. Symfony 8.1 adds outline-style alternatives that display a colored border around the message while keeping the default text color:

1
2
3
4
5
6
7
8
// outlined alternatives exist for all the result methods:
// outlineSuccess(), outlineError(), outlineWarning(), outlineNote(),
// outlineInfo() and outlineCaution()
$io->outlineSuccess('Operation completed successfully.');
$io->outlineError('Something went wrong.');

// use outlineBlock() to customize the title and the colors
$io->outlineBlock('Deployment finished in 3.2s', 'Deploy', 'fg=cyan');

Disable Trailing Slash on Prefixed Root Routes

vvaswani
Contributed by vvaswani in #63689

When you apply a prefix to a collection of routes defined with the PHP DSL, the root route of the collection always gets a trailing slash (e.g. /categories/). In Symfony 8.1, the prefix() method accepts a new trailingSlashOnRoot argument (already available in YAML/XML imports) to disable this:

1
2
3
4
5
6
7
8
9
10
// config/routes.php
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;

return function (RoutingConfigurator $routes): void {
    // this generates /categories (instead of /categories/) and /categories/{id}
    $routes->collection('category_')
        ->prefix('/categories', trailingSlashOnRoot: false)
        ->add('index', '/')
        ->add('show', '/{id}');
};

Get Parent Role Names

Pierre-Emmanuel CAPEL
Contributed by Pierre-Emmanuel CAPEL in #53998

When using hierarchical roles, the getReachableRoleNames() method returns all the roles inherited by the given roles. Symfony 8.1 adds the inverse operation: getParentRoleNames() returns the roles that inherit from the given roles. This is useful for finding which roles can access everything a given role can access.

Consider the following role hierarchy:

1
2
3
4
5
# config/packages/security.yaml
security:
    role_hierarchy:
        ROLE_ADMIN: ROLE_USER
        ROLE_SUPER_ADMIN: ROLE_ADMIN
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use Symfony\Component\Security\Core\Role\RoleHierarchyInterface;

class RoleService
{
    public function __construct(
        private RoleHierarchyInterface $roleHierarchy,
    ) {
    }

    public function getParentRoles(array $roles): array
    {
        // for ['ROLE_USER'] this returns an array with 'ROLE_USER', 'ROLE_ADMIN'
        // and 'ROLE_SUPER_ADMIN', because those roles inherit ROLE_USER permissions
        return $this->roleHierarchy->getParentRoleNames($roles);
    }
}

Mark Classes as Safe for Twig's Escaper

Jérôme Tamarelle
Contributed by Jérôme Tamarelle in #63929

If a value object wraps pre-escaped or trusted HTML content, you have to apply the raw Twig filter every time you output it in a template. In Symfony 8.1, you can mark the class as safe for Twig's escaper using the new twig.safe_class resource tag, so its output is no longer escaped:

1
2
3
4
5
# config/services.yaml
services:
    App\Twig\HtmlString:
        resource_tags:
            - { name: twig.safe_class, strategy: html }

Unlike regular service tags, resource tags are attached to the class itself, not to a service. The strategy option accepts a single escaping strategy or a list of them, and you can tag the same class several times to mark it as safe for multiple strategies.

New ContainerProviderInterface Contract

Nicolas Grekas
Contributed by Nicolas Grekas in #63663

Symfony 8.1 adds a minimal ContainerProviderInterface to symfony/service-contracts, providing a standard way for objects to expose their service container:

1
2
3
4
5
6
7
8
namespace Symfony\Contracts\Service;

use Psr\Container\ContainerInterface;

interface ContainerProviderInterface
{
    public function getContainer(): ContainerInterface;
}

The console Application class provided by FrameworkBundle implements it (booting the kernel if needed). This enables decoupled use cases such as Messenger parallel workers, which need to bootstrap the application and access the container to resolve the message bus.

Published in #Living on the edge