Home / Symfony / New in Symfony 8.1: Misc Improvements (Part 2)

New in Symfony 8.1: Misc Improvements (Part 2)

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 a second batch of them.

Build Semaphores on Any Lock Backend


Alexander Schranz
Contributed by
Alexander Schranz
in
#59202

The Semaphore component limits the number of processes that can access a shared
resource at the same time. Until now, its only built-in store was based on
Redis, which left applications using other backends without a usable option.

Symfony 8.1 adds a LockStore that builds a semaphore on top of the
Lock component, so you can reuse any lock backend (database, filesystem, Redis,
etc.). A semaphore with a limit of N is implemented using N individual locks
behind the scenes:

use SymfonyComponentLockLockFactory;
use SymfonyComponentLockStoreFlockStore;
use SymfonyComponentSemaphoreSemaphoreFactory;
use SymfonyComponentSemaphoreStoreLockStore;

$lockFactory = new LockFactory(new FlockStore());
$semaphoreFactory = new SemaphoreFactory(new LockStore($lockFactory));

// allow at most 3 processes to hold this semaphore at the same time
$semaphore = $semaphoreFactory->createSemaphore('thumbnail-generation', 3);
$semaphore->acquire();

When using FrameworkBundle, point the semaphore at a configured lock with the new
lock:// DSN:

# config/packages/semaphore.yaml
framework:
    lock:
        default: 'flock'
        redis: '%env(REDIS_DSN)%'
    semaphore:
        default: 'lock://'      # uses the "default" lock
        other: 'lock://redis'   # uses the "redis" lock

Collect Extra Attributes Errors While Deserializing


NorthBlue333
Contributed by
NorthBlue333
in
#46654

When deserializing an incoming payload into an object, you can pass the
COLLECT_DENORMALIZATION_ERRORS option to gather every type mismatch in a
single PartialDenormalizationException instead of failing on the first one.
However, attributes that didn’t exist on the target class were reported
separately, so you couldn’t validate everything in one pass.

Symfony 8.1 adds the COLLECT_EXTRA_ATTRIBUTES_ERRORS option, which collects
those unexpected attributes together with the type errors:

use SymfonyComponentSerializerExceptionPartialDenormalizationException;
use SymfonyComponentSerializerNormalizerAbstractNormalizer;
use SymfonyComponentSerializerNormalizerDenormalizerInterface;

try {
    $post = $serializer->deserialize($request->getContent(), BlogPost::class, 'json', [
        DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS => true,
        DenormalizerInterface::COLLECT_EXTRA_ATTRIBUTES_ERRORS => true,
        AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES => false,
    ]);
} catch (PartialDenormalizationException $e) {
    // type mismatches (e.g. a string where an int was expected)
    foreach ($e->getNotNormalizableValueErrors() as $error) {
        // $error->getPath(), $error->getExpectedTypes(), ...
    }

    // attributes sent by the client that don't exist on BlogPost
    if (null !== $extraAttributesError = $e->getExtraAttributesError()) {
        $unexpected = $extraAttributesError->getExtraAttributes();
    }
}

The previous getErrors() method is now deprecated; use
getNotNormalizableValueErrors() instead.

Use the :has() Selector in CSS Expressions


Franck RANAIVO-HARISOA
Contributed by
Franck RANAIVO-HARISOA
in
#49388

The CssSelector component converts CSS selectors into XPath expressions, which
is what lets you query the DOM with CSS syntax in the DomCrawler (for example, in
functional tests). Symfony 8.1 adds support for the :has() relational pseudo-class,
so you can select elements based on their descendants or siblings:

use SymfonyComponentCssSelectorCssSelector;

// <article> elements that contain an <h2> somewhere inside them
CssSelector::toXPath('article:has(h2)');

// <div> elements with a direct child that has the "error" class
CssSelector::toXPath('div:has(> .error)');

This is the same selector now available in all major browsers, and it works
everywhere CssSelector is used, including Crawler::filter() in your tests.

Wrap Response Fragments Before Parsing in Tests


Hubert Lenoir
Contributed by
Hubert Lenoir
in
#62892

Some endpoints return HTML fragments rather than full documents (for example, an
AJAX action that returns a few table rows). When the test client parses a bare
<tr> element, the HTML5 parser rewrites or drops it because it isn’t valid
outside a table, so your assertions run against mangled markup.

Symfony 8.1 adds the wrapContent() method to the test client. It wraps the
response body in the structure you provide (using %s as a placeholder)
before parsing, without changing the actual HTTP response:

// wrap fragments in a full table so <tr>/<td> elements are parsed correctly
$client->wrapContent('<table>%s</table>');

$crawler = $client->request('GET', '/comments/rows');

// the rows are now available in the crawler as expected
$this->assertCount(3, $crawler->filter('tr'));

Serialize Semaphore Keys Across Processes


Paul Clegg
Contributed by
Paul Clegg
in
#63059

Sometimes you need to acquire a semaphore in one process and release it in
another, for example when a controller starts a long task and a Messenger worker
finishes it. That requires passing the semaphore Key to the other process.

Symfony 8.1 makes the Key class serializable and adds a
SemaphoreKeyNormalizer so you can serialize keys with the Serializer
component too. When using FrameworkBundle, the normalizer is registered
automatically:

use SymfonyComponentSemaphoreKey;

// process 1: acquire the semaphore, then serialize its key
$key = new Key('report-generation', 3);
$semaphore = $factory->createSemaphoreFromKey($key, autoRelease: false);
$semaphore->acquire();

$serializedKey = $serializer->serialize($key, 'json');

// process 2: rebuild the semaphore from the key and release it
$key = $serializer->deserialize($serializedKey, Key::class, 'json');
$factory->createSemaphoreFromKey($key)->release();

Restrict the Allowed Values of APP_ENV


Vincent Pabst
Contributed by
Vincent Pabst
in
#63429

The APP_ENV environment variable selects which configuration your application
boots with. If it ever holds an unexpected value (a typo like prodution or a
leftover CI/CD value), Symfony happily boots into that unknown environment, which
can silently disable protections you rely on in production.

Symfony 8.1 lets you declare the list of valid environments via the new getAllowedEnvs()
method of MicroKernelTrait. If the current environment is not in the returned
list, the kernel throws an exception immediately instead of booting:

// src/Kernel.php
namespace App;

use SymfonyBundleFrameworkBundleKernelMicroKernelTrait;
use SymfonyComponentHttpKernelKernel as BaseKernel;

class Kernel extends BaseKernel
{
    use MicroKernelTrait;

    /**
     * @return list<string> An array of allowed values for APP_ENV
     */
    private function getAllowedEnvs(): array
    {
        return ['prod', 'dev', 'test'];
    }
}

Booting with APP_ENV=staging now fails fast with a clear error instead of
starting in an unintended state. When the method returns an empty array,
the previous behavior is unchanged and all environments are allowed.

More Clear-Site-Data Directives on Logout


Christian Flothmann
Contributed by
Christian Flothmann
in
#62322

When a user logs out, Symfony can send the Clear-Site-Data HTTP header to
tell the browser to wipe locally stored data. Until now, you could only clear
cache, cookies, storage and executionContexts.

Symfony 8.1 adds the three remaining directives defined by the specification:
clientHints, prefetchCache and prerenderCache:

# config/packages/security.yaml
security:
    firewalls:
        main:
            logout:
                clear_site_data:
                    - 'cookies'
                    - 'storage'
                    - 'clientHints'
                    - 'prefetchCache'
                    - 'prerenderCache'

Reproducible Builds with SOURCE_DATE_EPOCH


Charles-Edouard Coste
Contributed by
Charles-Edouard Coste
in
#62538

When Symfony compiles the service container, it records the build time so caches
can be invalidated. Because that value defaults to the current time, two builds
from the exact same source code produce slightly different containers, which breaks
reproducible builds.

Symfony 8.1 supports the conventional SOURCE_DATE_EPOCH environment variable
shared across the reproducible-builds ecosystem. When it’s defined, Symfony uses
its Unix timestamp as the container build time:

# use the date of the last commit as the build time
$ export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)
$ php bin/console cache:clear

If you set the kernel.container_build_time parameter explicitly, it still
takes precedence; otherwise Symfony falls back to SOURCE_DATE_EPOCH and then
to the current time.

Describe Object Shapes with TypeInfo


Benjamin Franzke
Contributed by
Benjamin Franzke
in
#62885

The TypeInfo component models PHP types as objects, allowing other components
(and your own code) to reason about them. Symfony 8.1 adds support for object shapes:
the exact structure of an object, described by its property names and types.

The StringTypeResolver now understands the object{...} PHPDoc syntax,
where a ? suffix marks an optional property:

use SymfonyComponentTypeInfoTypeResolverStringTypeResolver;

$resolver = new StringTypeResolver();

// resolves to an ObjectShapeType
$type = $resolver->resolve('object{name: string, age: int, email?: string}');

You can also build the same type programmatically with the new
Type::objectShape() factory:

use SymfonyComponentTypeInfoType;

$type = Type::objectShape([
    'name' => Type::string(),
    'age' => Type::int(),
    'email' => ['type' => Type::string(), 'optional' => true],
]);


Sponsor the Symfony project.
Tagged:

Leave a Reply

Your email address will not be published. Required fields are marked *