feature #16838 [PropertyAccess] Add PSR-6 cache (dunglas)

This PR was squashed before being merged into the 3.2-dev branch (closes #16838).

Discussion
----------

[PropertyAccess] Add PSR-6 cache

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | n/a
| License       | MIT
| Doc PR        | n/a

Follow #16294

Commits
-------

4ccabcd [PropertyAccess] Add PSR-6 cache
This commit is contained in:
Nicolas Grekas 2016-06-08 18:51:45 +02:00
commit ce28a869cc
7 changed files with 307 additions and 128 deletions

View File

@ -12,6 +12,7 @@
namespace Symfony\Bundle\FrameworkBundle\DependencyInjection;
use Doctrine\Common\Annotations\Reader;
use Symfony\Component\Cache\Adapter\AdapterInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Definition;
@ -24,6 +25,7 @@ use Symfony\Component\Config\Resource\DirectoryResource;
use Symfony\Component\Finder\Finder;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\PropertyAccess\PropertyAccessor;
use Symfony\Component\Serializer\Mapping\Factory\CacheClassMetadataFactory;
use Symfony\Component\Serializer\Normalizer\DataUriNormalizer;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
@ -1062,6 +1064,15 @@ class FrameworkExtension extends Extension
$container->setDefinition($name, $definition);
}
if (method_exists(PropertyAccessor::class, 'createCache')) {
$propertyAccessDefinition = $container->register('cache.property_access', AdapterInterface::class);
$propertyAccessDefinition->setPublic(false);
$propertyAccessDefinition->setFactory(array(PropertyAccessor::class, 'createCache'));
$propertyAccessDefinition->setArguments(array(null, null, $version, new Reference('logger', ContainerInterface::IGNORE_ON_INVALID_REFERENCE)));
$propertyAccessDefinition->addTag('cache.pool', array('clearer' => 'cache.default_clearer'));
$propertyAccessDefinition->addTag('monolog.logger', array('channel' => 'cache'));
}
$this->addClassesToCompile(array(
'Psr\Cache\CacheItemInterface',
'Psr\Cache\CacheItemPoolInterface',

View File

@ -8,6 +8,7 @@
<service id="property_accessor" class="Symfony\Component\PropertyAccess\PropertyAccessor" >
<argument /> <!-- magicCall, set by the extension -->
<argument /> <!-- throwExceptionOnInvalidIndex, set by the extension -->
<argument type="service" id="cache.property_access" on-invalid="ignore" />
</service>
</services>
</container>

View File

@ -11,6 +11,11 @@
namespace Symfony\Component\PropertyAccess;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Cache\Adapter\AdapterInterface;
use Symfony\Component\Cache\Adapter\ApcuAdapter;
use Symfony\Component\Cache\Adapter\NullAdapter;
use Symfony\Component\Inflector\Inflector;
use Symfony\Component\PropertyAccess\Exception\AccessException;
use Symfony\Component\PropertyAccess\Exception\InvalidArgumentException;
@ -97,6 +102,21 @@ class PropertyAccessor implements PropertyAccessorInterface
*/
const ACCESS_TYPE_NOT_FOUND = 4;
/**
* @internal
*/
const CACHE_PREFIX_READ = 'r';
/**
* @internal
*/
const CACHE_PREFIX_WRITE = 'w';
/**
* @internal
*/
const CACHE_PREFIX_PROPERTY_PATH = 'p';
/**
* @var bool
*/
@ -107,6 +127,11 @@ class PropertyAccessor implements PropertyAccessorInterface
*/
private $ignoreInvalidIndices;
/**
* @var CacheItemPoolInterface
*/
private $cacheItemPool;
/**
* @var array
*/
@ -120,17 +145,24 @@ class PropertyAccessor implements PropertyAccessorInterface
private static $errorHandler = array(__CLASS__, 'handleError');
private static $resultProto = array(self::VALUE => null);
/**
* @var array
*/
private $propertyPathCache = array();
/**
* Should not be used by application code. Use
* {@link PropertyAccess::createPropertyAccessor()} instead.
*
* @param bool $magicCall
* @param bool $throwExceptionOnInvalidIndex
* @param CacheItemPoolInterface $cacheItemPool
*/
public function __construct($magicCall = false, $throwExceptionOnInvalidIndex = false)
public function __construct($magicCall = false, $throwExceptionOnInvalidIndex = false, CacheItemPoolInterface $cacheItemPool = null)
{
$this->magicCall = $magicCall;
$this->ignoreInvalidIndices = !$throwExceptionOnInvalidIndex;
$this->cacheItemPool = $cacheItemPool instanceof NullAdapter ? null : $cacheItemPool; // Replace the NullAdapter by the null value
}
/**
@ -138,9 +170,7 @@ class PropertyAccessor implements PropertyAccessorInterface
*/
public function getValue($objectOrArray, $propertyPath)
{
if (!$propertyPath instanceof PropertyPathInterface) {
$propertyPath = new PropertyPath($propertyPath);
}
$propertyPath = $this->getPropertyPath($propertyPath);
$zval = array(
self::VALUE => $objectOrArray,
@ -155,9 +185,7 @@ class PropertyAccessor implements PropertyAccessorInterface
*/
public function setValue(&$objectOrArray, $propertyPath, $value)
{
if (!$propertyPath instanceof PropertyPathInterface) {
$propertyPath = new PropertyPath($propertyPath);
}
$propertyPath = $this->getPropertyPath($propertyPath);
$zval = array(
self::VALUE => $objectOrArray,
@ -284,9 +312,7 @@ class PropertyAccessor implements PropertyAccessorInterface
*/
public function isWritable($objectOrArray, $propertyPath)
{
if (!$propertyPath instanceof PropertyPathInterface) {
$propertyPath = new PropertyPath($propertyPath);
}
$propertyPath = $this->getPropertyPath($propertyPath);
try {
$zval = array(
@ -505,11 +531,19 @@ class PropertyAccessor implements PropertyAccessorInterface
*/
private function getReadAccessInfo($class, $property)
{
$key = $class.'::'.$property;
$key = $class.'..'.$property;
if (isset($this->readPropertyCache[$key])) {
$access = $this->readPropertyCache[$key];
} else {
return $this->readPropertyCache[$key];
}
if ($this->cacheItemPool) {
$item = $this->cacheItemPool->getItem(self::CACHE_PREFIX_READ.str_replace('\\', '.', $key));
if ($item->isHit()) {
return $this->readPropertyCache[$key] = $item->get();
}
}
$access = array();
$reflClass = new \ReflectionClass($class);
@ -560,10 +594,11 @@ class PropertyAccessor implements PropertyAccessorInterface
);
}
$this->readPropertyCache[$key] = $access;
if (isset($item)) {
$this->cacheItemPool->save($item->set($access));
}
return $access;
return $this->readPropertyCache[$key] = $access;
}
/**
@ -673,11 +708,19 @@ class PropertyAccessor implements PropertyAccessorInterface
*/
private function getWriteAccessInfo($class, $property, $value)
{
$key = $class.'::'.$property;
$key = $class.'..'.$property;
if (isset($this->writePropertyCache[$key])) {
$access = $this->writePropertyCache[$key];
} else {
return $this->writePropertyCache[$key];
}
if ($this->cacheItemPool) {
$item = $this->cacheItemPool->getItem(self::CACHE_PREFIX_WRITE.str_replace('\\', '.', $key));
if ($item->isHit()) {
return $this->writePropertyCache[$key] = $item->get();
}
}
$access = array();
$reflClass = new \ReflectionClass($class);
@ -731,10 +774,11 @@ class PropertyAccessor implements PropertyAccessorInterface
}
}
$this->writePropertyCache[$key] = $access;
if (isset($item)) {
$this->cacheItemPool->save($item->set($access));
}
return $access;
return $this->writePropertyCache[$key] = $access;
}
/**
@ -818,4 +862,68 @@ class PropertyAccessor implements PropertyAccessorInterface
return false;
}
/**
* Gets a PropertyPath instance and caches it.
*
* @param string|PropertyPath $propertyPath
*
* @return PropertyPath
*/
private function getPropertyPath($propertyPath)
{
if ($propertyPath instanceof PropertyPathInterface) {
// Don't call the copy constructor has it is not needed here
return $propertyPath;
}
if (isset($this->propertyPathCache[$propertyPath])) {
return $this->propertyPathCache[$propertyPath];
}
if ($this->cacheItemPool) {
$item = $this->cacheItemPool->getItem(self::CACHE_PREFIX_PROPERTY_PATH.$propertyPath);
if ($item->isHit()) {
return $this->propertyPathCache[$propertyPath] = $item->get();
}
}
$propertyPathInstance = new PropertyPath($propertyPath);
if (isset($item)) {
$item->set($propertyPathInstance);
$this->cacheItemPool->save($item);
}
return $this->propertyPathCache[$propertyPath] = $propertyPathInstance;
}
/**
* Creates the APCu adapter if applicable.
*
* @param string $namespace
* @param int $defaultLifetime
* @param string $version
* @param LoggerInterface|null $logger
*
* @return AdapterInterface
*
* @throws RuntimeException When the Cache Component isn't available
*/
public static function createCache($namespace, $defaultLifetime, $version, LoggerInterface $logger = null)
{
if (!class_exists('Symfony\Component\Cache\Adapter\ApcuAdapter')) {
throw new \RuntimeException(sprintf('The Symfony Cache component must be installed to use %s().', __METHOD__));
}
if (!ApcuAdapter::isSupported()) {
return new NullAdapter();
}
$apcu = new ApcuAdapter($namespace, $defaultLifetime / 5, $version);
if (null !== $logger) {
$apcu->setLogger($logger);
}
return $apcu;
}
}

View File

@ -11,6 +11,8 @@
namespace Symfony\Component\PropertyAccess;
use Psr\Cache\CacheItemPoolInterface;
/**
* A configurable builder to create a PropertyAccessor.
*
@ -28,6 +30,11 @@ class PropertyAccessorBuilder
*/
private $throwExceptionOnInvalidIndex = false;
/**
* @var CacheItemPoolInterface|null
*/
private $cacheItemPool;
/**
* Enables the use of "__call" by the PropertyAccessor.
*
@ -97,6 +104,30 @@ class PropertyAccessorBuilder
return $this->throwExceptionOnInvalidIndex;
}
/**
* Sets a cache system.
*
* @param CacheItemPoolInterface|null $cacheItemPool
*
* @return PropertyAccessorBuilder The builder object
*/
public function setCacheItemPool(CacheItemPoolInterface $cacheItemPool = null)
{
$this->cacheItemPool = $cacheItemPool;
return $this;
}
/**
* Gets the used cache system.
*
* @return CacheItemPoolInterface|null
*/
public function getCacheItemPool()
{
return $this->cacheItemPool;
}
/**
* Builds and returns a new PropertyAccessor object.
*
@ -104,6 +135,6 @@ class PropertyAccessorBuilder
*/
public function getPropertyAccessor()
{
return new PropertyAccessor($this->magicCall, $this->throwExceptionOnInvalidIndex);
return new PropertyAccessor($this->magicCall, $this->throwExceptionOnInvalidIndex, $this->cacheItemPool);
}
}

View File

@ -11,6 +11,8 @@
namespace Symfony\Component\PropertyAccess\Tests;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\PropertyAccess\PropertyAccessor;
use Symfony\Component\PropertyAccess\PropertyAccessorBuilder;
class PropertyAccessorBuilderTest extends \PHPUnit_Framework_TestCase
@ -49,7 +51,15 @@ class PropertyAccessorBuilderTest extends \PHPUnit_Framework_TestCase
public function testGetPropertyAccessor()
{
$this->assertInstanceOf('Symfony\Component\PropertyAccess\PropertyAccessor', $this->builder->getPropertyAccessor());
$this->assertInstanceOf('Symfony\Component\PropertyAccess\PropertyAccessor', $this->builder->enableMagicCall()->getPropertyAccessor());
$this->assertInstanceOf(PropertyAccessor::class, $this->builder->getPropertyAccessor());
$this->assertInstanceOf(PropertyAccessor::class, $this->builder->enableMagicCall()->getPropertyAccessor());
}
public function testUseCache()
{
$cacheItemPool = new ArrayAdapter();
$this->builder->setCacheItemPool($cacheItemPool);
$this->assertEquals($cacheItemPool, $this->builder->getCacheItemPool());
$this->assertInstanceOf(PropertyAccessor::class, $this->builder->getPropertyAccessor());
}
}

View File

@ -11,6 +11,7 @@
namespace Symfony\Component\PropertyAccess\Tests;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\PropertyAccess\Exception\NoSuchIndexException;
use Symfony\Component\PropertyAccess\PropertyAccessor;
use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClass;
@ -554,4 +555,15 @@ class PropertyAccessorTest extends \PHPUnit_Framework_TestCase
$this->assertSame('baz', $this->propertyAccessor->getValue($object, 'publicAccessor[value2]'));
$this->assertSame(array('value1' => 'foo', 'value2' => 'baz'), $object->getPublicAccessor());
}
public function testCacheReadAccess()
{
$obj = new TestClass('foo');
$propertyAccessor = new PropertyAccessor(false, false, new ArrayAdapter());
$this->assertEquals('foo', $propertyAccessor->getValue($obj, 'publicGetSetter'));
$propertyAccessor->setValue($obj, 'publicGetSetter', 'bar');
$propertyAccessor->setValue($obj, 'publicGetSetter', 'baz');
$this->assertEquals('baz', $propertyAccessor->getValue($obj, 'publicGetSetter'));
}
}

View File

@ -20,6 +20,12 @@
"symfony/polyfill-php70": "~1.0",
"symfony/inflector": "~3.1"
},
"require-dev": {
"symfony/cache": "~3.1"
},
"suggest": {
"psr/cache-implementation": "To cache access methods."
},
"autoload": {
"psr-4": { "Symfony\\Component\\PropertyAccess\\": "" },
"exclude-from-classmap": [