Merge branch '8.0' into 8.1

* 8.0:
  [Config][FrameworkBundle] Allow using ParamConfigurator with every configurable value
  [HttpFoundation] Improve doc blocks in `ParameterBag`
  [HttpClient] Fix ever growing $maxHostConnections
  Fix typo
  [DependencyInjection] Fix referencing build-time array parameters
  cs fix
  [FrameworkBundle] Fix cache:pool:prune exit code on failure
  [Form] Add type hint for FormTypeInterface in FormBuilderInterface
  [Form] Always normalize CRLF and CR to LF in `TextareaType`
  [Cache] Fix stampede protection when forcing item recomputation
  [DoctrineBridge] Fix checking for the session table when using PDO
  fix(messenger): allow signing message without routing definition
  [Console] Fix EofShortcut instruction when using a modern terminal on Windows
  [Console] Do not call non-static method via class-name
  [Console] Fix choice autocomplete issue when string has spaces
  Update SameOriginCsrfTokenManager.php
  [Serializer] Fix inconsistent field naming from accessors when using groups
  [Finder] Fix converting unanchored glob patterns to regex
This commit is contained in:
Nicolas Grekas
2025-12-23 15:52:15 +01:00
6 changed files with 83 additions and 24 deletions

View File

@@ -18,6 +18,7 @@ use Symfony\Component\Cache\Adapter\NullAdapter;
use Symfony\Component\Cache\Adapter\ParameterNormalizer;
use Symfony\Component\Cache\Adapter\TagAwareAdapter;
use Symfony\Component\Cache\Messenger\EarlyExpirationDispatcher;
use Symfony\Component\Cache\PruneableInterface;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
@@ -49,6 +50,7 @@ class CachePoolPass implements CompilerPassInterface
'default_lifetime',
'early_expiration_message_bus',
'reset',
'pruneable',
];
foreach ($container->findTaggedServiceIds('cache.pool') as $id => $tags) {
$adapter = $pool = $container->getDefinition($id);
@@ -88,6 +90,8 @@ class CachePoolPass implements CompilerPassInterface
$tags[0]['provider'] = new Reference(static::getServiceProvider($container, $tags[0]['provider']));
}
$pruneable = $tags[0]['pruneable'] ?? $container->getReflectionClass($class, false)?->implementsInterface(PruneableInterface::class) ?? false;
if (ChainAdapter::class === $class) {
$adapters = [];
foreach ($providers['index_0'] ?? $providers[0] as $provider => $adapter) {
@@ -154,6 +158,8 @@ class CachePoolPass implements CompilerPassInterface
),
]);
$pool->addTag('container.reversible');
} elseif ('pruneable' === $attr) {
// no-op
} elseif ('namespace' !== $attr || !\in_array($class, [ArrayAdapter::class, NullAdapter::class, TagAwareAdapter::class], true)) {
$argument = $tags[0][$attr];
@@ -167,13 +173,17 @@ class CachePoolPass implements CompilerPassInterface
unset($tags[0][$attr]);
}
if (!empty($tags[0])) {
throw new InvalidArgumentException(\sprintf('Invalid "cache.pool" tag for service "%s": accepted attributes are "clearer", "provider", "name", "namespace", "default_lifetime", "early_expiration_message_bus" and "reset", found "%s".', $id, implode('", "', array_keys($tags[0]))));
throw new InvalidArgumentException(\sprintf('Invalid "cache.pool" tag for service "%s": accepted attributes are "clearer", "provider", "name", "namespace", "default_lifetime", "early_expiration_message_bus", "reset" and "pruneable", found "%s".', $id, implode('", "', array_keys($tags[0]))));
}
if (null !== $clearer) {
$clearers[$clearer][$name] = new Reference($id, $container::IGNORE_ON_UNINITIALIZED_REFERENCE);
}
$poolTags = $pool->getTags();
$poolTags['cache.pool'][0]['pruneable'] ??= $pruneable;
$pool->setTags($poolTags);
$allPools[$name] = new Reference($id, $container::IGNORE_ON_UNINITIALIZED_REFERENCE);
}

View File

@@ -15,7 +15,6 @@ use Symfony\Component\Cache\PruneableInterface;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Reference;
/**
@@ -32,14 +31,8 @@ class CachePoolPrunerPass implements CompilerPassInterface
$services = [];
foreach ($container->findTaggedServiceIds('cache.pool') as $id => $tags) {
$class = $container->getParameterBag()->resolveValue($container->getDefinition($id)->getClass());
if (!$reflection = $container->getReflectionClass($class)) {
throw new InvalidArgumentException(\sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id));
}
if ($reflection->implementsInterface(PruneableInterface::class)) {
$services[$id] = new Reference($id);
if ($tags[0]['pruneable'] ?? $container->getReflectionClass($container->getDefinition($id)->getClass(), false)?->implementsInterface(PruneableInterface::class) ?? false) {
$services[$tags[0]['name'] ?? $id] = new Reference($id);
}
}

View File

@@ -82,7 +82,7 @@ final class LockRegistry
return $previousFiles;
}
public static function compute(callable $callback, ItemInterface $item, bool &$save, CacheInterface $pool, ?\Closure $setMetadata = null, ?LoggerInterface $logger = null): mixed
public static function compute(callable $callback, ItemInterface $item, bool &$save, CacheInterface $pool, ?\Closure $setMetadata = null, ?LoggerInterface $logger = null, ?float $beta = null): mixed
{
if ('\\' === \DIRECTORY_SEPARATOR && null === self::$lockedFiles) {
// disable locking on Windows by default
@@ -123,6 +123,11 @@ final class LockRegistry
// if we failed the race, retry locking in blocking mode to wait for the winner
$logger?->info('Item "{key}" is locked, waiting for it to be released', ['key' => $item->getKey()]);
flock($lock, \LOCK_SH);
if (\INF === $beta) {
$logger?->info('Force-recomputing item "{key}"', ['key' => $item->getKey()]);
continue;
}
} finally {
flock($lock, \LOCK_UN);
unset(self::$lockedFiles[$key]);

View File

@@ -33,13 +33,13 @@ class EarlyExpirationDispatcher
$this->callbackWrapper = null === $callbackWrapper ? null : $callbackWrapper(...);
}
public function __invoke(callable $callback, CacheItem $item, bool &$save, AdapterInterface $pool, \Closure $setMetadata, ?LoggerInterface $logger = null): mixed
public function __invoke(callable $callback, CacheItem $item, bool &$save, AdapterInterface $pool, \Closure $setMetadata, ?LoggerInterface $logger = null, ?float $beta = null): mixed
{
if (!$item->isHit() || null === $message = EarlyExpirationMessage::create($this->reverseContainer, $callback, $item, $pool)) {
// The item is stale or the callback cannot be reversed: we must compute the value now
$logger?->info('Computing item "{key}" online: '.($item->isHit() ? 'callback cannot be reversed' : 'item is stale'), ['key' => $item->getKey()]);
return null !== $this->callbackWrapper ? ($this->callbackWrapper)($callback, $item, $save, $pool, $setMetadata, $logger) : $callback($item, $save);
return null !== $this->callbackWrapper ? ($this->callbackWrapper)($callback, $item, $save, $pool, $setMetadata, $logger, $beta) : $callback($item, $save);
}
$envelope = $this->bus->dispatch($message);

View File

@@ -12,12 +12,14 @@
namespace Symfony\Component\Cache\Tests\DependencyInjection;
use PHPUnit\Framework\TestCase;
use Symfony\Bundle\FrameworkBundle\Command\CachePoolPruneCommand;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Symfony\Component\Cache\Adapter\PhpFilesAdapter;
use Symfony\Component\Cache\DependencyInjection\CachePoolPass;
use Symfony\Component\Cache\DependencyInjection\CachePoolPrunerPass;
use Symfony\Component\Cache\PruneableInterface;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Reference;
class CachePoolPrunerPassTest extends TestCase
@@ -57,17 +59,66 @@ class CachePoolPrunerPassTest extends TestCase
$this->assertCount($aliasesBefore, $container->getAliases());
}
public function testCompilerPassThrowsOnInvalidDefinitionClass()
public function testNonPruneablePoolsAreNotAdded()
{
$container = new ContainerBuilder();
$container->register('console.command.cache_pool_prune')->addArgument([]);
$container->register('pool.not-found', NotFound::class)->addTag('cache.pool');
$container->setParameter('kernel.debug', false);
$container->setParameter('kernel.project_dir', __DIR__);
$container->setParameter('kernel.container_class', 'TestContainer');
$pass = new CachePoolPrunerPass();
$container->register('console.command.cache_pool_prune', CachePoolPruneCommand::class)
->setArguments([new IteratorArgument([])]);
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Class "Symfony\Component\Cache\Tests\DependencyInjection\NotFound" used for service "pool.not-found" cannot be found.');
$container->register('cache.null', NonPruneableAdapter::class)
->setArguments([null])
->addTag('cache.pool');
$pass->process($container);
$container->register('cache.fs', PruneableAdapter::class)
->setArguments([null])
->addTag('cache.pool');
(new CachePoolPass())->process($container);
(new CachePoolPrunerPass())->process($container);
$arg = $container->getDefinition('console.command.cache_pool_prune')->getArgument(0);
$values = $arg->getValues();
$this->assertArrayNotHasKey('cache.null', $values);
$this->assertArrayHasKey('cache.fs', $values);
}
public function testPruneableAttributeOverridesInterfaceCheck()
{
$container = new ContainerBuilder();
$container->setParameter('kernel.debug', false);
$container->setParameter('kernel.project_dir', __DIR__);
$container->setParameter('kernel.container_class', 'TestContainer');
$container->register('console.command.cache_pool_prune', 'stdClass')
->setArguments([new IteratorArgument([])]);
$container->register('manual.pool', NonPruneableAdapter::class)
->setArguments([null])
->addTag('cache.pool', ['pruneable' => true]);
(new CachePoolPass())->process($container);
(new CachePoolPrunerPass())->process($container);
$arg = $container->getDefinition('console.command.cache_pool_prune')->getArgument(0);
$values = $arg->getValues();
$this->assertArrayHasKey('manual.pool', $values);
}
}
class PruneableAdapter implements PruneableInterface
{
public function prune(): bool
{
return true;
}
}
class NonPruneableAdapter
{
}

View File

@@ -54,7 +54,7 @@ trait ContractsTrait
}
$previousWrapper = $this->callbackWrapper;
$this->callbackWrapper = $callbackWrapper ?? static fn (callable $callback, ItemInterface $item, bool &$save, CacheInterface $pool, \Closure $setMetadata, ?LoggerInterface $logger) => $callback($item, $save);
$this->callbackWrapper = $callbackWrapper ?? static fn (callable $callback, ItemInterface $item, bool &$save, CacheInterface $pool, \Closure $setMetadata, ?LoggerInterface $logger, ?float $beta = null) => $callback($item, $save);
return $previousWrapper;
}
@@ -82,7 +82,7 @@ trait ContractsTrait
$this->callbackWrapper ??= LockRegistry::compute(...);
return $this->contractsGet($pool, $key, function (CacheItem $item, bool &$save) use ($pool, $callback, $setMetadata, &$metadata, $key) {
return $this->contractsGet($pool, $key, function (CacheItem $item, bool &$save) use ($pool, $callback, $setMetadata, &$metadata, $key, $beta) {
// don't wrap nor save recursive calls
if (isset($this->computing[$key])) {
$value = $callback($item, $save);
@@ -101,7 +101,7 @@ trait ContractsTrait
try {
$value = ($this->callbackWrapper)($callback, $item, $save, $pool, static function (CacheItem $item) use ($setMetadata, $startTime, &$metadata) {
$setMetadata($item, $startTime, $metadata);
}, $this->logger ?? null);
}, $this->logger ?? null, $beta);
$setMetadata($item, $startTime, $metadata);
return $value;