diff --git a/src/Symfony/Component/Serializer/Mapping/Factory/CacheClassMetadataFactory.php b/src/Symfony/Component/Serializer/Mapping/Factory/CacheClassMetadataFactory.php new file mode 100644 index 0000000000000..0b904c14400d0 --- /dev/null +++ b/src/Symfony/Component/Serializer/Mapping/Factory/CacheClassMetadataFactory.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Mapping\Factory; + +use Psr\Cache\CacheItemPoolInterface; + +/** + * Caches metadata using a PSR-6 implementation. + * + * @author Kévin Dunglas + */ +class CacheClassMetadataFactory implements ClassMetadataFactoryInterface +{ + use ClassResolverTrait; + + /** + * @var ClassMetadataFactoryInterface + */ + private $decorated; + + /** + * @var CacheItemPoolInterface + */ + private $cacheItemPool; + + public function __construct(ClassMetadataFactoryInterface $decorated, CacheItemPoolInterface $cacheItemPool) + { + $this->decorated = $decorated; + $this->cacheItemPool = $cacheItemPool; + } + + /** + * {@inheritdoc} + */ + public function getMetadataFor($value) + { + $class = $this->getClass($value); + // Key cannot contain backslashes according to PSR-6 + $key = strtr($class, '\\', '_'); + + $item = $this->cacheItemPool->getItem($key); + if ($item->isHit()) { + return $item->get(); + } + + $metadata = $this->decorated->getMetadataFor($value); + $this->cacheItemPool->save($item->set($metadata)); + + return $metadata; + } + + /** + * {@inheritdoc} + */ + public function hasMetadataFor($value) + { + return $this->decorated->hasMetadataFor($value); + } +} diff --git a/src/Symfony/Component/Serializer/Mapping/Factory/ClassMetadataFactory.php b/src/Symfony/Component/Serializer/Mapping/Factory/ClassMetadataFactory.php index 3a354e354e9e3..00e214fe03d89 100644 --- a/src/Symfony/Component/Serializer/Mapping/Factory/ClassMetadataFactory.php +++ b/src/Symfony/Component/Serializer/Mapping/Factory/ClassMetadataFactory.php @@ -23,6 +23,8 @@ */ class ClassMetadataFactory implements ClassMetadataFactoryInterface { + use ClassResolverTrait; + /** * @var LoaderInterface */ @@ -44,6 +46,10 @@ public function __construct(LoaderInterface $loader, Cache $cache = null) { $this->loader = $loader; $this->cache = $cache; + + if (null !== $cache) { + @trigger_error(sprintf('Passing a Doctrine Cache instance as 2nd parameter of the "%s" constructor is deprecated. This parameter will be removed in Symfony 4.0. Use the "%s" class instead.', __CLASS__, CacheClassMetadataFactory::class), E_USER_DEPRECATED); + } } /** @@ -52,9 +58,6 @@ public function __construct(LoaderInterface $loader, Cache $cache = null) public function getMetadataFor($value) { $class = $this->getClass($value); - if (!$class) { - throw new InvalidArgumentException(sprintf('Cannot create metadata for non-objects. Got: "%s"', gettype($value))); - } if (isset($this->loadedClasses[$class])) { return $this->loadedClasses[$class]; @@ -64,10 +67,6 @@ public function getMetadataFor($value) return $this->loadedClasses[$class]; } - if (!class_exists($class) && !interface_exists($class)) { - throw new InvalidArgumentException(sprintf('The class or interface "%s" does not exist.', $class)); - } - $classMetadata = new ClassMetadata($class); $this->loader->loadClassMetadata($classMetadata); @@ -95,24 +94,14 @@ public function getMetadataFor($value) */ public function hasMetadataFor($value) { - $class = $this->getClass($value); - - return class_exists($class) || interface_exists($class); - } + try { + $this->getClass($value); - /** - * Gets a class name for a given class or instance. - * - * @param mixed $value - * - * @return string|bool - */ - private function getClass($value) - { - if (!is_object($value) && !is_string($value)) { - return false; + return true; + } catch (InvalidArgumentException $invalidArgumentException) { + // Return false in case of exception } - return ltrim(is_object($value) ? get_class($value) : $value, '\\'); + return false; } } diff --git a/src/Symfony/Component/Serializer/Mapping/Factory/ClassResolverTrait.php b/src/Symfony/Component/Serializer/Mapping/Factory/ClassResolverTrait.php new file mode 100644 index 0000000000000..e93277a6be765 --- /dev/null +++ b/src/Symfony/Component/Serializer/Mapping/Factory/ClassResolverTrait.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Mapping\Factory; + +use Symfony\Component\Serializer\Exception\InvalidArgumentException; + +/** + * Resolves a class name. + * + * @internal + * + * @author Kévin Dunglas + */ +trait ClassResolverTrait +{ + /** + * Gets a class name for a given class or instance. + * + * @param mixed $value + * + * @return string + * + * @throws InvalidArgumentException If the class does not exists + */ + private function getClass($value) + { + if (is_string($value)) { + if (!class_exists($value) && !interface_exists($value)) { + throw new InvalidArgumentException(sprintf('The class or interface "%s" does not exist.', $value)); + } + + return ltrim($value, '\\'); + } + + if (!is_object($value)) { + throw new InvalidArgumentException(sprintf('Cannot create metadata for non-objects. Got: "%s"', gettype($value))); + } + + return get_class($value); + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Mapping/Factory/CacheMetadataFactoryTest.php b/src/Symfony/Component/Serializer/Tests/Mapping/Factory/CacheMetadataFactoryTest.php new file mode 100644 index 0000000000000..a42003f358906 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Mapping/Factory/CacheMetadataFactoryTest.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Tests\Mapping\Factory; + +use Symfony\Component\Cache\Adapter\ArrayAdapter; +use Symfony\Component\Serializer\Mapping\ClassMetadata; +use Symfony\Component\Serializer\Mapping\Factory\CacheClassMetadataFactory; + +/** + * @author Kévin Dunglas + */ +class CacheMetadataFactoryTest extends \PHPUnit_Framework_TestCase +{ + public function testGetMetadataFor() + { + $metadata = new ClassMetadata('Symfony\Component\Serializer\Tests\Fixtures\Dummy'); + + $decorated = $this->getMock('Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface'); + $decorated + ->expects($this->once()) + ->method('getMetadataFor') + ->will($this->returnValue($metadata)) + ; + + $factory = new CacheClassMetadataFactory($decorated, new ArrayAdapter()); + + $this->assertEquals($metadata, $factory->getMetadataFor('Symfony\Component\Serializer\Tests\Fixtures\Dummy')); + // The second call should retrieve the value from the cache + $this->assertEquals($metadata, $factory->getMetadataFor('Symfony\Component\Serializer\Tests\Fixtures\Dummy')); + } + + public function testHasMetadataFor() + { + $decorated = $this->getMock('Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface'); + $decorated + ->expects($this->once()) + ->method('hasMetadataFor') + ->will($this->returnValue(true)) + ; + + $factory = new CacheClassMetadataFactory($decorated, new ArrayAdapter()); + + $this->assertTrue($factory->hasMetadataFor('Symfony\Component\Serializer\Tests\Fixtures\Dummy')); + } + + /** + * @expectedException \Symfony\Component\Serializer\Exception\InvalidArgumentException + */ + public function testInvalidClassThrowsException() + { + $decorated = $this->getMock('Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface'); + $factory = new CacheClassMetadataFactory($decorated, new ArrayAdapter()); + + $factory->getMetadataFor('Not\Exist'); + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Mapping/Factory/ClassMetadataFactoryTest.php b/src/Symfony/Component/Serializer/Tests/Mapping/Factory/ClassMetadataFactoryTest.php index 2e2ba22dcee0b..a237c32313b12 100644 --- a/src/Symfony/Component/Serializer/Tests/Mapping/Factory/ClassMetadataFactoryTest.php +++ b/src/Symfony/Component/Serializer/Tests/Mapping/Factory/ClassMetadataFactoryTest.php @@ -25,7 +25,7 @@ class ClassMetadataFactoryTest extends \PHPUnit_Framework_TestCase public function testInterface() { $classMetadata = new ClassMetadataFactory(new LoaderChain(array())); - $this->assertInstanceOf('Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory', $classMetadata); + $this->assertInstanceOf('Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface', $classMetadata); } public function testGetMetadataFor() @@ -45,6 +45,9 @@ public function testHasMetadataFor() $this->assertFalse($factory->hasMetadataFor('Dunglas\Entity')); } + /** + * @group legacy + */ public function testCacheExists() { $cache = $this->getMock('Doctrine\Common\Cache\Cache'); @@ -58,17 +61,14 @@ public function testCacheExists() $this->assertEquals('foo', $factory->getMetadataFor('Symfony\Component\Serializer\Tests\Fixtures\GroupDummy')); } + /** + * @group legacy + */ public function testCacheNotExists() { $cache = $this->getMock('Doctrine\Common\Cache\Cache'); - $cache - ->method('fetch') - ->will($this->returnValue(false)) - ; - - $cache - ->method('save') - ; + $cache->method('fetch')->will($this->returnValue(false)); + $cache->method('save'); $factory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()), $cache); $metadata = $factory->getMetadataFor('Symfony\Component\Serializer\Tests\Fixtures\GroupDummy'); diff --git a/src/Symfony/Component/Serializer/composer.json b/src/Symfony/Component/Serializer/composer.json index acec92f24eb21..f17da4d2aafc0 100644 --- a/src/Symfony/Component/Serializer/composer.json +++ b/src/Symfony/Component/Serializer/composer.json @@ -23,16 +23,18 @@ "symfony/config": "~2.8|~3.0", "symfony/property-access": "~2.8|~3.0", "symfony/http-foundation": "~2.8|~3.0", + "symfony/cache": "~3.1", "doctrine/annotations": "~1.0", "doctrine/cache": "~1.0" }, "suggest": { - "doctrine/annotations": "For using the annotation mapping. You will also need doctrine/cache.", - "doctrine/cache": "For using the default cached annotation reader and metadata cache.", + "psr/cache-implementation": "For using the metadata cache.", "symfony/yaml": "For using the default YAML mapping loader.", "symfony/config": "For using the XML mapping loader.", "symfony/property-access": "For using the ObjectNormalizer.", - "symfony/http-foundation": "To use the DataUriNormalizer." + "symfony/http-foundation": "To use the DataUriNormalizer.", + "doctrine/annotations": "For using the annotation mapping. You will also need doctrine/cache.", + "doctrine/cache": "For using the default cached annotation reader and metadata cache." }, "autoload": { "psr-4": { "Symfony\\Component\\Serializer\\": "" },