diff --git a/README.md b/README.md index 15d9d23..c54b4a7 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,10 @@ You have to configure the name of the service that is PSR6 compliant, that means ## How to use +EmagCacheBundle comes with 2 different ways you can add annotation cache to your service: + +1. @Cache annotation + Add @Cache annotation to the methods you want to be cached: @@ -106,6 +110,60 @@ Here is an example from a service: } ``` +2. @CacheExpression annotation witch uses [Symfony ExpressionLanguage](http://symfony.com/doc/current/components/expression_language.html) +component: + + ```php + + use Emag\CacheBundle\Annotation\CacheExpression; + + /** + * @CacheExpression(cache="", [key="", [ttl=600, [reset=true ]]]) + */ + ``` + + +Here is an example from a service: + +```php + + namespace AppCacheBundle\Service; + + use Emag\CacheBundle\Annotation as eMAG; + + class AppService + { + /** @var string */ + private $prefix; + + public function __construct(string $prefix) + { + $this->prefix = $prefix; + } + + /** + * @eMAG\CacheExpression(cache="this.buildCachePrefix()") + * + * @return int + */ + public function getIntenseResult() : int + { + // 'Simulate a time consuming operation'; + sleep(20); + + return rand(); + } + + /** + * @return string + */ + public function buildCachePrefix() : string + { + return sprintf('_expr[%s]', $this->prefix); + } + } +``` + ## Want to contribute? Submit a PR and join the fun. diff --git a/composer.json b/composer.json index 6bbb0d3..66e5ff9 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,8 @@ "doctrine/annotations": "1.3.*" }, "suggest": { - "symfony/cache": "3.*" + "symfony/cache": "3.*", + "symfony/expression-language": "3.*" }, "require-dev": { "phpunit/phpunit": "5.*", @@ -28,7 +29,8 @@ "symfony/cache": "3.*", "satooshi/php-coveralls": "~1.0", "symfony/monolog-bundle": "@stable", - "symfony/framework-bundle": "@stable" + "symfony/framework-bundle": "@stable", + "symfony/expression-language": "3.*" }, "autoload": { "psr-4": { "Emag\\CacheBundle\\": "src/" }, diff --git a/src/Annotation/CacheExpression.php b/src/Annotation/CacheExpression.php new file mode 100644 index 0000000..31ff527 --- /dev/null +++ b/src/Annotation/CacheExpression.php @@ -0,0 +1,66 @@ +hasEvaluation) { + $this->cache = $this->expressionLanguage->evaluate($this->cache, ['this' => $this->context]); + $this->hasEvaluation = true; + } + + return $this->cache; + } + + /** + * @param object $context + * + * @return CacheExpression + */ + public function setContext($context) : self + { + $this->context = $context; + + return $this; + } + + /** + * @param ExpressionLanguage $language + * + * @return CacheExpression + */ + public function setExpressionLanguage(ExpressionLanguage $language) : self + { + $this->expressionLanguage = $language; + + return $this; + } +} diff --git a/src/DependencyInjection/Compiler/CacheCompilerPass.php b/src/DependencyInjection/Compiler/CacheCompilerPass.php index b146321..35847ea 100644 --- a/src/DependencyInjection/Compiler/CacheCompilerPass.php +++ b/src/DependencyInjection/Compiler/CacheCompilerPass.php @@ -43,6 +43,7 @@ protected function analyzeServicesTobeCached(ContainerBuilder $container) $proxyWarmup = $container->getDefinition('emag.cache.warmup'); $cacheProxyFactory = new Reference('emag.cache.proxy.factory'); $cacheServiceReference = new Reference($container->getParameter('emag.cache.service')); + $expressionLanguage = $container->hasDefinition('emag.cache.expression.language') || $container->hasAlias('emag.cache.expression.language') ? new Reference('emag.cache.expression.language') : null; foreach ($container->getDefinitions() as $serviceId => $definition) { if (!class_exists($definition->getClass()) || $this->isFromIgnoredNamespace($container, $definition->getClass())) { @@ -74,6 +75,7 @@ protected function analyzeServicesTobeCached(ContainerBuilder $container) ->setProperties($definition->getProperties()) ->addMethodCall('setReaderForCacheMethod', [$annotationReaderReference]) ->addMethodCall('setCacheServiceForMethod', [$cacheServiceReference]) + ->addMethodCall('setExpressionLanguage', [$expressionLanguage]) ; $proxyWarmup->addMethodCall('addClassToGenerate', [$definition->getClass()]); @@ -99,4 +101,9 @@ private function isFromIgnoredNamespace(ContainerBuilder $container, $className) } return false; } + + private function getExpressionLanguage() + { + + } } diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index f922f7b..896d4f4 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -38,6 +38,7 @@ public function getConfigTreeBuilder() $rootNode ->children() ->scalarNode('provider')->cannotBeEmpty()->isRequired()->end() + ->scalarNode('expression_language')->defaultNull()->end() ->arrayNode('ignore_namespaces') ->prototype('scalar')->end() ->end() diff --git a/src/DependencyInjection/EmagCacheExtension.php b/src/DependencyInjection/EmagCacheExtension.php index 1d810a1..a7e15ac 100644 --- a/src/DependencyInjection/EmagCacheExtension.php +++ b/src/DependencyInjection/EmagCacheExtension.php @@ -4,10 +4,14 @@ use Emag\CacheBundle\Exception\CacheException; use Psr\Cache\CacheItemPoolInterface; +use Symfony\Component\Cache\Adapter\FilesystemAdapter; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; use Symfony\Component\DependencyInjection\Loader; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\ExpressionLanguage\ExpressionLanguage; use Symfony\Component\HttpKernel\DependencyInjection\Extension; /** @@ -31,6 +35,19 @@ public function prepend(ContainerBuilder $container) if (!$provider->implementsInterface(CacheItemPoolInterface::class)) { throw new CacheException(sprintf('You\'ve referenced a service "%s" that can not be used for caching!', $config['provider'])); } + + if (!$config['expression_language']) { + return; + } + + if (!class_exists('Symfony\Component\ExpressionLanguage\ExpressionLanguage')) { + throw new CacheException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed.'); + } + + $expressionLanguage = new \ReflectionClass($container->getDefinition($config['expression_language'])->getClass()); + if ($expressionLanguage->getName() !== ExpressionLanguage::class) { + throw new CacheException(sprintf('You must provide a valid Expression Language service')); + } } /** @@ -43,6 +60,14 @@ public function load(array $configs, ContainerBuilder $container) $container->setParameter('emag.cache.service', $config['provider']); $container->setParameter('emag.cache.ignore.namespaces', $config['ignore_namespaces']); + if (!$config['expression_language'] && class_exists('Symfony\Component\ExpressionLanguage\ExpressionLanguage')) { + $container->addDefinitions([ + 'emag.cache.filesystem.adapter' => (new Definition(FilesystemAdapter::class))->addArgument('expr_cache'), + 'emag.cache.expression.language'=> (new Definition(ExpressionLanguage::class))->addArgument(new Reference('emag.cache.filesystem.adapter')), + ]); + } elseif ($config['expression_language']) { + $container->setAlias('emag.cache.expression.language', $config['expression_language']); + } $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); $loader->load('services.yml'); diff --git a/src/ProxyManager/CacheableClassTrait.php b/src/ProxyManager/CacheableClassTrait.php index 9a2830c..c8f20d1 100644 --- a/src/ProxyManager/CacheableClassTrait.php +++ b/src/ProxyManager/CacheableClassTrait.php @@ -3,10 +3,12 @@ namespace Emag\CacheBundle\ProxyManager; use Emag\CacheBundle\Annotation\Cache; +use Emag\CacheBundle\Annotation\CacheExpression; use Emag\CacheBundle\Exception\CacheException; use Doctrine\Common\Annotations\AnnotationReader; use Doctrine\Common\Annotations\Reader; use Psr\Cache\CacheItemPoolInterface; +use Symfony\Component\ExpressionLanguage\ExpressionLanguage; trait CacheableClassTrait { @@ -23,6 +25,8 @@ trait CacheableClassTrait */ protected $readerForCacheMethod; + protected $__expressionLanguage; + /** * @param CacheItemPoolInterface $cacheServiceForMethod */ @@ -39,12 +43,23 @@ public function setReaderForCacheMethod(Reader $readerForCacheMethod) $this->readerForCacheMethod = $readerForCacheMethod; } + public function setExpressionLanguage(ExpressionLanguage $language = null) + { + $this->__expressionLanguage = $language; + } + public function getCached(\ReflectionMethod $method, $params) { $method->setAccessible(true); /** @var Cache $annotation */ $annotation = $this->readerForCacheMethod->getMethodAnnotation($method, Cache::class); + if ($annotation instanceof CacheExpression) { + $annotation + ->setContext($this) + ->setExpressionLanguage($this->__expressionLanguage) + ; + } $cacheKey = $this->getCacheKey($method, $params, $annotation); $cacheItem = $this->cacheServiceForMethod->getItem($cacheKey); diff --git a/tests/CacheExpressionDefaultTest.php b/tests/CacheExpressionDefaultTest.php new file mode 100644 index 0000000..c75f643 --- /dev/null +++ b/tests/CacheExpressionDefaultTest.php @@ -0,0 +1,82 @@ + 'test_expr_lang_default']); + $this->container = self::$kernel->getContainer(); + } + + protected static function getKernelClass() + { + return get_class(new class('test_expr_lang_default', []) extends Kernel + { + public function registerBundles() + { + return [ + new \Emag\CacheBundle\EmagCacheBundle() + ]; + } + + public function registerContainerConfiguration(LoaderInterface $loader) + { + $loader->load(__DIR__ . '/config_default_expression.yml'); + } + + public function __construct($environment, $debug) + { + parent::__construct($environment, $debug); + + $loader = require __DIR__ . '/../vendor/autoload.php'; + + AnnotationRegistry::registerLoader(array($loader, 'loadClass')); + $this->rootDir = __DIR__ . '/app/'; + } + }); + } + + public function testDefaultExpressionLanguage() + { + /** @var CacheableExpressionClass $object */ + $object = $this->container->get('cache.expr.test.service'); + $methodName = 'getIntenseResult'; + $objectReflectionClass = new \ReflectionClass($object); + $annotationReader = $this->container->get('annotation_reader'); + /** @var CacheExpression $cacheExpressionAnnotation */ + $cacheExpressionAnnotation = $annotationReader->getMethodAnnotation(new \ReflectionMethod($objectReflectionClass->getParentClass()->getName(), $methodName), CacheExpression::class); + $cacheExpressionAnnotation + ->setExpressionLanguage($this->container->get('emag.cache.expression.language')) + ->setContext($object) + ; + + $result = $object->$methodName(); + $this->assertContains($object->buildCachePrefix(), $cacheExpressionAnnotation->getCache()); + $this->assertEquals(0, strpos($cacheExpressionAnnotation->getCache(), $object->buildCachePrefix())); + $this->assertEquals($result, $object->$methodName()); + } + + public function tearDown() + { + static::$class = null; + } +} diff --git a/tests/CacheWrapperTest.php b/tests/CacheWrapperTest.php index 5e3ec69..7b251cc 100644 --- a/tests/CacheWrapperTest.php +++ b/tests/CacheWrapperTest.php @@ -2,6 +2,9 @@ namespace Emag\CacheBundle\Tests; +use Emag\CacheBundle\Annotation\CacheExpression; +use Emag\CacheBundle\Tests\Helpers\CacheableClass; +use Doctrine\Common\Annotations\AnnotationReader; use Doctrine\Common\Annotations\AnnotationRegistry; use Monolog\Handler\TestHandler; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; @@ -148,6 +151,27 @@ public function testServiceWithArrayParameter() $this->assertGreaterThanOrEqual($min, $result); } + public function testCachePrefixExpressions() + { + /** @var CacheableClass $object */ + $object = $this->container->get('cache.testservice'); + $methodName = 'getCachePrefixFromExpression'; + $objectReflectionClass = new \ReflectionClass($object); + $annotationReader = $this->container->get('annotation_reader'); + /** @var CacheExpression $cacheExpressionAnnotation */ + $cacheExpressionAnnotation = $annotationReader->getMethodAnnotation(new \ReflectionMethod($objectReflectionClass->getParentClass()->getName(), $methodName), CacheExpression::class); + $cacheExpressionAnnotation + ->setExpressionLanguage($this->container->get('emag.cache.expression.language')) + ->setContext($object) + ; + + $result = $object->$methodName(); + $this->assertContains($object->calculateCachePrefix(), $cacheExpressionAnnotation->getCache()); + $this->assertEquals(0, strpos($cacheExpressionAnnotation->getCache(), $object->calculateCachePrefix())); + sleep(1); + $this->assertEquals($result, $object->$methodName()); + } + /** * Get TestHandler object * diff --git a/tests/Helpers/CacheableClass.php b/tests/Helpers/CacheableClass.php index 8169fb5..e99ec69 100644 --- a/tests/Helpers/CacheableClass.php +++ b/tests/Helpers/CacheableClass.php @@ -1,10 +1,9 @@ prefix = $prefix; + } + + /** + * @eMAG\CacheExpression(cache="this.buildCachePrefix()") + * + * @return int + */ + public function getIntenseResult() : int + { + return rand(); + } + + /** + * @return string + */ + public function buildCachePrefix() : string + { + return sprintf('_expr[%s]', $this->prefix); + } +} diff --git a/tests/IncorrectCachingServiceTest.php b/tests/IncorrectCachingServiceTest.php index 67ba766..54954a3 100644 --- a/tests/IncorrectCachingServiceTest.php +++ b/tests/IncorrectCachingServiceTest.php @@ -10,7 +10,7 @@ class IncorrectCachingServiceTest extends KernelTestCase { protected static function getKernelClass() { - return get_class(new class('test_incorrect_service', []) extends Kernel + return get_class(new class('test_incorrect_cache_service', []) extends Kernel { public function registerBundles() { @@ -21,7 +21,7 @@ public function registerBundles() public function registerContainerConfiguration(LoaderInterface $loader) { - $loader->load(__DIR__ . '/config_incorrect_service.yml'); + $loader->load(__DIR__ . '/config_incorrect_cache_service.yml'); } public function __construct($environment, $debug) @@ -42,6 +42,6 @@ public function __construct($environment, $debug) public function testIncorrectService() { static::$class = null; - self::bootKernel(['environment' => 'test_incorrect_service']); + self::bootKernel(['environment' => 'test_incorrect_cache_service']); } } \ No newline at end of file diff --git a/tests/IncorrectExpressiongLanguageServiceTest.php b/tests/IncorrectExpressiongLanguageServiceTest.php new file mode 100644 index 0000000..da290fe --- /dev/null +++ b/tests/IncorrectExpressiongLanguageServiceTest.php @@ -0,0 +1,47 @@ +load(__DIR__ . '/config_incorrect_expr_lang_service.yml'); + } + + public function __construct($environment, $debug) + { + require __DIR__ . '/../vendor/autoload.php'; + + parent::__construct($environment, $debug); + + $this->rootDir = __DIR__ . '/app/'; + } + }); + } + + /** + * @expectedException \Emag\CacheBundle\Exception\CacheException + * @expectedExceptionMessage You must provide a valid Expression Language service + */ + public function testIncorrectService() + { + static::$class = null; + self::bootKernel(['environment' => 'test_incorrect_expr_lang_service']); + } +} \ No newline at end of file diff --git a/tests/config.yml b/tests/config.yml index 499ea5e..889a916 100644 --- a/tests/config.yml +++ b/tests/config.yml @@ -1,5 +1,4 @@ parameters: - cache.service: cache.service max.value: 20 monolog: @@ -9,6 +8,7 @@ monolog: level: info emag_cache: provider: cache.service + expression_language: expr.lang.service services: cache_warmer: @@ -23,3 +23,5 @@ services: class: Emag\CacheBundle\Tests\Helpers\ExtendedCacheableClass annotation_reader: class: Doctrine\Common\Annotations\AnnotationReader + expr.lang.service: + class: Symfony\Component\ExpressionLanguage\ExpressionLanguage diff --git a/tests/config_default_expression.yml b/tests/config_default_expression.yml new file mode 100644 index 0000000..54ca2ad --- /dev/null +++ b/tests/config_default_expression.yml @@ -0,0 +1,13 @@ +emag_cache: + provider: cache.service + +services: + cache_warmer: + class: Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerAggregate + cache.service: + class: Symfony\Component\Cache\Adapter\ArrayAdapter + cache.expr.test.service: + class: Emag\CacheBundle\Tests\Helpers\CacheableExpressionClass + arguments: ["prefix_cache"] + annotation_reader: + class: Doctrine\Common\Annotations\AnnotationReader diff --git a/tests/config_incorrect_service.yml b/tests/config_incorrect_cache_service.yml similarity index 100% rename from tests/config_incorrect_service.yml rename to tests/config_incorrect_cache_service.yml diff --git a/tests/config_incorrect_expr_lang_service.yml b/tests/config_incorrect_expr_lang_service.yml new file mode 100644 index 0000000..e19fc4a --- /dev/null +++ b/tests/config_incorrect_expr_lang_service.yml @@ -0,0 +1,9 @@ +emag_cache: + provider: cache.service + expression_language: expr.lang.fake + +services: + expr.lang.fake: + class: Emag\CacheBundle\Tests\Helpers\CacheableClass + cache.service: + class: Symfony\Component\Cache\Adapter\ArrayAdapter