Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cache expression #16

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
58 changes: 58 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:


Expand Down Expand Up @@ -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="<put your expression language code>", [key="<name of argument to include in cache key separated by comma>", [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.
Expand Down
6 changes: 4 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"doctrine/annotations": "1.3.*"
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

expression-language can be put as suggested?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you put it as suggested you can't use @CacheExpression annotation I think.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can put it in both require-dev (so you can test it) and in suggests.
Who requires usage of expression should include the package explictly

"suggest": {
"symfony/cache": "3.*"
"symfony/cache": "3.*",
"symfony/expression-language": "3.*"
},
"require-dev": {
"phpunit/phpunit": "5.*",
Expand All @@ -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/" },
Expand Down
66 changes: 66 additions & 0 deletions src/Annotation/CacheExpression.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

declare(strict_types=1);

namespace Emag\CacheBundle\Annotation;

use Symfony\Component\ExpressionLanguage\ExpressionLanguage;

/**
* @Annotation
* @Target({"METHOD"})
*/
class CacheExpression extends Cache
{
/**
* @var ExpressionLanguage
*/
protected $expressionLanguage;

/**
* @var object
*/
private $context;

/**
* @var bool
*/
private $hasEvaluation = false;

/**
* @inheritDoc
*/
public function getCache() : string
{
if (!$this->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;
}
}
7 changes: 7 additions & 0 deletions src/DependencyInjection/Compiler/CacheCompilerPass.php
Original file line number Diff line number Diff line change
Expand Up @@ -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())) {
Expand Down Expand Up @@ -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()]);
Expand All @@ -99,4 +101,9 @@ private function isFromIgnoredNamespace(ContainerBuilder $container, $className)
}
return false;
}

private function getExpressionLanguage()
{

}
}
1 change: 1 addition & 0 deletions src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
25 changes: 25 additions & 0 deletions src/DependencyInjection/EmagCacheExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -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'));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Too exact class match. You should be able to provide anything that extends expressionLanguage.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Of course I will fix it.

}
}

/**
Expand All @@ -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']);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i would reverse the if. it's cheaper... :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do.

}

$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.yml');
Expand Down
15 changes: 15 additions & 0 deletions src/ProxyManager/CacheableClassTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -23,6 +25,8 @@ trait CacheableClassTrait
*/
protected $readerForCacheMethod;

protected $__expressionLanguage;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's with the __ ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid parameter collision.

/**
* @param CacheItemPoolInterface $cacheServiceForMethod
*/
Expand All @@ -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);
Expand Down
82 changes: 82 additions & 0 deletions tests/CacheExpressionDefaultTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

namespace CacheBundle\Tests;

use Doctrine\Common\Annotations\AnnotationReader;
use Doctrine\Common\Annotations\AnnotationRegistry;
use Emag\CacheBundle\Annotation\CacheExpression;
use Emag\CacheBundle\Tests\Helpers\CacheableExpressionClass;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\Kernel;

class CacheExpressionDefaultTest extends KernelTestCase
{
/**
* @var ContainerInterface
*/
protected $container;

public function setUp()
{
parent::setUp();

static::$class = null;
self::bootKernel(['environment' => '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;
}
}
Loading