feature #33459 [Validator] Deprecated CacheInterface in favor of PSR-6 (derrabus)

This PR was merged into the 4.4 branch.

Discussion
----------

[Validator] Deprecated CacheInterface in favor of PSR-6

| Q             | A
| ------------- | ---
| Branch?       | 4.4
| Bug fix?      | no
| New feature?  | no
| BC breaks?    | no
| Deprecations? | yes
| Tests pass?   | yes
| Fixed tickets | #33414
| License       | MIT
| Doc PR        | TODO

With PSR-6, the validator component does not need its own `CacheInterface`, so let's remove it.

Note that I still use the now deprecated `Psr6Cache` class to keep the logic inside `LazyLoadingMetadataFactory` simple. My plan would be to inline all logic from `Psr6Cache` into `LazyLoadingMetadataFactory` for 5.0.

Commits
-------

0b08040459 [Validator] Deprecated CacheInterface in favor of PSR-6.
This commit is contained in:
Nicolas Grekas 2019-09-05 14:32:50 +02:00
commit eb0ffbc8a4
17 changed files with 163 additions and 49 deletions

View File

@ -313,6 +313,8 @@ Validator
Set it to `true` to keep the current behavior and `false` to reject empty strings.
In 5.0, it'll become optional and will default to `false`.
* Overriding the methods `ConstraintValidatorTestCase::setUp()` and `ConstraintValidatorTestCase::tearDown()` without the `void` return-type is deprecated.
* deprecated `Symfony\Component\Validator\Mapping\Cache\CacheInterface` and all implementations in favor of PSR-6.
* deprecated `ValidatorBuilder::setMetadataCache`, use `ValidatorBuilder::setMappingCache` instead.
WebProfilerBundle
-----------------

View File

@ -567,6 +567,8 @@ Validator
* The `symfony/expression-language` component is now required for using the `Expression` constraint
* Changed the default value of `Length::$allowEmptyString` to `false` and made it optional
* Added support for PHPUnit 8. A `void` return-type was added to the `ConstraintValidatorTestCase::setUp()` and `ConstraintValidatorTestCase::tearDown()` methods.
* The `Symfony\Component\Validator\Mapping\Cache\CacheInterface` and all its implementations have been removed.
* The `ValidatorBuilder::setMetadataCache` has been removed, use `ValidatorBuilder::setMappingCache` instead.
WebProfilerBundle
-----------------

View File

@ -15,7 +15,6 @@ use Doctrine\Common\Annotations\AnnotationException;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\Cache\Adapter\PhpArrayAdapter;
use Symfony\Component\Validator\Mapping\Cache\Psr6Cache;
use Symfony\Component\Validator\Mapping\Factory\LazyLoadingMetadataFactory;
use Symfony\Component\Validator\Mapping\Loader\LoaderChain;
use Symfony\Component\Validator\Mapping\Loader\LoaderInterface;
@ -59,7 +58,7 @@ class ValidatorCacheWarmer extends AbstractPhpFileCacheWarmer
}
$loaders = $this->validatorBuilder->getLoaders();
$metadataFactory = new LazyLoadingMetadataFactory(new LoaderChain($loaders), new Psr6Cache($arrayAdapter));
$metadataFactory = new LazyLoadingMetadataFactory(new LoaderChain($loaders), $arrayAdapter);
foreach ($this->extractSupportedLoaders($loaders) as $loader) {
foreach ($loader->getMappedClasses() as $mappedClass) {

View File

@ -1288,7 +1288,7 @@ class FrameworkExtension extends Extension
}
if (!$container->getParameter('kernel.debug')) {
$validatorBuilder->addMethodCall('setMetadataCache', [new Reference('validator.mapping.cache.symfony')]);
$validatorBuilder->addMethodCall('setMappingCache', [new Reference('validator.mapping.cache.adapter')]);
}
$container->setParameter('validator.auto_mapping', $config['auto_mapping']);

View File

@ -39,13 +39,14 @@
</service>
<service id="validator.mapping.cache.symfony" class="Symfony\Component\Validator\Mapping\Cache\Psr6Cache">
<argument type="service">
<service class="Symfony\Component\Cache\Adapter\PhpArrayAdapter">
<factory class="Symfony\Component\Cache\Adapter\PhpArrayAdapter" method="create" />
<argument>%validator.mapping.cache.file%</argument>
<argument type="service" id="cache.validator" />
</service>
</argument>
<argument type="service" id="validator.mapping.cache.adapter" />
<deprecated>The "%service_id%" service is deprecated since Symfony 4.4. Use validator.mapping.cache.adapter instead.</deprecated>
</service>
<service id="validator.mapping.cache.adapter" class="Symfony\Component\Cache\Adapter\PhpArrayAdapter">
<factory class="Symfony\Component\Cache\Adapter\PhpArrayAdapter" method="create" />
<argument>%validator.mapping.cache.file%</argument>
<argument type="service" id="cache.validator" />
</service>
<service id="validator.validator_factory" class="Symfony\Component\Validator\ContainerConstraintValidatorFactory">

View File

@ -908,8 +908,8 @@ abstract class FrameworkExtensionTest extends TestCase
}
$this->assertSame('addMethodMapping', $calls[++$i][0]);
$this->assertSame(['loadValidatorMetadata'], $calls[$i][1]);
$this->assertSame('setMetadataCache', $calls[++$i][0]);
$this->assertEquals([new Reference('validator.mapping.cache.symfony')], $calls[$i][1]);
$this->assertSame('setMappingCache', $calls[++$i][0]);
$this->assertEquals([new Reference('validator.mapping.cache.adapter')], $calls[$i][1]);
}
public function testValidationService()
@ -951,8 +951,8 @@ abstract class FrameworkExtensionTest extends TestCase
$this->assertEquals([new Reference('annotation_reader')], $calls[4][1]);
$this->assertSame('addMethodMapping', $calls[5][0]);
$this->assertSame(['loadValidatorMetadata'], $calls[5][1]);
$this->assertSame('setMetadataCache', $calls[6][0]);
$this->assertEquals([new Reference('validator.mapping.cache.symfony')], $calls[6][1]);
$this->assertSame('setMappingCache', $calls[6][0]);
$this->assertEquals([new Reference('validator.mapping.cache.adapter')], $calls[6][1]);
// no cache this time
}
@ -973,8 +973,8 @@ abstract class FrameworkExtensionTest extends TestCase
$this->assertSame('enableAnnotationMapping', $calls[5][0]);
$this->assertSame('addMethodMapping', $calls[6][0]);
$this->assertSame(['loadValidatorMetadata'], $calls[6][1]);
$this->assertSame('setMetadataCache', $calls[7][0]);
$this->assertEquals([new Reference('validator.mapping.cache.symfony')], $calls[7][1]);
$this->assertSame('setMappingCache', $calls[7][0]);
$this->assertEquals([new Reference('validator.mapping.cache.adapter')], $calls[7][1]);
$xmlMappings = $calls[3][1][0];
$this->assertCount(3, $xmlMappings);
@ -1033,8 +1033,8 @@ abstract class FrameworkExtensionTest extends TestCase
if ($annotations) {
$this->assertSame('enableAnnotationMapping', $calls[++$i][0]);
}
$this->assertSame('setMetadataCache', $calls[++$i][0]);
$this->assertEquals([new Reference('validator.mapping.cache.symfony')], $calls[$i][1]);
$this->assertSame('setMappingCache', $calls[++$i][0]);
$this->assertEquals([new Reference('validator.mapping.cache.adapter')], $calls[$i][1]);
// no cache, no annotations, no static methods
}

View File

@ -53,7 +53,7 @@
"symfony/translation": "^4.3|^5.0",
"symfony/templating": "^3.4|^4.0|^5.0",
"symfony/twig-bundle": "^4.4|^5.0",
"symfony/validator": "^4.1|^5.0",
"symfony/validator": "^4.4|^5.0",
"symfony/var-dumper": "^4.3|^5.0",
"symfony/workflow": "^4.3|^5.0",
"symfony/yaml": "^3.4|^4.0|^5.0",
@ -83,7 +83,7 @@
"symfony/translation": "<4.3",
"symfony/twig-bridge": "<4.1.1",
"symfony/twig-bundle": "<4.4",
"symfony/validator": "<4.1",
"symfony/validator": "<4.4",
"symfony/workflow": "<4.3"
},
"suggest": {

View File

@ -22,6 +22,8 @@ CHANGELOG
be used in the violation builder when both `min` and `max` are not null
* added ability to use stringable objects as violation messages
* Overriding the methods `ConstraintValidatorTestCase::setUp()` and `ConstraintValidatorTestCase::tearDown()` without the `void` return-type is deprecated.
* deprecated `Symfony\Component\Validator\Mapping\Cache\CacheInterface` in favor of PSR-6.
* deprecated `ValidatorBuilder::setMetadataCache`, use `ValidatorBuilder::setMappingCache` instead.
4.3.0
-----

View File

@ -13,10 +13,14 @@ namespace Symfony\Component\Validator\Mapping\Cache;
use Symfony\Component\Validator\Mapping\ClassMetadata;
@trigger_error(sprintf('The "%s" interface is deprecated since Symfony 4.4.', CacheInterface::class), E_USER_DEPRECATED);
/**
* Persists ClassMetadata instances in a cache.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @deprecated since Symfony 4.4.
*/
interface CacheInterface
{

View File

@ -14,10 +14,14 @@ namespace Symfony\Component\Validator\Mapping\Cache;
use Doctrine\Common\Cache\Cache;
use Symfony\Component\Validator\Mapping\ClassMetadata;
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4.', DoctrineCache::class), E_USER_DEPRECATED);
/**
* Adapts a Doctrine cache to a CacheInterface.
*
* @author Florian Voutzinos <florian@voutzinos.com>
*
* @deprecated since Symfony 4.4.
*/
final class DoctrineCache implements CacheInterface
{

View File

@ -14,10 +14,14 @@ namespace Symfony\Component\Validator\Mapping\Cache;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Validator\Mapping\ClassMetadata;
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4.', Psr6Cache::class), E_USER_DEPRECATED);
/**
* PSR-6 adapter.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*
* @deprecated since Symfony 4.4.
*/
class Psr6Cache implements CacheInterface
{

View File

@ -11,6 +11,7 @@
namespace Symfony\Component\Validator\Mapping\Factory;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Validator\Exception\NoSuchMetadataException;
use Symfony\Component\Validator\Mapping\Cache\CacheInterface;
use Symfony\Component\Validator\Mapping\ClassMetadata;
@ -51,12 +52,17 @@ class LazyLoadingMetadataFactory implements MetadataFactoryInterface
/**
* Creates a new metadata factory.
*
* @param LoaderInterface|null $loader The loader for configuring new metadata
* @param CacheInterface|null $cache The cache for persisting metadata
* between multiple PHP requests
* @param CacheItemPoolInterface|null $cache The cache for persisting metadata
* between multiple PHP requests
*/
public function __construct(LoaderInterface $loader = null, CacheInterface $cache = null)
public function __construct(LoaderInterface $loader = null, $cache = null)
{
if ($cache instanceof CacheInterface) {
@trigger_error(sprintf('Passing a "%s" to "%s" is deprecated in Symfony 4.4 and will trigger a TypeError in 5.0. Please pass an implementation of "%s" instead.', \get_class($cache), __METHOD__, CacheItemPoolInterface::class), E_USER_DEPRECATED);
} elseif (!$cache instanceof CacheItemPoolInterface && null !== $cache) {
throw new \TypeError(sprintf('Expected an instance of %s, got %s.', CacheItemPoolInterface::class, \is_object($cache) ? \get_class($cache) : \gettype($cache)));
}
$this->loader = $loader;
$this->cache = $cache;
}
@ -92,11 +98,24 @@ class LazyLoadingMetadataFactory implements MetadataFactoryInterface
throw new NoSuchMetadataException(sprintf('The class or interface "%s" does not exist.', $class));
}
if (null !== $this->cache && false !== ($metadata = $this->cache->read($class))) {
// Include constraints from the parent class
$this->mergeConstraints($metadata);
$cacheItem = null;
if ($this->cache instanceof CacheInterface) {
if ($metadata = $this->cache->read($class)) {
// Include constraints from the parent class
$this->mergeConstraints($metadata);
return $this->loadedClasses[$class] = $metadata;
return $this->loadedClasses[$class] = $metadata;
}
} elseif (null !== $this->cache) {
$cacheItem = $this->cache->getItem($this->escapeClassName($class));
if ($cacheItem->isHit()) {
$metadata = $cacheItem->get();
// Include constraints from the parent class
$this->mergeConstraints($metadata);
return $this->loadedClasses[$class] = $metadata;
}
}
$metadata = new ClassMetadata($class);
@ -105,8 +124,10 @@ class LazyLoadingMetadataFactory implements MetadataFactoryInterface
$this->loader->loadClassMetadata($metadata);
}
if (null !== $this->cache) {
if ($this->cache instanceof CacheInterface) {
$this->cache->write($metadata);
} elseif (null !== $cacheItem) {
$this->cache->save($cacheItem->set($metadata));
}
// Include constraints from the parent class
@ -162,4 +183,17 @@ class LazyLoadingMetadataFactory implements MetadataFactoryInterface
return class_exists($class) || interface_exists($class, false);
}
/**
* Replaces backslashes by dots in a class name.
*/
private function escapeClassName(string $class): string
{
if (false !== strpos($class, '@')) {
// anonymous class: replace all PSR6-reserved characters
return str_replace(["\0", '\\', '/', '@', ':', '{', '}', '(', ')'], '.', $class);
}
return str_replace('\\', '.', $class);
}
}

View File

@ -14,6 +14,9 @@ namespace Symfony\Component\Validator\Tests\Mapping\Cache;
use Doctrine\Common\Cache\ArrayCache;
use Symfony\Component\Validator\Mapping\Cache\DoctrineCache;
/**
* @group legacy
*/
class DoctrineCacheTest extends AbstractCacheTest
{
protected function setUp(): void

View File

@ -8,6 +8,8 @@ use Symfony\Component\Validator\Mapping\ClassMetadata;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*
* @group legacy
*/
class Psr6CacheTest extends AbstractCacheTest
{

View File

@ -12,6 +12,8 @@
namespace Symfony\Component\Validator\Tests\Mapping\Factory;
use PHPUnit\Framework\TestCase;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\Validator\Constraints\Callback;
use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Validator\Mapping\Factory\LazyLoadingMetadataFactory;
@ -76,7 +78,36 @@ class LazyLoadingMetadataFactoryTest extends TestCase
$this->assertEquals($constraints, $metadata->getConstraints());
}
public function testWriteMetadataToCache()
public function testCachedMetadata()
{
$cache = new ArrayAdapter();
$factory = new LazyLoadingMetadataFactory(new TestLoader(), $cache);
$expectedConstraints = [
new ConstraintA(['groups' => ['Default', 'EntityParent']]),
new ConstraintA(['groups' => ['Default', 'EntityInterfaceA', 'EntityParent']]),
];
$metadata = $factory->getMetadataFor(self::PARENT_CLASS);
$this->assertEquals(self::PARENT_CLASS, $metadata->getClassName());
$this->assertEquals($expectedConstraints, $metadata->getConstraints());
$loader = $this->createMock(LoaderInterface::class);
$loader->expects($this->never())->method('loadClassMetadata');
$factory = new LazyLoadingMetadataFactory($loader, $cache);
$metadata = $factory->getMetadataFor(self::PARENT_CLASS);
$this->assertEquals(self::PARENT_CLASS, $metadata->getClassName());
$this->assertEquals($expectedConstraints, $metadata->getConstraints());
}
/**
* @group legacy
*/
public function testWriteMetadataToLegacyCache()
{
$cache = $this->getMockBuilder('Symfony\Component\Validator\Mapping\Cache\CacheInterface')->getMock();
$factory = new LazyLoadingMetadataFactory(new TestLoader(), $cache);
@ -115,7 +146,10 @@ class LazyLoadingMetadataFactoryTest extends TestCase
$this->assertEquals($parentClassConstraints, $metadata->getConstraints());
}
public function testReadMetadataFromCache()
/**
* @group legacy
*/
public function testReadMetadataFromLegacyCache()
{
$loader = $this->getMockBuilder('Symfony\Component\Validator\Mapping\Loader\LoaderInterface')->getMock();
$cache = $this->getMockBuilder('Symfony\Component\Validator\Mapping\Cache\CacheInterface')->getMock();
@ -154,29 +188,19 @@ class LazyLoadingMetadataFactoryTest extends TestCase
$this->expectException('Symfony\Component\Validator\Exception\NoSuchMetadataException');
$testedValue = 'error@example.com';
$loader = $this->getMockBuilder('Symfony\Component\Validator\Mapping\Loader\LoaderInterface')->getMock();
$cache = $this->getMockBuilder('Symfony\Component\Validator\Mapping\Cache\CacheInterface')->getMock();
$cache = $this->createMock(CacheItemPoolInterface::class);
$factory = new LazyLoadingMetadataFactory($loader, $cache);
$cache
->expects($this->never())
->method('read');
->method('getItem');
$factory->getMetadataFor($testedValue);
}
public function testMetadataCacheWithRuntimeConstraint()
{
$cache = $this->getMockBuilder('Symfony\Component\Validator\Mapping\Cache\CacheInterface')->getMock();
$cache = new ArrayAdapter();
$factory = new LazyLoadingMetadataFactory(new TestLoader(), $cache);
$cache
->expects($this->any())
->method('write')
->willReturnCallback(function ($metadata) { serialize($metadata); })
;
$cache->expects($this->any())
->method('read')
->willReturn(false);
$metadata = $factory->getMetadataFor(self::PARENT_CLASS);
$metadata->addConstraint(new Callback(function () {}));

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\Validator\Tests;
use PHPUnit\Framework\TestCase;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Validator\Util\LegacyTranslatorProxy;
use Symfony\Component\Validator\ValidatorBuilder;
use Symfony\Component\Validator\ValidatorBuilderInterface;
@ -85,6 +86,15 @@ class ValidatorBuilderTest extends TestCase
$this->assertSame($this->builder, $this->builder->disableAnnotationMapping());
}
public function testSetMappingCache()
{
$this->assertSame($this->builder, $this->builder->setMappingCache($this->createMock(CacheItemPoolInterface::class)));
}
/**
* @group legacy
* @expectedDeprecation Symfony\Component\Validator\ValidatorBuilder::setMetadataCache is deprecated since Symfony 4.4. Use setMappingCache() instead.
*/
public function testSetMetadataCache()
{
$this->assertSame($this->builder, $this->builder->setMetadataCache(

View File

@ -15,6 +15,7 @@ use Doctrine\Common\Annotations\AnnotationReader;
use Doctrine\Common\Annotations\CachedReader;
use Doctrine\Common\Annotations\Reader;
use Doctrine\Common\Cache\ArrayCache;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Translation\TranslatorInterface as LegacyTranslatorInterface;
use Symfony\Component\Validator\Context\ExecutionContextFactory;
use Symfony\Component\Validator\Exception\LogicException;
@ -63,9 +64,9 @@ class ValidatorBuilder implements ValidatorBuilderInterface
private $validatorFactory;
/**
* @var CacheInterface|null
* @var CacheItemPoolInterface|null
*/
private $metadataCache;
private $mappingCache;
/**
* @var TranslatorInterface|null
@ -228,15 +229,37 @@ class ValidatorBuilder implements ValidatorBuilderInterface
}
/**
* {@inheritdoc}
* Sets the cache for caching class metadata.
*
* @return $this
*
* @deprecated since Symfony 4.4.
*/
public function setMetadataCache(CacheInterface $cache)
{
@trigger_error(sprintf('%s is deprecated since Symfony 4.4. Use setMappingCache() instead.', __METHOD__), E_USER_DEPRECATED);
if (null !== $this->metadataFactory) {
throw new ValidatorException('You cannot set a custom metadata cache after setting a custom metadata factory. Configure your metadata factory instead.');
}
$this->metadataCache = $cache;
$this->mappingCache = $cache;
return $this;
}
/**
* Sets the cache for caching class metadata.
*
* @return $this
*/
public function setMappingCache(CacheItemPoolInterface $cache)
{
$this->mappingCache = $cache;
if (null !== $this->metadataFactory) {
throw new ValidatorException('You cannot set a custom mapping cache after setting a custom metadata factory. Configure your metadata factory instead.');
}
return $this;
}
@ -330,7 +353,7 @@ class ValidatorBuilder implements ValidatorBuilderInterface
$loader = $loaders[0];
}
$metadataFactory = new LazyLoadingMetadataFactory($loader, $this->metadataCache);
$metadataFactory = new LazyLoadingMetadataFactory($loader, $this->mappingCache);
}
$validatorFactory = $this->validatorFactory ?: new ConstraintValidatorFactory();