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
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:
use SymfonyComponentUidUuid47Transformer;
use SymfonyComponentUidUuidV7;
// 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
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:
use SymfonyComponentSerializerNormalizerAbstractObjectNormalizer;
// ...
// 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
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:
# 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
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:
# 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
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:
// 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
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:
// 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
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:
// config/routes.php
use SymfonyComponentRoutingLoaderConfiguratorRoutingConfigurator;
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
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:
# config/packages/security.yaml
security:
role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: ROLE_ADMIN
use SymfonyComponentSecurityCoreRoleRoleHierarchyInterface;
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
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:
# config/services.yaml
services:
AppTwigHtmlString:
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 ContainerAwareInterface Contract
Symfony 8.1 adds a minimal ContainerAwareInterface to
symfony/service-contracts, providing a standard way for objects to expose
their service container:
namespace SymfonyContractsService;
use PsrContainerContainerInterface;
interface ContainerAwareInterface
{
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.




