diff --git a/src/Symfony/Component/PropertyInfo/PropertyInfoCacheExtractor.php b/src/Symfony/Component/PropertyInfo/PropertyInfoCacheExtractor.php new file mode 100644 index 0000000000000..b7d5c6d372ca6 --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/PropertyInfoCacheExtractor.php @@ -0,0 +1,152 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo; + +use Psr\Cache\CacheItemPoolInterface; + +/** + * Adds a PSR-6 cache layer on top of an extractor. + * + * @author Kévin Dunglas + */ +class PropertyInfoCacheExtractor implements PropertyInfoExtractorInterface +{ + /** + * @var PropertyInfoExtractorInterface + */ + private $propertyInfoExtractor; + + /** + * @var CacheItemPoolInterface + */ + private $cacheItemPool; + + /** + * @var array + */ + private $arrayCache = array(); + + public function __construct(PropertyInfoExtractorInterface $propertyInfoExtractor, CacheItemPoolInterface $cacheItemPool) + { + $this->propertyInfoExtractor = $propertyInfoExtractor; + $this->cacheItemPool = $cacheItemPool; + } + + /** + * {@inheritdoc} + */ + public function isReadable($class, $property, array $context = array()) + { + return $this->extract('isReadable', array($class, $property, $context)); + } + + /** + * {@inheritdoc} + */ + public function isWritable($class, $property, array $context = array()) + { + return $this->extract('isWritable', array($class, $property, $context)); + } + + /** + * {@inheritdoc} + */ + public function getShortDescription($class, $property, array $context = array()) + { + return $this->extract('getShortDescription', array($class, $property, $context)); + } + + /** + * {@inheritdoc} + */ + public function getLongDescription($class, $property, array $context = array()) + { + return $this->extract('getLongDescription', array($class, $property, $context)); + } + + /** + * {@inheritdoc} + */ + public function getProperties($class, array $context = array()) + { + return $this->extract('getProperties', array($class, $context)); + } + + /** + * {@inheritdoc} + */ + public function getTypes($class, $property, array $context = array()) + { + return $this->extract('getTypes', array($class, $context)); + } + + /** + * Retrieves the cached data if applicable or delegates to the decorated extractor. + * + * @param string $method + * @param array $arguments + * + * @return mixed + */ + private function extract($method, array $arguments) + { + try { + $serializedArguments = serialize($arguments); + } catch (\Exception $exception) { + // If arguments are not serializable, skip the cache + return call_user_func_array(array($this->propertyInfoExtractor, $method), $arguments); + } + + $key = $this->escape($method.'.'.$serializedArguments); + + if (isset($this->arrayCache[$key])) { + return $this->arrayCache[$key]; + } + + $item = $this->cacheItemPool->getItem($key); + + if ($item->isHit()) { + return $this->arrayCache[$key] = $item->get(); + } + + $value = call_user_func_array(array($this->propertyInfoExtractor, $method), $arguments); + $item->set($value); + $this->cacheItemPool->save($item); + + return $this->arrayCache[$key] = $value; + } + + /** + * Escapes a key according to PSR-6. + * + * Replaces characters forbidden by PSR-6 and the _ char by the _ char followed by the ASCII + * code of the escaped char. + * + * @param string $key + * + * @return string + */ + private function escape($key) + { + return strtr($key, array( + '{' => '_123', + '}' => '_125', + '(' => '_40', + ')' => '_41', + '/' => '_47', + '\\' => '_92', + '@' => '_64', + ':' => '_58', + '_' => '_95', + )); + } +} diff --git a/src/Symfony/Component/PropertyInfo/Tests/AbstractPropertyInfoExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/AbstractPropertyInfoExtractorTest.php new file mode 100644 index 0000000000000..dd65e308781eb --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/Tests/AbstractPropertyInfoExtractorTest.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo\Tests; + +use Symfony\Component\PropertyInfo\PropertyInfoExtractor; +use Symfony\Component\PropertyInfo\Tests\Fixtures\DummyExtractor; +use Symfony\Component\PropertyInfo\Tests\Fixtures\NullExtractor; +use Symfony\Component\PropertyInfo\Type; + +/** + * @author Kévin Dunglas + */ +class AbstractPropertyInfoExtractorTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var PropertyInfoExtractor + */ + protected $propertyInfo; + + public function setUp() + { + $extractors = array(new NullExtractor(), new DummyExtractor()); + $this->propertyInfo = new PropertyInfoExtractor($extractors, $extractors, $extractors, $extractors); + } + + public function testInstanceOf() + { + $this->assertInstanceOf('Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface', $this->propertyInfo); + $this->assertInstanceOf('Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface', $this->propertyInfo); + $this->assertInstanceOf('Symfony\Component\PropertyInfo\PropertyDescriptionExtractorInterface', $this->propertyInfo); + $this->assertInstanceOf('Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface', $this->propertyInfo); + } + + public function testGetShortDescription() + { + $this->assertSame('short', $this->propertyInfo->getShortDescription('Foo', 'bar', array())); + } + + public function testGetLongDescription() + { + $this->assertSame('long', $this->propertyInfo->getLongDescription('Foo', 'bar', array())); + } + + public function testGetTypes() + { + $this->assertEquals(array(new Type(Type::BUILTIN_TYPE_INT)), $this->propertyInfo->getTypes('Foo', 'bar', array())); + } + + public function testIsReadable() + { + $this->assertTrue($this->propertyInfo->isReadable('Foo', 'bar', array())); + } + + public function testIsWritable() + { + $this->assertTrue($this->propertyInfo->isWritable('Foo', 'bar', array())); + } + + public function testGetProperties() + { + $this->assertEquals(array('a', 'b'), $this->propertyInfo->getProperties('Foo')); + } +} diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractors/SerializerExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractors/SerializerExtractorTest.php index c170f2237add9..267dd21a11db5 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractors/SerializerExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractors/SerializerExtractorTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\PropertyInfo\PropertyInfo\Tests\Extractors; +namespace Symfony\Component\PropertyInfo\Tests\Extractors; use Doctrine\Common\Annotations\AnnotationReader; use Symfony\Component\PropertyInfo\Extractor\SerializerExtractor; diff --git a/src/Symfony/Component/PropertyInfo/Tests/PropertyInfoCacheExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/PropertyInfoCacheExtractorTest.php new file mode 100644 index 0000000000000..5f09fb041a986 --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/Tests/PropertyInfoCacheExtractorTest.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo\Tests; + +use Symfony\Component\Cache\Adapter\ArrayAdapter; +use Symfony\Component\PropertyInfo\PropertyInfoCacheExtractor; + +/** + * @author Kévin Dunglas + */ +class PropertyInfoCacheExtractorTest extends AbstractPropertyInfoExtractorTest +{ + public function setUp() + { + parent::setUp(); + + $this->propertyInfo = new PropertyInfoCacheExtractor($this->propertyInfo, new ArrayAdapter()); + } + + public function testCache() + { + $this->assertSame('short', $this->propertyInfo->getShortDescription('Foo', 'bar', array())); + $this->assertSame('short', $this->propertyInfo->getShortDescription('Foo', 'bar', array())); + } + + public function testNotSerializableContext() + { + $this->assertSame('short', $this->propertyInfo->getShortDescription('Foo', 'bar', array('foo' => function () {}))); + } + + /** + * @dataProvider escapeDataProvider + */ + public function testEscape($toEscape, $expected) + { + $reflectionMethod = new \ReflectionMethod($this->propertyInfo, 'escape'); + $reflectionMethod->setAccessible(true); + + $this->assertSame($expected, $reflectionMethod->invoke($this->propertyInfo, $toEscape)); + } + + public function escapeDataProvider() + { + return array( + array('foo_bar', 'foo_95bar'), + array('foo_95bar', 'foo_9595bar'), + array('foo{bar}', 'foo_123bar_125'), + array('foo(bar)', 'foo_40bar_41'), + array('foo/bar', 'foo_47bar'), + array('foo\bar', 'foo_92bar'), + array('foo@bar', 'foo_64bar'), + array('foo:bar', 'foo_58bar'), + ); + } +} diff --git a/src/Symfony/Component/PropertyInfo/Tests/PropertyInfoExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/PropertyInfoExtractorTest.php index c7375d3006e0b..53c1b1d8a5c22 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/PropertyInfoExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/PropertyInfoExtractorTest.php @@ -11,62 +11,9 @@ namespace Symfony\Component\PropertyInfo\Tests; -use Symfony\Component\PropertyInfo\PropertyInfoExtractor; -use Symfony\Component\PropertyInfo\Tests\Fixtures\DummyExtractor; -use Symfony\Component\PropertyInfo\Tests\Fixtures\NullExtractor; -use Symfony\Component\PropertyInfo\Type; - /** * @author Kévin Dunglas */ -class PropertyInfoExtractorTest extends \PHPUnit_Framework_TestCase +class PropertyInfoExtractorTest extends AbstractPropertyInfoExtractorTest { - /** - * @var PropertyInfoExtractor - */ - private $propertyInfo; - - public function setUp() - { - $extractors = array(new NullExtractor(), new DummyExtractor()); - $this->propertyInfo = new PropertyInfoExtractor($extractors, $extractors, $extractors, $extractors); - } - - public function testInstanceOf() - { - $this->assertInstanceOf('Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface', $this->propertyInfo); - $this->assertInstanceOf('Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface', $this->propertyInfo); - $this->assertInstanceOf('Symfony\Component\PropertyInfo\PropertyDescriptionExtractorInterface', $this->propertyInfo); - $this->assertInstanceOf('Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface', $this->propertyInfo); - } - - public function testGetShortDescription() - { - $this->assertSame('short', $this->propertyInfo->getShortDescription('Foo', 'bar', array())); - } - - public function testGetLongDescription() - { - $this->assertSame('long', $this->propertyInfo->getLongDescription('Foo', 'bar', array())); - } - - public function testGetTypes() - { - $this->assertEquals(array(new Type(Type::BUILTIN_TYPE_INT)), $this->propertyInfo->getTypes('Foo', 'bar', array())); - } - - public function testIsReadable() - { - $this->assertTrue($this->propertyInfo->isReadable('Foo', 'bar', array())); - } - - public function testIsWritable() - { - $this->assertTrue($this->propertyInfo->isWritable('Foo', 'bar', array())); - } - - public function testGetProperties() - { - $this->assertEquals(array('a', 'b'), $this->propertyInfo->getProperties('Foo')); - } } diff --git a/src/Symfony/Component/PropertyInfo/composer.json b/src/Symfony/Component/PropertyInfo/composer.json index b730caad9cf31..b484a6fe3c626 100644 --- a/src/Symfony/Component/PropertyInfo/composer.json +++ b/src/Symfony/Component/PropertyInfo/composer.json @@ -27,6 +27,7 @@ }, "require-dev": { "symfony/serializer": "~2.8|~3.0", + "symfony/cache": "~3.1", "phpdocumentor/reflection": "^1.0.7", "doctrine/annotations": "~1.0" }, @@ -34,6 +35,7 @@ "phpdocumentor/reflection": "<1.0.7" }, "suggest": { + "psr/cache-implementation": "To cache results", "symfony/doctrine-bridge": "To use Doctrine metadata", "phpdocumentor/reflection": "To use the PHPDoc", "symfony/serializer": "To use Serializer metadata"