From a7352ff9759a3033a759b9367898133cf2f22a72 Mon Sep 17 00:00:00 2001 From: Alexandre GESLIN Date: Fri, 26 Aug 2016 09:29:50 +0200 Subject: [PATCH] [ExpressionLanguage] Making cache PSR6 compliant --- UPGRADE-3.2.md | 7 + UPGRADE-4.0.md | 7 + .../DoctrineParserCache.php | 4 + .../DoctrineParserCacheTest.php | 3 + .../ExpressionLanguage/ExpressionLanguage.php | 28 +++- .../ParserCache/ArrayParserCache.php | 4 + .../ParserCache/ParserCacheAdapter.php | 120 +++++++++++++++ .../ParserCache/ParserCacheInterface.php | 4 + .../Tests/ExpressionLanguageTest.php | 99 +++++++++++-- .../ParserCache/ParserCacheAdapterTest.php | 139 ++++++++++++++++++ .../ExpressionLanguage/composer.json | 3 +- 11 files changed, 396 insertions(+), 22 deletions(-) create mode 100644 src/Symfony/Component/ExpressionLanguage/ParserCache/ParserCacheAdapter.php create mode 100644 src/Symfony/Component/ExpressionLanguage/Tests/ParserCache/ParserCacheAdapterTest.php diff --git a/UPGRADE-3.2.md b/UPGRADE-3.2.md index f90b5c885a..394b3fb447 100644 --- a/UPGRADE-3.2.md +++ b/UPGRADE-3.2.md @@ -37,6 +37,13 @@ DependencyInjection * Calling `get()` on a `ContainerBuilder` instance before compiling the container is deprecated and will throw an exception in Symfony 4.0. +ExpressionLanguage +------------------- + +* Passing a `ParserCacheInterface` instance to the `ExpressionLanguage` has been + deprecated and will not be supported in Symfony 4.0. You should use the + `CacheItemPoolInterface` interface instead. + Form ---- diff --git a/UPGRADE-4.0.md b/UPGRADE-4.0.md index e3f82ab74b..420f6b3b2b 100644 --- a/UPGRADE-4.0.md +++ b/UPGRADE-4.0.md @@ -33,6 +33,13 @@ DependencyInjection * Requesting a private service with the `Container::get()` method is no longer supported. +ExpressionLanguage +---------- + + * The ability to pass a `ParserCacheInterface` instance to the `ExpressionLanguage` + class has been removed. You should use the `CacheItemPoolInterface` interface + instead. + Form ---- diff --git a/src/Symfony/Bridge/Doctrine/ExpressionLanguage/DoctrineParserCache.php b/src/Symfony/Bridge/Doctrine/ExpressionLanguage/DoctrineParserCache.php index 1c0e8cdfee..e2eb6e6645 100644 --- a/src/Symfony/Bridge/Doctrine/ExpressionLanguage/DoctrineParserCache.php +++ b/src/Symfony/Bridge/Doctrine/ExpressionLanguage/DoctrineParserCache.php @@ -11,12 +11,16 @@ namespace Symfony\Bridge\Doctrine\ExpressionLanguage; +@trigger_error('The '.__NAMESPACE__.'\DoctrineParserCache class is deprecated since version 3.2 and will be removed in 4.0. Use the Symfony\Component\Cache\Adapter\DoctrineAdapter class instead.', E_USER_DEPRECATED); + use Doctrine\Common\Cache\Cache; use Symfony\Component\ExpressionLanguage\ParsedExpression; use Symfony\Component\ExpressionLanguage\ParserCache\ParserCacheInterface; /** * @author Adrien Brault + * + * @deprecated DoctrineParserCache class is deprecated since version 3.2 and will be removed in 4.0. Use the Symfony\Component\Cache\Adapter\DoctrineAdapter class instead. */ class DoctrineParserCache implements ParserCacheInterface { diff --git a/src/Symfony/Bridge/Doctrine/Tests/ExpressionLanguage/DoctrineParserCacheTest.php b/src/Symfony/Bridge/Doctrine/Tests/ExpressionLanguage/DoctrineParserCacheTest.php index a473b3ace2..6bf22c1851 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/ExpressionLanguage/DoctrineParserCacheTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/ExpressionLanguage/DoctrineParserCacheTest.php @@ -13,6 +13,9 @@ namespace Symfony\Bridge\Doctrine\Tests\ExpressionLanguage; use Symfony\Bridge\Doctrine\ExpressionLanguage\DoctrineParserCache; +/** + * @group legacy + */ class DoctrineParserCacheTest extends \PHPUnit_Framework_TestCase { public function testFetch() diff --git a/src/Symfony/Component/ExpressionLanguage/ExpressionLanguage.php b/src/Symfony/Component/ExpressionLanguage/ExpressionLanguage.php index e40afd00ed..ec56890d2e 100644 --- a/src/Symfony/Component/ExpressionLanguage/ExpressionLanguage.php +++ b/src/Symfony/Component/ExpressionLanguage/ExpressionLanguage.php @@ -11,7 +11,9 @@ namespace Symfony\Component\ExpressionLanguage; -use Symfony\Component\ExpressionLanguage\ParserCache\ArrayParserCache; +use Psr\Cache\CacheItemPoolInterface; +use Symfony\Component\Cache\Adapter\ArrayAdapter; +use Symfony\Component\ExpressionLanguage\ParserCache\ParserCacheAdapter; use Symfony\Component\ExpressionLanguage\ParserCache\ParserCacheInterface; /** @@ -22,7 +24,7 @@ use Symfony\Component\ExpressionLanguage\ParserCache\ParserCacheInterface; class ExpressionLanguage { /** - * @var ParserCacheInterface + * @var CacheItemPoolInterface */ private $cache; private $lexer; @@ -32,12 +34,21 @@ class ExpressionLanguage protected $functions = array(); /** - * @param ParserCacheInterface $cache + * @param CacheItemPoolInterface $cache * @param ExpressionFunctionProviderInterface[] $providers */ - public function __construct(ParserCacheInterface $cache = null, array $providers = array()) + public function __construct($cache = null, array $providers = array()) { - $this->cache = $cache ?: new ArrayParserCache(); + if (null !== $cache) { + if ($cache instanceof ParserCacheInterface) { + @trigger_error(sprintf('Passing an instance of %s as constructor argument for %s is deprecated as of 3.2 and will be removed in 4.0. Pass an instance of %s instead.', ParserCacheInterface::class, self::class, CacheItemPoolInterface::class), E_USER_DEPRECATED); + $cache = new ParserCacheAdapter($cache); + } elseif (!$cache instanceof CacheItemPoolInterface) { + throw new \InvalidArgumentException(sprintf('Cache argument has to implement %s.', CacheItemPoolInterface::class)); + } + } + + $this->cache = $cache ?: new ArrayAdapter(); $this->registerFunctions(); foreach ($providers as $provider) { $this->registerProvider($provider); @@ -91,13 +102,14 @@ class ExpressionLanguage $cacheKeyItems[] = is_int($nameKey) ? $name : $nameKey.':'.$name; } - $key = $expression.'//'.implode('|', $cacheKeyItems); + $cacheItem = $this->cache->getItem(rawurlencode($expression.'//'.implode('|', $cacheKeyItems))); - if (null === $parsedExpression = $this->cache->fetch($key)) { + if (null === $parsedExpression = $cacheItem->get()) { $nodes = $this->getParser()->parse($this->getLexer()->tokenize((string) $expression), $names); $parsedExpression = new ParsedExpression((string) $expression, $nodes); - $this->cache->save($key, $parsedExpression); + $cacheItem->set($parsedExpression); + $this->cache->save($cacheItem); } return $parsedExpression; diff --git a/src/Symfony/Component/ExpressionLanguage/ParserCache/ArrayParserCache.php b/src/Symfony/Component/ExpressionLanguage/ParserCache/ArrayParserCache.php index 1d2aedb514..777de42170 100644 --- a/src/Symfony/Component/ExpressionLanguage/ParserCache/ArrayParserCache.php +++ b/src/Symfony/Component/ExpressionLanguage/ParserCache/ArrayParserCache.php @@ -11,10 +11,14 @@ namespace Symfony\Component\ExpressionLanguage\ParserCache; +@trigger_error('The '.__NAMESPACE__.'\ArrayParserCache class is deprecated since version 3.2 and will be removed in 4.0. Use the Symfony\Component\Cache\Adapter\ArrayAdapter class instead.', E_USER_DEPRECATED); + use Symfony\Component\ExpressionLanguage\ParsedExpression; /** * @author Adrien Brault + * + * @deprecated ArrayParserCache class is deprecated since version 3.2 and will be removed in 4.0. Use the Symfony\Component\Cache\Adapter\ArrayAdapter class instead. */ class ArrayParserCache implements ParserCacheInterface { diff --git a/src/Symfony/Component/ExpressionLanguage/ParserCache/ParserCacheAdapter.php b/src/Symfony/Component/ExpressionLanguage/ParserCache/ParserCacheAdapter.php new file mode 100644 index 0000000000..2867aa3d48 --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/ParserCache/ParserCacheAdapter.php @@ -0,0 +1,120 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage\ParserCache; + +use Psr\Cache\CacheItemInterface; +use Psr\Cache\CacheItemPoolInterface; +use Symfony\Component\Cache\CacheItem; + +/** + * @author Alexandre GESLIN + * + * @internal This class should be removed in Symfony 4.0. + */ +class ParserCacheAdapter implements CacheItemPoolInterface +{ + private $pool; + private $createCacheItem; + + public function __construct(ParserCacheInterface $pool) + { + $this->pool = $pool; + + $this->createCacheItem = \Closure::bind( + function ($key, $value, $isHit) { + $item = new CacheItem(); + $item->key = $key; + $item->value = $value; + $item->isHit = $isHit; + + return $item; + }, + null, + CacheItem::class + ); + } + + /** + * {@inheritdoc} + */ + public function getItem($key) + { + $value = $this->pool->fetch($key); + $f = $this->createCacheItem; + + return $f($key, $value, null !== $value); + } + + /** + * {@inheritdoc} + */ + public function save(CacheItemInterface $item) + { + $this->pool->save($item->getKey(), $item->get()); + } + + /** + * {@inheritdoc} + */ + public function getItems(array $keys = array()) + { + throw new \BadMethodCallException('Not implemented'); + } + + /** + * {@inheritdoc} + */ + public function hasItem($key) + { + throw new \BadMethodCallException('Not implemented'); + } + + /** + * {@inheritdoc} + */ + public function clear() + { + throw new \BadMethodCallException('Not implemented'); + } + + /** + * {@inheritdoc} + */ + public function deleteItem($key) + { + throw new \BadMethodCallException('Not implemented'); + } + + /** + * {@inheritdoc} + */ + public function deleteItems(array $keys) + { + throw new \BadMethodCallException('Not implemented'); + } + + /** + * {@inheritdoc} + */ + public function saveDeferred(CacheItemInterface $item) + { + throw new \BadMethodCallException('Not implemented'); + } + + /** + * {@inheritdoc} + */ + public function commit() + { + throw new \BadMethodCallException('Not implemented'); + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/ParserCache/ParserCacheInterface.php b/src/Symfony/Component/ExpressionLanguage/ParserCache/ParserCacheInterface.php index 4b7a4b6d6f..1e10cb419a 100644 --- a/src/Symfony/Component/ExpressionLanguage/ParserCache/ParserCacheInterface.php +++ b/src/Symfony/Component/ExpressionLanguage/ParserCache/ParserCacheInterface.php @@ -11,10 +11,14 @@ namespace Symfony\Component\ExpressionLanguage\ParserCache; +@trigger_error('The '.__NAMESPACE__.'\ParserCacheInterface interface is deprecated since version 3.2 and will be removed in 4.0. Use Psr\Cache\CacheItemPoolInterface instead.', E_USER_DEPRECATED); + use Symfony\Component\ExpressionLanguage\ParsedExpression; /** * @author Adrien Brault + * + * @deprecated since version 3.2, to be removed in 4.0. Use Psr\Cache\CacheItemPoolInterface instead. */ interface ParserCacheInterface { diff --git a/src/Symfony/Component/ExpressionLanguage/Tests/ExpressionLanguageTest.php b/src/Symfony/Component/ExpressionLanguage/Tests/ExpressionLanguageTest.php index 5af0c4a8e8..dd2165bda3 100644 --- a/src/Symfony/Component/ExpressionLanguage/Tests/ExpressionLanguageTest.php +++ b/src/Symfony/Component/ExpressionLanguage/Tests/ExpressionLanguageTest.php @@ -12,28 +12,77 @@ namespace Symfony\Component\ExpressionLanguage\Tests; use Symfony\Component\ExpressionLanguage\ExpressionLanguage; +use Symfony\Component\ExpressionLanguage\ParsedExpression; use Symfony\Component\ExpressionLanguage\Tests\Fixtures\TestProvider; class ExpressionLanguageTest extends \PHPUnit_Framework_TestCase { public function testCachedParse() { - $cacheMock = $this->getMock('Symfony\Component\ExpressionLanguage\ParserCache\ParserCacheInterface'); + $cacheMock = $this->getMock('Psr\Cache\CacheItemPoolInterface'); + $cacheItemMock = $this->getMock('Psr\Cache\CacheItemInterface'); $savedParsedExpression = null; $expressionLanguage = new ExpressionLanguage($cacheMock); $cacheMock ->expects($this->exactly(2)) - ->method('fetch') - ->with('1 + 1//') + ->method('getItem') + ->with('1%20%2B%201%2F%2F') + ->willReturn($cacheItemMock) + ; + + $cacheItemMock + ->expects($this->exactly(2)) + ->method('get') ->will($this->returnCallback(function () use (&$savedParsedExpression) { return $savedParsedExpression; })) ; + + $cacheItemMock + ->expects($this->exactly(1)) + ->method('set') + ->with($this->isInstanceOf(ParsedExpression::class)) + ->will($this->returnCallback(function ($parsedExpression) use (&$savedParsedExpression) { + $savedParsedExpression = $parsedExpression; + })) + ; + $cacheMock ->expects($this->exactly(1)) ->method('save') - ->with('1 + 1//', $this->isInstanceOf('Symfony\Component\ExpressionLanguage\ParsedExpression')) + ->with($cacheItemMock) + ; + + $parsedExpression = $expressionLanguage->parse('1 + 1', array()); + $this->assertSame($savedParsedExpression, $parsedExpression); + + $parsedExpression = $expressionLanguage->parse('1 + 1', array()); + $this->assertSame($savedParsedExpression, $parsedExpression); + } + + /** + * @group legacy + */ + public function testCachedParseWithDeprecatedParserCacheInterface() + { + $cacheMock = $this->getMock('Symfony\Component\ExpressionLanguage\ParserCache\ParserCacheInterface'); + + $cacheItemMock = $this->getMock('Psr\Cache\CacheItemInterface'); + $savedParsedExpression = null; + $expressionLanguage = new ExpressionLanguage($cacheMock); + + $cacheMock + ->expects($this->exactly(1)) + ->method('fetch') + ->with('1%20%2B%201%2F%2F') + ->willReturn($savedParsedExpression) + ; + + $cacheMock + ->expects($this->exactly(1)) + ->method('save') + ->with('1%20%2B%201%2F%2F', $this->isInstanceOf(ParsedExpression::class)) ->will($this->returnCallback(function ($key, $expression) use (&$savedParsedExpression) { $savedParsedExpression = $expression; })) @@ -41,9 +90,16 @@ class ExpressionLanguageTest extends \PHPUnit_Framework_TestCase $parsedExpression = $expressionLanguage->parse('1 + 1', array()); $this->assertSame($savedParsedExpression, $parsedExpression); + } - $parsedExpression = $expressionLanguage->parse('1 + 1', array()); - $this->assertSame($savedParsedExpression, $parsedExpression); + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Cache argument has to implement Psr\Cache\CacheItemPoolInterface. + */ + public function testWrongCacheImplementation() + { + $cacheMock = $this->getMock('Psr\Cache\CacheItemSpoolInterface'); + $expressionLanguage = new ExpressionLanguage($cacheMock); } public function testConstantFunction() @@ -116,22 +172,39 @@ class ExpressionLanguageTest extends \PHPUnit_Framework_TestCase public function testCachingWithDifferentNamesOrder() { - $cacheMock = $this->getMock('Symfony\Component\ExpressionLanguage\ParserCache\ParserCacheInterface'); + $cacheMock = $this->getMock('Psr\Cache\CacheItemPoolInterface'); + $cacheItemMock = $this->getMock('Psr\Cache\CacheItemInterface'); $expressionLanguage = new ExpressionLanguage($cacheMock); $savedParsedExpressions = array(); + $cacheMock ->expects($this->exactly(2)) - ->method('fetch') - ->will($this->returnCallback(function ($key) use (&$savedParsedExpressions) { - return isset($savedParsedExpressions[$key]) ? $savedParsedExpressions[$key] : null; + ->method('getItem') + ->with('a%20%2B%20b%2F%2Fa%7CB%3Ab') + ->willReturn($cacheItemMock) + ; + + $cacheItemMock + ->expects($this->exactly(2)) + ->method('get') + ->will($this->returnCallback(function () use (&$savedParsedExpression) { + return $savedParsedExpression; })) ; + + $cacheItemMock + ->expects($this->exactly(1)) + ->method('set') + ->with($this->isInstanceOf(ParsedExpression::class)) + ->will($this->returnCallback(function ($parsedExpression) use (&$savedParsedExpression) { + $savedParsedExpression = $parsedExpression; + })) + ; + $cacheMock ->expects($this->exactly(1)) ->method('save') - ->will($this->returnCallback(function ($key, $expression) use (&$savedParsedExpressions) { - $savedParsedExpressions[$key] = $expression; - })) + ->with($cacheItemMock) ; $expression = 'a + b'; diff --git a/src/Symfony/Component/ExpressionLanguage/Tests/ParserCache/ParserCacheAdapterTest.php b/src/Symfony/Component/ExpressionLanguage/Tests/ParserCache/ParserCacheAdapterTest.php new file mode 100644 index 0000000000..5c15c3282f --- /dev/null +++ b/src/Symfony/Component/ExpressionLanguage/Tests/ParserCache/ParserCacheAdapterTest.php @@ -0,0 +1,139 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ExpressionLanguage\Tests; + +use Symfony\Component\ExpressionLanguage\ParsedExpression; +use Symfony\Component\ExpressionLanguage\ParserCache\ParserCacheAdapter; +use Symfony\Component\ExpressionLanguage\Node\Node; + +/** + * @group legacy + */ +class ParserCacheAdapterTest extends \PHPUnit_Framework_TestCase +{ + public function testGetItem() + { + $poolMock = $this->getMock('Symfony\Component\ExpressionLanguage\ParserCache\ParserCacheInterface'); + + $key = 'key'; + $value = 'value'; + $parserCacheAdapter = new ParserCacheAdapter($poolMock); + + $poolMock + ->expects($this->once()) + ->method('fetch') + ->with($key) + ->willReturn($value) + ; + + $cacheItem = $parserCacheAdapter->getItem($key); + + $this->assertEquals($cacheItem->get(), $value); + $this->assertEquals($cacheItem->isHit(), true); + } + + public function testSave() + { + $poolMock = $this->getMock('Symfony\Component\ExpressionLanguage\ParserCache\ParserCacheInterface'); + $cacheItemMock = $this->getMock('Psr\Cache\CacheItemInterface'); + $key = 'key'; + $value = new ParsedExpression('1 + 1', new Node(array(), array())); + $parserCacheAdapter = new ParserCacheAdapter($poolMock); + + $poolMock + ->expects($this->once()) + ->method('save') + ->with($key, $value) + ; + + $cacheItemMock + ->expects($this->once()) + ->method('getKey') + ->willReturn($key) + ; + + $cacheItemMock + ->expects($this->once()) + ->method('get') + ->willReturn($value) + ; + + $cacheItem = $parserCacheAdapter->save($cacheItemMock); + } + + public function testGetItems() + { + $poolMock = $this->getMock('Symfony\Component\ExpressionLanguage\ParserCache\ParserCacheInterface'); + $parserCacheAdapter = new ParserCacheAdapter($poolMock); + $this->setExpectedException(\BadMethodCallException::class); + + $parserCacheAdapter->getItems(); + } + + public function testHasItem() + { + $poolMock = $this->getMock('Symfony\Component\ExpressionLanguage\ParserCache\ParserCacheInterface'); + $key = 'key'; + $parserCacheAdapter = new ParserCacheAdapter($poolMock); + $this->setExpectedException(\BadMethodCallException::class); + + $parserCacheAdapter->hasItem($key); + } + + public function testClear() + { + $poolMock = $this->getMock('Symfony\Component\ExpressionLanguage\ParserCache\ParserCacheInterface'); + $parserCacheAdapter = new ParserCacheAdapter($poolMock); + $this->setExpectedException(\BadMethodCallException::class); + + $parserCacheAdapter->clear(); + } + + public function testDeleteItem() + { + $poolMock = $this->getMock('Symfony\Component\ExpressionLanguage\ParserCache\ParserCacheInterface'); + $key = 'key'; + $parserCacheAdapter = new ParserCacheAdapter($poolMock); + $this->setExpectedException(\BadMethodCallException::class); + + $parserCacheAdapter->deleteItem($key); + } + + public function testDeleteItems() + { + $poolMock = $this->getMock('Symfony\Component\ExpressionLanguage\ParserCache\ParserCacheInterface'); + $keys = array('key'); + $parserCacheAdapter = new ParserCacheAdapter($poolMock); + $this->setExpectedException(\BadMethodCallException::class); + + $parserCacheAdapter->deleteItems($keys); + } + + public function testSaveDeferred() + { + $poolMock = $this->getMock('Symfony\Component\ExpressionLanguage\ParserCache\ParserCacheInterface'); + $parserCacheAdapter = new ParserCacheAdapter($poolMock); + $cacheItemMock = $this->getMock('Psr\Cache\CacheItemInterface'); + $this->setExpectedException(\BadMethodCallException::class); + + $parserCacheAdapter->saveDeferred($cacheItemMock); + } + + public function testCommit() + { + $poolMock = $this->getMock('Symfony\Component\ExpressionLanguage\ParserCache\ParserCacheInterface'); + $parserCacheAdapter = new ParserCacheAdapter($poolMock); + $this->setExpectedException(\BadMethodCallException::class); + + $parserCacheAdapter->commit(); + } +} diff --git a/src/Symfony/Component/ExpressionLanguage/composer.json b/src/Symfony/Component/ExpressionLanguage/composer.json index 2a5b8a3c30..fc7e3c3b89 100644 --- a/src/Symfony/Component/ExpressionLanguage/composer.json +++ b/src/Symfony/Component/ExpressionLanguage/composer.json @@ -16,7 +16,8 @@ } ], "require": { - "php": ">=5.5.9" + "php": ">=5.5.9", + "symfony/cache": "~3.1" }, "autoload": { "psr-4": { "Symfony\\Component\\ExpressionLanguage\\": "" },