├── Resources ├── doc │ ├── debug-bar-01.png │ ├── debug-bar-02.png │ └── debug-bar-03.png ├── config │ ├── logger.xml │ ├── data_collectors.xml │ ├── key_generators.xml │ ├── cache.xml │ ├── cache_factories.xml │ ├── doctrine_caches.xml │ └── aop.xml ├── meta │ └── LICENSE └── views │ └── Collector │ └── cache_operations.html.twig ├── .gitignore ├── Tests ├── bootstrap.php.dist ├── autoload.php.dist ├── Functional │ ├── Fixtures │ │ ├── Book.php │ │ └── BookService.php │ ├── FunctionalTestCase.php │ ├── app │ │ ├── config │ │ │ └── config.yml │ │ └── AppKernel.php │ └── CacheAnnotationTest.php ├── Metadata │ └── Driver │ │ ├── Fixtures │ │ └── CacheableService.php │ │ └── AnnotationDriverTest.php ├── Aop │ ├── Pointcut │ │ └── CachePointcutTest.php │ └── Interceptor │ │ ├── AbstractCacheOperationTest.php │ │ ├── CacheInterceptorTest.php │ │ ├── CacheUpdateOperationTest.php │ │ ├── CacheableOperationTest.php │ │ └── CacheEvictOperationTest.php └── Cache │ └── KeyGenerator │ ├── LiteralKeyGeneratorTest.php │ └── SimpleHashKeyGeneratorTest.php ├── .travis.yml ├── Exception ├── Exception.php ├── RuntimeException.php ├── InvalidArgumentException.php └── UnsupportedKeyParameterException.php ├── Cache ├── SimpleCacheManager.php ├── KeyGenerator │ ├── KeyGeneratorInterface.php │ ├── LiteralKeyGenerator.php │ └── SimpleHashKeyGenerator.php ├── CacheManagerInterface.php ├── CacheInterface.php ├── CacheManager.php └── DoctrineProxyCache.php ├── Annotation ├── Cacheable.php ├── CacheUpdate.php ├── CacheEvict.php └── Cache.php ├── Metadata ├── ClassMetadata.php ├── CacheMethodMetadataInterface.php ├── CacheUpdateMethodMetadata.php ├── CacheableMethodMetadata.php ├── CacheEvictMethodMetadata.php ├── AbstractCacheMethodMetadata.php └── Driver │ └── AnnotationDriver.php ├── Logger ├── CacheLoggerInterface.php └── CacheLogger.php ├── Aop ├── Interceptor │ ├── CacheOperationInterface.php │ ├── CacheUpdateOperation.php │ ├── CacheEvictOperation.php │ ├── CacheableOperation.php │ ├── CacheInterceptor.php │ ├── AbstractCacheOperation.php │ └── CacheOperationContext.php └── Pointcut │ └── CachePointcut.php ├── phpunit.xml.dist ├── TbbcCacheBundle.php ├── CacheEvents.php ├── Event ├── CacheEvent.php ├── CacheUpdateEvent.php └── CacheHitEvent.php ├── LICENSE ├── DependencyInjection ├── CacheFactory │ ├── CacheFactoryInterface.php │ ├── ArrayCacheFactory.php │ ├── ApcCacheFactory.php │ ├── RedisCacheFactory.php │ └── MemcachedCacheFactory.php ├── Compiler │ └── CacheEligibleServicesPass.php ├── Configuration.php └── TbbcCacheExtension.php ├── composer.json ├── DataCollector └── CacheableOperationDataCollector.php └── README.md /Resources/doc/debug-bar-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheBigBrainsCompany/TbbcCacheBundle/HEAD/Resources/doc/debug-bar-01.png -------------------------------------------------------------------------------- /Resources/doc/debug-bar-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheBigBrainsCompany/TbbcCacheBundle/HEAD/Resources/doc/debug-bar-02.png -------------------------------------------------------------------------------- /Resources/doc/debug-bar-03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheBigBrainsCompany/TbbcCacheBundle/HEAD/Resources/doc/debug-bar-03.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/* 2 | /phpunit.xml 3 | .idea/ 4 | /composer.lock 5 | /Tests/Functional/app/cache 6 | /Tests/Functional/app/logs 7 | -------------------------------------------------------------------------------- /Tests/bootstrap.php.dist: -------------------------------------------------------------------------------- 1 | 6 | * 7 | */ 8 | 9 | namespace Tbbc\CacheBundle\Exception; 10 | 11 | /** 12 | * Base exception for the TbbcCacheBundle 13 | * 14 | * @author Benjamin Dulau 15 | */ 16 | interface Exception 17 | { 18 | } 19 | -------------------------------------------------------------------------------- /Cache/SimpleCacheManager.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | */ 8 | 9 | namespace Tbbc\CacheBundle\Cache; 10 | 11 | /** 12 | * Class SimpleCacheManager 13 | * 14 | * @author Benjamin Dulau 15 | */ 16 | class SimpleCacheManager extends CacheManager 17 | { 18 | 19 | } 20 | -------------------------------------------------------------------------------- /Exception/RuntimeException.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | */ 8 | 9 | namespace Tbbc\CacheBundle\Exception; 10 | 11 | /** 12 | * RuntimeException 13 | * 14 | * @author Benjamin Dulau 15 | */ 16 | class RuntimeException extends \RuntimeException implements Exception 17 | { 18 | } 19 | -------------------------------------------------------------------------------- /Tests/Functional/Fixtures/Book.php: -------------------------------------------------------------------------------- 1 | isbn = $isbn; 14 | $this->author = $author; 15 | $this->title = $title; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Annotation/Cacheable.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | */ 9 | 10 | namespace Tbbc\CacheBundle\Annotation; 11 | 12 | /** 13 | * Represents a @Cacheable annotation. 14 | * 15 | * @Annotation 16 | * @Target("METHOD") 17 | * @author Benjamin Dulau 18 | */ 19 | final class Cacheable extends Cache 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /Exception/InvalidArgumentException.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | */ 8 | 9 | namespace Tbbc\CacheBundle\Exception; 10 | 11 | /** 12 | * InvalidArgumentException 13 | * 14 | * @author Benjamin Dulau 15 | */ 16 | class InvalidArgumentException extends \InvalidArgumentException implements Exception 17 | { 18 | } 19 | -------------------------------------------------------------------------------- /Annotation/CacheUpdate.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | */ 9 | 10 | namespace Tbbc\CacheBundle\Annotation; 11 | 12 | /** 13 | * Represents a @CacheUpdate annotation. 14 | * 15 | * @Annotation 16 | * @Target("METHOD") 17 | * @author Benjamin Dulau 18 | */ 19 | final class CacheUpdate extends Cache 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /Metadata/ClassMetadata.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | */ 8 | 9 | namespace Tbbc\CacheBundle\Metadata; 10 | 11 | use Metadata\MergeableClassMetadata; 12 | 13 | /** 14 | * Contains class metadata information 15 | * 16 | * @author Benjamin Dulau 17 | */ 18 | class ClassMetadata extends MergeableClassMetadata 19 | { 20 | 21 | } 22 | -------------------------------------------------------------------------------- /Metadata/CacheMethodMetadataInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | */ 8 | 9 | namespace Tbbc\CacheBundle\Metadata; 10 | 11 | /** 12 | * @author Boris Guéry 13 | */ 14 | interface CacheMethodMetadataInterface 15 | { 16 | /** 17 | * Returns the associated operation name 18 | * 19 | * @return string 20 | */ 21 | public function getOperation(); 22 | } 23 | -------------------------------------------------------------------------------- /Metadata/CacheUpdateMethodMetadata.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | */ 8 | 9 | namespace Tbbc\CacheBundle\Metadata; 10 | 11 | /** 12 | * Contains method metadata information 13 | * 14 | * @author Benjamin Dulau 15 | */ 16 | class CacheUpdateMethodMetadata extends AbstractCacheMethodMetadata 17 | { 18 | public function getOperation() 19 | { 20 | return 'cache_update'; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Logger/CacheLoggerInterface.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | namespace Tbbc\CacheBundle\Logger; 7 | 8 | use Tbbc\CacheBundle\Aop\Interceptor\CacheOperationContext; 9 | 10 | interface CacheLoggerInterface 11 | { 12 | /** 13 | * @param CacheOperationContext $context 14 | * @return mixed 15 | */ 16 | public function log(CacheOperationContext $context); 17 | 18 | /** 19 | * @return array|CacheOperationContext[] 20 | */ 21 | public function getCacheOperationContexts(); 22 | } 23 | -------------------------------------------------------------------------------- /Aop/Interceptor/CacheOperationInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | */ 8 | 9 | namespace Tbbc\CacheBundle\Aop\Interceptor; 10 | 11 | use CG\Proxy\MethodInvocation; 12 | use Tbbc\CacheBundle\Metadata\CacheMethodMetadataInterface; 13 | 14 | /** 15 | * @author Boris Guéry 16 | */ 17 | interface CacheOperationInterface 18 | { 19 | public function handle(CacheMethodMetadataInterface $methodMetadata, MethodInvocation $methodInvocation); 20 | } 21 | -------------------------------------------------------------------------------- /Metadata/CacheableMethodMetadata.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | */ 8 | 9 | namespace Tbbc\CacheBundle\Metadata; 10 | 11 | /** 12 | * Contains method metadata information 13 | * 14 | * @author Benjamin Dulau 15 | * @author Boris Guéry 16 | */ 17 | class CacheableMethodMetadata extends AbstractCacheMethodMetadata 18 | { 19 | public function getOperation() 20 | { 21 | return 'cacheable'; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Exception/UnsupportedKeyParameterException.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | */ 8 | 9 | namespace Tbbc\CacheBundle\Exception; 10 | 11 | /** 12 | * @author Boris Guéry 13 | */ 14 | class UnsupportedKeyParameterException extends InvalidArgumentException 15 | { 16 | public function __construct($param) 17 | { 18 | parent::__construct(sprintf('Only scalar values are allowed to be used as key, "%s" provided', gettype($param))); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Tests/Functional/FunctionalTestCase.php: -------------------------------------------------------------------------------- 1 | getContainer(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Logger/CacheLogger.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | namespace Tbbc\CacheBundle\Logger; 7 | 8 | use Tbbc\CacheBundle\Aop\Interceptor\CacheOperationContext; 9 | 10 | class CacheLogger implements CacheLoggerInterface 11 | { 12 | protected $cacheOperationContexts = array(); 13 | 14 | public function log(CacheOperationContext $context) 15 | { 16 | $this->cacheOperationContexts[] = $context; 17 | } 18 | 19 | public function getCacheOperationContexts() 20 | { 21 | return $this->cacheOperationContexts; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Tests/Functional/app/config/config.yml: -------------------------------------------------------------------------------- 1 | framework: 2 | secret: secret 3 | test: ~ 4 | 5 | services: 6 | book_service: 7 | class: Tbbc\CacheBundle\Tests\Functional\Fixtures\BookService 8 | tags: 9 | - { name: tbbc_cache.cache_eligible } 10 | 11 | tbbc_cache: 12 | annotations: { enabled: true } 13 | manager: simple_cache 14 | key_generator: simple_hash 15 | metadata: 16 | use_cache: false # Whether or not use metadata cache 17 | cache_dir: %kernel.cache_dir%/tbbc_cache 18 | cache: 19 | books: 20 | type: array 21 | -------------------------------------------------------------------------------- /Cache/KeyGenerator/KeyGeneratorInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | */ 8 | 9 | namespace Tbbc\CacheBundle\Cache\KeyGenerator; 10 | 11 | /** 12 | * Class KeyGeneratorInterface 13 | * 14 | * @author Benjamin Dulau 15 | */ 16 | interface KeyGeneratorInterface 17 | { 18 | /** 19 | * @param mixed $params An array of mixed values used for generating the key 20 | * @return string Unique key 21 | */ 22 | public function generateKey($params); 23 | } 24 | -------------------------------------------------------------------------------- /Resources/config/logger.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | Tbbc\CacheBundle\Logger\CacheLogger 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | ./Tests 11 | 12 | 13 | 14 | 15 | 16 | ./ 17 | 18 | ./vendor 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Tests/Functional/app/AppKernel.php: -------------------------------------------------------------------------------- 1 | load(__DIR__.'/config/config.yml'); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /TbbcCacheBundle.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | */ 8 | 9 | namespace Tbbc\CacheBundle; 10 | 11 | use Tbbc\CacheBundle\DependencyInjection\Compiler\CacheEligibleServicesPass; 12 | use Symfony\Component\DependencyInjection\ContainerBuilder; 13 | use Symfony\Component\HttpKernel\Bundle\Bundle; 14 | 15 | class TbbcCacheBundle extends Bundle 16 | { 17 | public function build(ContainerBuilder $container) 18 | { 19 | parent::build($container); 20 | 21 | $cacheEligibleServicesPass = new CacheEligibleServicesPass(); 22 | $container->addCompilerPass($cacheEligibleServicesPass); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Annotation/CacheEvict.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | */ 9 | 10 | namespace Tbbc\CacheBundle\Annotation; 11 | 12 | /** 13 | * Represents a @CacheEvict annotation. 14 | * 15 | * @Annotation 16 | * @Target("METHOD") 17 | * @author Benjamin Dulau 18 | */ 19 | final class CacheEvict extends Cache 20 | { 21 | public $allEntries = false; 22 | 23 | public function __construct(array $values) 24 | { 25 | parent::__construct($values); 26 | 27 | if (isset($values['allEntries'])) { 28 | $this->allEntries = 'true' == $values['allEntries'] ? true : false; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Cache/CacheManagerInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | */ 8 | 9 | namespace Tbbc\CacheBundle\Cache; 10 | 11 | /** 12 | * Interface CacheManager 13 | * 14 | * @author Benjamin Dulau 15 | */ 16 | interface CacheManagerInterface 17 | { 18 | /** 19 | * Returns the Cache associated with the given name. 20 | * 21 | * @param string $name 22 | * @return CacheInterface 23 | */ 24 | public function getCache($name); 25 | 26 | /** 27 | * Returns a list of registered cache names. 28 | * 29 | * @return array of string 30 | */ 31 | public function getCacheNames(); 32 | } 33 | -------------------------------------------------------------------------------- /Tests/Metadata/Driver/Fixtures/CacheableService.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | */ 8 | 9 | namespace Tbbc\CacheBundle\Metadata; 10 | 11 | /** 12 | * @author Boris Guéry 13 | */ 14 | class CacheEvictMethodMetadata extends AbstractCacheMethodMetadata 15 | { 16 | public $allEntries; 17 | 18 | public function serialize() 19 | { 20 | return serialize(array( 21 | parent::serialize(), 22 | $this->allEntries 23 | )); 24 | } 25 | 26 | public function unserialize($str) 27 | { 28 | list($parentStr, 29 | $this->allEntries 30 | ) = unserialize($str); 31 | 32 | parent::unserialize($parentStr); 33 | } 34 | 35 | public function getOperation() 36 | { 37 | return 'cache_evict'; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Resources/config/data_collectors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | Tbbc\CacheBundle\DataCollector\CacheableOperationDataCollector 8 | 9 | 10 | 11 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /CacheEvents.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | */ 9 | 10 | namespace Tbbc\CacheBundle; 11 | 12 | /** 13 | * Contains all events dispatched by the TbbcCacheBundle 14 | * 15 | * @author Benjamin Dulau 16 | */ 17 | final class CacheEvents 18 | { 19 | /** 20 | * The AFTER_CACHE_HIT event occurs after a cache hit on key fetch. 21 | * 22 | * This event gives the developer a chance to modify the fetched value 23 | * before it is returned. 24 | * 25 | * @var string 26 | */ 27 | const AFTER_CACHE_HIT = 'tbbc_cache.after_cache_hit'; 28 | 29 | /** 30 | * The AFTER_CACHE_UPDATE event occurs after a cache entry has been updated. 31 | * 32 | * @var string 33 | */ 34 | const AFTER_CACHE_UPDATE = 'tbbc_cache.after_cache_update'; 35 | } 36 | -------------------------------------------------------------------------------- /Metadata/AbstractCacheMethodMetadata.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | */ 8 | 9 | namespace Tbbc\CacheBundle\Metadata; 10 | 11 | use Metadata\MethodMetadata; 12 | 13 | /** 14 | * @author Boris Guéry 15 | */ 16 | abstract class AbstractCacheMethodMetadata extends MethodMetadata implements CacheMethodMetadataInterface 17 | { 18 | public $caches = array(); 19 | public $key; 20 | 21 | public function serialize() 22 | { 23 | return serialize(array( 24 | parent::serialize(), 25 | $this->caches, $this->key 26 | )); 27 | } 28 | 29 | public function unserialize($str) 30 | { 31 | list($parentStr, 32 | $this->caches, $this->key 33 | ) = unserialize($str); 34 | 35 | parent::unserialize($parentStr); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Tests/Functional/Fixtures/BookService.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | Tbbc\CacheBundle\Cache\KeyGenerator\SimpleHashKeyGenerator 9 | Tbbc\CacheBundle\Cache\KeyGenerator\LiteralKeyGenerator 10 | tbbc_cache.key_generator.simple_hash 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /Tests/Aop/Pointcut/CachePointcutTest.php: -------------------------------------------------------------------------------- 1 | metadataFactory); 14 | $pointcut->setEligibleClasses(array('stdClass')); 15 | 16 | $this->assertTrue($pointcut->matchesClass(new \ReflectionClass('stdClass'))); 17 | } 18 | 19 | public function testMatchesClassReturnFalseWhenEligibleClass() 20 | { 21 | $pointcut = new CachePointcut($this->metadataFactory); 22 | $pointcut->setEligibleClasses(array()); 23 | 24 | $this->assertFalse($pointcut->matchesClass(new \ReflectionClass('stdClass'))); 25 | } 26 | 27 | protected function setUp() 28 | { 29 | $this->metadataFactory = $this->getMock('Metadata\MetadataFactoryInterface'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Annotation/Cache.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | */ 9 | 10 | namespace Tbbc\CacheBundle\Annotation; 11 | 12 | use Tbbc\CacheBundle\Exception\InvalidArgumentException; 13 | 14 | /** 15 | * Base class for Cache annotations. 16 | * 17 | * @Annotation 18 | * @author Benjamin Dulau 19 | */ 20 | abstract class Cache 21 | { 22 | /** 23 | * @Required 24 | * @var mixed 25 | */ 26 | public $caches; 27 | 28 | /** 29 | * @Required 30 | * @var string 31 | */ 32 | public $key; 33 | 34 | public function __construct(array $values) 35 | { 36 | if (!isset($values['caches'])) { 37 | throw new InvalidArgumentException('You must define a "caches" attribute for each Cacheable annotation.'); 38 | } 39 | 40 | $this->caches = array_map('trim', explode(',', $values['caches'])); 41 | 42 | if (isset($values['key'])) { 43 | $this->key = $values['key']; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Cache/KeyGenerator/LiteralKeyGenerator.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | */ 8 | 9 | namespace Tbbc\CacheBundle\Cache\KeyGenerator; 10 | 11 | use Tbbc\CacheBundle\Exception\UnsupportedKeyParameterException; 12 | 13 | /** 14 | * Class LiteralKeyGenerator 15 | * 16 | * @author Boris Guéry 17 | */ 18 | class LiteralKeyGenerator implements KeyGeneratorInterface 19 | { 20 | /** 21 | * {@inheritDoc} 22 | */ 23 | public function generateKey($params) 24 | { 25 | $parameters = !is_array($params) ? array($params) : $params; 26 | 27 | $key = ''; 28 | foreach ($parameters as $parameter) { 29 | if (!is_scalar($parameter)) { 30 | 31 | throw new UnsupportedKeyParameterException($parameter); 32 | } 33 | $key .= preg_replace('/[^a-zA-Z0-9\s]/', '-', $parameter) . '_'; 34 | } 35 | 36 | $uniqueHash = sha1(serialize($parameters)); 37 | 38 | return $key . $uniqueHash; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Event/CacheEvent.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | */ 9 | 10 | namespace Tbbc\CacheBundle\Event; 11 | 12 | use Metadata\MethodMetadata; 13 | use Symfony\Component\EventDispatcher\Event; 14 | 15 | /** 16 | * Class CacheEvent 17 | * 18 | * @author Benjamin Dulau 19 | */ 20 | abstract class CacheEvent extends Event 21 | { 22 | /** 23 | * @var MethodMetadata 24 | */ 25 | protected $metadata; 26 | 27 | /** 28 | * @var string 29 | */ 30 | protected $key; 31 | 32 | public function __construct(MethodMetadata $metadata, $key) 33 | { 34 | $this->metadata = $metadata; 35 | $this->key = $key; 36 | } 37 | 38 | /** 39 | * @return string 40 | */ 41 | public function getKey() 42 | { 43 | return $this->key; 44 | } 45 | 46 | /** 47 | * @return \Metadata\MethodMetadata 48 | */ 49 | public function getMetadata() 50 | { 51 | return $this->metadata; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 The Big Brains Company 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | -------------------------------------------------------------------------------- /Resources/meta/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 TheBigBrainsCompany 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /Resources/config/cache.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | Tbbc\CacheBundle\Cache\CacheManager 10 | Tbbc\CacheBundle\Cache\SimpleCacheManager 11 | 12 | 13 | Tbbc\CacheBundle\Cache\DoctrineProxyCache 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Event/CacheUpdateEvent.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | */ 9 | 10 | namespace Tbbc\CacheBundle\Event; 11 | 12 | use Metadata\MethodMetadata; 13 | 14 | /** 15 | * Class CacheUpdateEvent 16 | * 17 | * @author Benjamin Dulau 18 | */ 19 | class CacheUpdateEvent extends CacheEvent 20 | { 21 | /** 22 | * Value returned by cache 23 | * 24 | * @var mixed 25 | */ 26 | protected $value; 27 | 28 | /** 29 | * Cache name 30 | * 31 | * @var string 32 | */ 33 | protected $cache; 34 | 35 | public function __construct(MethodMetadata $metadata, $cache, $key, $value) 36 | { 37 | parent::__construct($metadata, $key); 38 | $this->value = $value; 39 | $this->cache = $cache; 40 | } 41 | 42 | /** 43 | * @return mixed 44 | */ 45 | public function getValue() 46 | { 47 | return $this->value; 48 | } 49 | 50 | /** 51 | * @return string 52 | */ 53 | public function getCache() 54 | { 55 | return $this->cache; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /DependencyInjection/CacheFactory/CacheFactoryInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | */ 8 | 9 | namespace Tbbc\CacheBundle\DependencyInjection\CacheFactory; 10 | 11 | use Symfony\Component\Config\Definition\Builder\NodeBuilder; 12 | use Symfony\Component\DependencyInjection\ContainerBuilder; 13 | 14 | /** 15 | * Interface for the cache factories 16 | * 17 | * @author Benjamin Dulau 18 | */ 19 | interface CacheFactoryInterface 20 | { 21 | /** 22 | * Creates the cache, registers it and returns its id 23 | * 24 | * @param ContainerBuilder $container A ContainerBuilder instance 25 | * @param string $id The id of the service 26 | * @param array $config An array of configuration 27 | */ 28 | public function create(ContainerBuilder $container, $id, array $config); 29 | 30 | /** 31 | * Returns the key for the factory configuration 32 | * 33 | * @return string 34 | */ 35 | public function getKey(); 36 | 37 | /** 38 | * Adds configuration nodes for the factory 39 | * 40 | * @param NodeBuilder $builder 41 | */ 42 | public function addConfiguration(NodeBuilder $builder); 43 | } 44 | -------------------------------------------------------------------------------- /Event/CacheHitEvent.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | */ 9 | 10 | namespace Tbbc\CacheBundle\Event; 11 | 12 | use Metadata\MethodMetadata; 13 | 14 | /** 15 | * Class CacheHitEvent 16 | * 17 | * @author Benjamin Dulau 18 | */ 19 | class CacheHitEvent extends CacheEvent 20 | { 21 | /** 22 | * Value returned by cache 23 | * 24 | * @var mixed 25 | */ 26 | protected $value; 27 | 28 | /** 29 | * Cache name 30 | * 31 | * @var string 32 | */ 33 | protected $cache; 34 | 35 | public function __construct(MethodMetadata $metadata, $cache, $key, &$value) 36 | { 37 | parent::__construct($metadata, $key); 38 | $this->value = &$value; 39 | $this->cache = $cache; 40 | } 41 | 42 | /** 43 | * @return mixed 44 | */ 45 | public function getValue() 46 | { 47 | return $this->value; 48 | } 49 | 50 | /** 51 | * @param mixed $value 52 | */ 53 | public function setValue($value) 54 | { 55 | $this->value = $value; 56 | } 57 | 58 | /** 59 | * @return string 60 | */ 61 | public function getCache() 62 | { 63 | return $this->cache; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Resources/config/cache_factories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 10 | 11 | 12 | 13 | 15 | 16 | 17 | 18 | 20 | 21 | 22 | 23 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /Cache/KeyGenerator/SimpleHashKeyGenerator.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | */ 8 | 9 | namespace Tbbc\CacheBundle\Cache\KeyGenerator; 10 | 11 | use Tbbc\CacheBundle\Exception\UnsupportedKeyParameterException; 12 | 13 | /** 14 | * Class SimpleHashKeyGenerator 15 | * 16 | * @author Benjamin Dulau 17 | */ 18 | class SimpleHashKeyGenerator implements KeyGeneratorInterface 19 | { 20 | /** 21 | * {@inheritDoc} 22 | */ 23 | public function generateKey($params) 24 | { 25 | $parameters = !is_array($params) ? array($params) : $params; 26 | 27 | $hash = 1234; 28 | foreach ($parameters as $parameter) { 29 | if (null == $parameter) { 30 | $paramHash = 5678; 31 | } elseif (is_scalar($parameter)) { 32 | $paramHash = md5($parameter); 33 | } elseif (is_array($parameter) || is_object($parameter)) { 34 | $paramHash = md5(serialize($parameter)); 35 | } else { 36 | throw new UnsupportedKeyParameterException(sprintf('Not supported parameter type "%s"', 37 | gettype($parameter))); 38 | } 39 | 40 | $hash = $hash . $paramHash; 41 | } 42 | 43 | return base_convert($hash, 16, 10); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Resources/config/doctrine_caches.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | Doctrine\Common\Cache\MemcachedCache 8 | Doctrine\Common\Cache\ApcCache 9 | Doctrine\Common\Cache\ArrayCache 10 | Doctrine\Common\Cache\RedisCache 11 | 12 | 13 | 14 | 16 | 18 | 20 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tbbc/cache-bundle", 3 | "description": "Symfony Bundle - Cache abstraction and method annotations for controlling cache", 4 | "keywords": ["cache", "caching", "cache abstraction"], 5 | "type": "symfony-bundle", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Benjamin Dulau", 10 | "email": "benjamin.dulau@gmail.com", 11 | "homepage": "http://www.anonymation.com" 12 | }, 13 | { 14 | "name": "Boris Guéry", 15 | "email": "guery.b@gmail.com", 16 | "homepage": "http://borisguery.com" 17 | } 18 | ], 19 | "minimum-stability": "dev", 20 | "require": { 21 | "symfony/framework-bundle": ">=2.0", 22 | "doctrine/cache": ">=1.0", 23 | "jms/metadata": "1.*", 24 | "jms/aop-bundle": ">=1.0.0,<1.2-dev", 25 | "symfony/expression-language": "~2.4" 26 | }, 27 | "require-dev": { 28 | "phpunit/phpunit": "3.*", 29 | "symfony/class-loader": "~2.3", 30 | "symfony/yaml": "~2.3", 31 | "symfony/finder": "~2.3", 32 | "symfony/browser-kit": "~2.3" 33 | }, 34 | "suggest": { 35 | "doctrine/cache": "Allows to use Doctrine Cache library" 36 | }, 37 | "autoload": { 38 | "psr-0": { 39 | "Tbbc\\CacheBundle": "" 40 | } 41 | }, 42 | "target-dir": "Tbbc/CacheBundle" 43 | } 44 | -------------------------------------------------------------------------------- /Cache/CacheInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | */ 8 | 9 | namespace Tbbc\CacheBundle\Cache; 10 | 11 | /** 12 | * Interface Cache 13 | * 14 | * @author Benjamin Dulau 15 | */ 16 | interface CacheInterface 17 | { 18 | /** 19 | * Returns the cache name 20 | * 21 | * @return string 22 | */ 23 | public function getName(); 24 | 25 | /** 26 | * Return the value to which this cache maps the specified key. 27 | * 28 | * @param string $key The unique key of the cache entry to fetch. 29 | * @return mixed The cached data or null, if no cache entry exists for the given id. 30 | */ 31 | public function get($key); 32 | 33 | /** 34 | * Puts data into the cache. 35 | * 36 | * @param string $key 37 | * @param mixed $value 38 | * @return boolean TRUE if the entry was successfully stored in the cache, FALSE otherwise. 39 | */ 40 | public function set($key, $value); 41 | 42 | /** 43 | * Deletes the mapping for this key from this cache 44 | * 45 | * @param string $key 46 | * @return boolean TRUE if the cache entry was successfully deleted, FALSE otherwise. 47 | */ 48 | public function delete($key); 49 | 50 | /** 51 | * Invalidates all mappings from the cache 52 | * 53 | * @return mixed 54 | */ 55 | public function flush(); 56 | } 57 | -------------------------------------------------------------------------------- /Tests/Cache/KeyGenerator/LiteralKeyGeneratorTest.php: -------------------------------------------------------------------------------- 1 | generator = new LiteralKeyGenerator(); 27 | } 28 | 29 | public function testSingleScalarParameter() 30 | { 31 | $expected = 'foo_' . sha1(serialize(array('foo'))); 32 | 33 | $this->assertEquals($expected, $this->generator->generateKey('foo')); 34 | } 35 | 36 | public function testArrayOfScalarParameters() 37 | { 38 | $expected = 'foo_bar_' . sha1(serialize(array('foo', 'bar'))); 39 | 40 | $this->assertEquals($expected, $this->generator->generateKey(array('foo', 'bar'))); 41 | } 42 | 43 | /** 44 | * @expectedException Tbbc\CacheBundle\Exception\UnsupportedKeyParameterException 45 | */ 46 | public function testNotScalarParameterThrowsAnException() 47 | { 48 | $this->generator->generateKey(array(null, 'foo', 'bar')); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /DependencyInjection/Compiler/CacheEligibleServicesPass.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | */ 8 | 9 | namespace Tbbc\CacheBundle\DependencyInjection\Compiler; 10 | 11 | use Symfony\Component\DependencyInjection\ContainerBuilder; 12 | use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; 13 | 14 | /** 15 | * Collects classes eligible to cache annotations. 16 | * These are user defined by using the "tbbc_cache.cache_eligible" Tag. 17 | * Classes without this Tag won't match and "Cache" family annotations won't 18 | * work for them. 19 | * 20 | * @author Benjamin Dulau 21 | * @author Boris Guéry 22 | */ 23 | class CacheEligibleServicesPass implements CompilerPassInterface 24 | { 25 | public function process(ContainerBuilder $container) 26 | { 27 | // Do not process tags if annotations are not enabled 28 | if (true === $container->getParameter('tbbc_cache.annotations.enabled')) { 29 | $eligibleClasses = array(); 30 | foreach ($container->findTaggedServiceIds('tbbc_cache.cache_eligible') as $id => $attr) { 31 | $eligibleClasses[] = $container->getDefinition($id)->getClass(); 32 | } 33 | 34 | $container 35 | ->getDefinition('tbbc_cache.aop.pointcut.cache') 36 | ->addMethodCall('setEligibleClasses', array($eligibleClasses)) 37 | ; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Cache/CacheManager.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | */ 8 | 9 | namespace Tbbc\CacheBundle\Cache; 10 | 11 | use Tbbc\CacheBundle\Exception\InvalidArgumentException; 12 | 13 | /** 14 | * Abstract Class CacheManager 15 | * 16 | * @author Benjamin Dulau 17 | */ 18 | abstract class CacheManager implements CacheManagerInterface 19 | { 20 | /** 21 | * @var array of string 22 | */ 23 | protected $cacheNames; 24 | 25 | /** 26 | * @var array|CacheInterface[] 27 | */ 28 | protected $cacheMap; 29 | 30 | /** 31 | * Adds a Cache 32 | * 33 | * @param CacheInterface $cache 34 | */ 35 | public function addCache(CacheInterface $cache) 36 | { 37 | $this->cacheMap[$cache->getName()] = $cache; 38 | $this->cacheNames[] = $cache->getName(); 39 | } 40 | 41 | /** 42 | * {@inheritDoc} 43 | */ 44 | public function getCache($name) 45 | { 46 | if (!$this->hasCache($name)) { 47 | throw new InvalidArgumentException(sprintf('No cache with the name "%s" registered.', $name)); 48 | } 49 | 50 | return $this->cacheMap[$name]; 51 | } 52 | 53 | /** 54 | * Returns whether or not a cache with the given name is registered. 55 | * 56 | * @param string $name 57 | * @return bool 58 | */ 59 | public function hasCache($name) 60 | { 61 | return isset($this->cacheMap[$name]); 62 | } 63 | 64 | /** 65 | * {@inheritDoc} 66 | */ 67 | public function getCacheNames() 68 | { 69 | return $this->cacheNames; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /DependencyInjection/CacheFactory/ArrayCacheFactory.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | */ 8 | 9 | namespace Tbbc\CacheBundle\DependencyInjection\CacheFactory; 10 | 11 | use Symfony\Component\Config\Definition\Builder\NodeBuilder; 12 | use Symfony\Component\DependencyInjection\ContainerBuilder; 13 | use Symfony\Component\DependencyInjection\DefinitionDecorator; 14 | use Symfony\Component\DependencyInjection\Reference; 15 | 16 | /** 17 | * Factory for Array Cache 18 | * 19 | * @author Benjamin Dulau 20 | */ 21 | class ArrayCacheFactory implements CacheFactoryInterface 22 | { 23 | /** 24 | * {@inheritDoc} 25 | */ 26 | public function create(ContainerBuilder $container, $id, array $config) 27 | { 28 | 29 | $doctrineArrayId = sprintf('tbbc_cache.doctrine_cache.%s_array_instance', $config['name']); 30 | $container 31 | ->setDefinition($doctrineArrayId, new DefinitionDecorator('tbbc_cache.doctrine_cache.array')) 32 | ->setPublic(false) 33 | ; 34 | 35 | $container 36 | ->setDefinition($id, new DefinitionDecorator('tbbc_cache.cache.doctrine_proxy')) 37 | ->addArgument($config['name']) 38 | ->addArgument(new Reference($doctrineArrayId)) 39 | ; 40 | } 41 | 42 | /** 43 | * {@inheritDoc} 44 | */ 45 | public function getKey() 46 | { 47 | return 'array'; 48 | } 49 | 50 | /** 51 | * {@inheritDoc} 52 | */ 53 | public function addConfiguration(NodeBuilder $node) 54 | { 55 | // Array cache doesn't require any configuration to be set 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Tests/Aop/Interceptor/AbstractCacheOperationTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | */ 8 | 9 | namespace Tbbc\CacheBundle\Tests\Aop\Interceptor; 10 | 11 | /** 12 | * @author Boris Guéry 13 | */ 14 | abstract class AbstractCacheOperationTest extends \PHPUnit_Framework_TestCase 15 | { 16 | protected function getMethodInvocation() 17 | { 18 | $methodInvocation = $this->getMockBuilder('\CG\Proxy\MethodInvocation') 19 | ->disableOriginalConstructor() 20 | ->getMock() 21 | ; 22 | 23 | return $methodInvocation; 24 | } 25 | 26 | protected function getCache() 27 | { 28 | return $this->getMockBuilder('\Tbbc\CacheBundle\Cache\CacheInterface') 29 | ->getMock() 30 | ; 31 | } 32 | 33 | protected function getCacheManager() 34 | { 35 | return $this->getMockBuilder('\Tbbc\CacheBundle\Cache\CacheManagerInterface') 36 | ->getMock() 37 | ; 38 | } 39 | 40 | protected function getKeyGenerator() 41 | { 42 | return $this->getMockBuilder('\Tbbc\CacheBundle\Cache\KeyGenerator\KeyGeneratorInterface') 43 | ->getMock() 44 | ; 45 | } 46 | 47 | protected function getExpressionLanguage() 48 | { 49 | return $this->getMockBuilder('\Symfony\Component\ExpressionLanguage\ExpressionLanguage') 50 | ->getMock() 51 | ; 52 | } 53 | 54 | protected function getEventDispatcher() 55 | { 56 | return $this->getMockBuilder('\Symfony\Component\EventDispatcher\EventDispatcherInterface') 57 | ->getMock() 58 | ; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Aop/Pointcut/CachePointcut.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | */ 9 | 10 | namespace Tbbc\CacheBundle\Aop\Pointcut; 11 | 12 | use CG\Core\ClassUtils; 13 | use Metadata\MetadataFactoryInterface; 14 | use JMS\AopBundle\Aop\PointcutInterface; 15 | 16 | /** 17 | * Class CachePointcut 18 | * 19 | * @author Benjamin Dulau 20 | */ 21 | class CachePointcut implements PointcutInterface 22 | { 23 | private $metadataFactory; 24 | private $eligibleClasses = array(); 25 | 26 | public function __construct(MetadataFactoryInterface $metadataFactory) 27 | { 28 | $this->metadataFactory = $metadataFactory; 29 | } 30 | 31 | /** 32 | * {@inheritDoc} 33 | */ 34 | public function matchesMethod(\ReflectionMethod $method) 35 | { 36 | $userClass = ClassUtils::getUserClass($method->class); 37 | $metadata = $this->metadataFactory->getMetadataForClass($userClass); 38 | 39 | if (null === $metadata) { 40 | return false; 41 | } 42 | 43 | return isset($metadata->methodMetadata[$method->name]); 44 | } 45 | 46 | /** 47 | * {@inheritDoc} 48 | */ 49 | public function matchesClass(\ReflectionClass $class) 50 | { 51 | foreach ($this->eligibleClasses as $eligibleClass) { 52 | if ($class->name === $eligibleClass || $class->isSubclassOf($eligibleClass)) { 53 | return true; 54 | } 55 | } 56 | 57 | return false; 58 | } 59 | 60 | public function setEligibleClasses(array $eligibleClasses) 61 | { 62 | $this->eligibleClasses = $eligibleClasses; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /DependencyInjection/CacheFactory/ApcCacheFactory.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | */ 8 | 9 | namespace Tbbc\CacheBundle\DependencyInjection\CacheFactory; 10 | 11 | use Symfony\Component\Config\Definition\Builder\NodeBuilder; 12 | use Symfony\Component\DependencyInjection\ContainerBuilder; 13 | use Symfony\Component\DependencyInjection\DefinitionDecorator; 14 | use Symfony\Component\DependencyInjection\Reference; 15 | 16 | /** 17 | * Factory for APC Cache 18 | * 19 | * @author Boris Guéry 20 | */ 21 | class ApcCacheFactory implements CacheFactoryInterface 22 | { 23 | /** 24 | * {@inheritDoc} 25 | */ 26 | public function create(ContainerBuilder $container, $id, array $config) 27 | { 28 | 29 | $doctrineApcId = sprintf('tbbc_cache.doctrine_cache.%s_apc_instance', $config['name']); 30 | $container 31 | ->setDefinition($doctrineApcId, new DefinitionDecorator('tbbc_cache.doctrine_cache.apc')) 32 | ->setPublic(false) 33 | ; 34 | 35 | $container 36 | ->setDefinition($id, new DefinitionDecorator('tbbc_cache.cache.doctrine_proxy')) 37 | ->addArgument($config['name']) 38 | ->addArgument(new Reference($doctrineApcId)) 39 | ->addArgument($config['ttl']) 40 | ; 41 | } 42 | 43 | /** 44 | * {@inheritDoc} 45 | */ 46 | public function getKey() 47 | { 48 | return 'apc'; 49 | } 50 | 51 | /** 52 | * {@inheritDoc} 53 | */ 54 | public function addConfiguration(NodeBuilder $node) 55 | { 56 | $node 57 | ->scalarNode('ttl')->defaultNull()->end() 58 | ; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Cache/DoctrineProxyCache.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | */ 8 | 9 | namespace Tbbc\CacheBundle\Cache; 10 | 11 | use Doctrine\Common\Cache\CacheProvider; 12 | 13 | /** 14 | * Class MemcachedCache 15 | * For now, this is a proxy for Doctrine\Common\Cache\MemcachedCache 16 | * 17 | * @author Benjamin Dulau 18 | */ 19 | class DoctrineProxyCache implements CacheInterface 20 | { 21 | protected $name; 22 | protected $doctrineCache; 23 | protected $ttl; 24 | 25 | public function __construct($name, CacheProvider $doctrineCacheProvider, $ttl = null) 26 | { 27 | $this->name = $name; 28 | $this->doctrineCache = $doctrineCacheProvider; 29 | $this->doctrineCache->setNamespace($name); 30 | $this->ttl = $ttl; 31 | } 32 | 33 | /** 34 | * {@inheritDoc} 35 | */ 36 | public function get($key) 37 | { 38 | if (false === ($value = $this->doctrineCache->fetch($key))) { 39 | return null; 40 | } 41 | 42 | return $value; 43 | } 44 | 45 | /** 46 | * {@inheritDoc} 47 | */ 48 | public function set($key, $value) 49 | { 50 | return $this->doctrineCache->save($key, $value, (int) $this->ttl); 51 | } 52 | 53 | /** 54 | * {@inheritDoc} 55 | */ 56 | public function delete($key) 57 | { 58 | return $this->doctrineCache->delete($key); 59 | } 60 | 61 | /** 62 | * {@inheritDoc} 63 | */ 64 | public function flush() 65 | { 66 | return $this->doctrineCache->flushAll(); 67 | } 68 | 69 | /** 70 | * {@inheritDoc} 71 | */ 72 | public function getName() 73 | { 74 | return $this->name; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Aop/Interceptor/CacheUpdateOperation.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | */ 8 | 9 | namespace Tbbc\CacheBundle\Aop\Interceptor; 10 | 11 | use CG\Proxy\MethodInvocation; 12 | use Tbbc\CacheBundle\CacheEvents; 13 | use Tbbc\CacheBundle\Event\CacheUpdateEvent; 14 | use Tbbc\CacheBundle\Exception\InvalidArgumentException; 15 | use Tbbc\CacheBundle\Metadata\CacheMethodMetadataInterface; 16 | use Tbbc\CacheBundle\Metadata\CacheUpdateMethodMetadata; 17 | 18 | /** 19 | * @author Benjamin Dulau 20 | */ 21 | class CacheUpdateOperation extends AbstractCacheOperation 22 | { 23 | public function handle(CacheMethodMetadataInterface $methodMetadata, MethodInvocation $methodInvocation) 24 | { 25 | if (!$methodMetadata instanceof CacheUpdateMethodMetadata) { 26 | throw new InvalidArgumentException(sprintf('%s does only support "CacheUpdateMethodMetadata" objects', __CLASS__ )); 27 | } 28 | 29 | $returnValue = $methodInvocation->proceed(); 30 | 31 | $this->cacheOperationContext->setTargetClass($methodMetadata->class); 32 | $this->cacheOperationContext->setTargetMethod($methodMetadata->name); 33 | 34 | $cacheKey = $this->generateCacheKey($methodMetadata, $methodInvocation); 35 | 36 | // Updates all caches 37 | foreach ($methodMetadata->caches as $cacheName) { 38 | $this->getCacheManager()->getCache($cacheName)->set($cacheKey, $returnValue); 39 | 40 | $this->cacheOperationContext->addCacheUpdate($cacheName); 41 | 42 | $event = new CacheUpdateEvent($methodMetadata, $cacheName, $cacheKey, $returnValue); 43 | $this->dispatcher->dispatch(CacheEvents::AFTER_CACHE_UPDATE, $event); 44 | } 45 | 46 | return $returnValue; 47 | } 48 | 49 | public function getOperationName() 50 | { 51 | return 'cache_update'; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Tests/Cache/KeyGenerator/SimpleHashKeyGeneratorTest.php: -------------------------------------------------------------------------------- 1 | generator = new SimpleHashKeyGenerator(); 19 | } 20 | 21 | public function testSingleScalarParameter() 22 | { 23 | $expected = base_convert(1234 . md5('foo'), 16, 10); 24 | 25 | $this->assertEquals($expected, $this->generator->generateKey('foo')); 26 | } 27 | 28 | public function testArrayOfScalarParameters() 29 | { 30 | $expected = base_convert(1234 . md5('foo') . md5('bar'), 16, 10); 31 | 32 | $this->assertEquals($expected, $this->generator->generateKey(array('foo', 'bar'))); 33 | } 34 | 35 | public function testNullParameter() 36 | { 37 | $expected = base_convert(1234 . 5678, 16, 10); 38 | 39 | $this->assertEquals($expected, $this->generator->generateKey(null)); 40 | } 41 | 42 | public function testArrayParameter() 43 | { 44 | $parameter = array('foo', 'bar'); 45 | $expected = base_convert(1234 . md5(serialize($parameter)), 16, 10); 46 | 47 | $this->assertEquals($expected, $this->generator->generateKey(array($parameter))); 48 | } 49 | 50 | public function testArrayOfMixedParameter() 51 | { 52 | $param1 = new Foo(); 53 | $param2 = array('foo', 'bar'); 54 | $param3 = 'foo'; 55 | $param4 = null; 56 | 57 | $expected = base_convert(1234 . md5(serialize($param1)) . md5(serialize($param2)) . md5($param3) . 5678, 16, 10); 58 | 59 | $this->assertEquals($expected, $this->generator->generateKey(array( 60 | $param1, 61 | $param2, 62 | $param3, 63 | $param4 64 | ))); 65 | } 66 | } 67 | 68 | class Foo 69 | { 70 | public $bar = 'bar'; 71 | } 72 | -------------------------------------------------------------------------------- /Aop/Interceptor/CacheEvictOperation.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | */ 8 | 9 | namespace Tbbc\CacheBundle\Aop\Interceptor; 10 | 11 | use CG\Proxy\MethodInvocation; 12 | use Tbbc\CacheBundle\Exception\InvalidArgumentException; 13 | use Tbbc\CacheBundle\Metadata\CacheEvictMethodMetadata; 14 | use Tbbc\CacheBundle\Metadata\CacheMethodMetadataInterface; 15 | 16 | /** 17 | * @author Boris Guéry 18 | */ 19 | class CacheEvictOperation extends AbstractCacheOperation 20 | { 21 | public function handle(CacheMethodMetadataInterface $methodMetadata, MethodInvocation $methodInvocation) 22 | { 23 | if (!$methodMetadata instanceof CacheEvictMethodMetadata) { 24 | throw new InvalidArgumentException(sprintf('%s does only support "CacheEvictMethodMetadata" objects', __CLASS__ )); 25 | } 26 | 27 | $this->cacheOperationContext->setTargetClass($methodMetadata->class); 28 | $this->cacheOperationContext->setTargetMethod($methodMetadata->name); 29 | $this->cacheOperationContext->setCaches($methodMetadata->caches); 30 | // TODO: Set before invocation on operation context 31 | // $this->cacheOperationContext->setBeforeInvocation($methodMetadata->beforeInvocation) 32 | 33 | $returnValue = $methodInvocation->proceed(); 34 | 35 | if ($methodMetadata->allEntries) { 36 | $this->cacheOperationContext->setFlush(true); 37 | foreach ($methodMetadata->caches as $cacheName) { 38 | $this->getCacheManager()->getCache($cacheName)->flush(); 39 | } 40 | } else { 41 | $cacheKey = $this->generateCacheKey($methodMetadata, $methodInvocation); 42 | $this->cacheOperationContext->setKey($cacheKey); 43 | 44 | foreach ($methodMetadata->caches as $cacheName) { 45 | $this->getCacheManager()->getCache($cacheName)->delete($cacheKey); 46 | } 47 | } 48 | 49 | return $returnValue; 50 | } 51 | 52 | public function getOperationName() 53 | { 54 | return 'cache_evict'; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Tests/Metadata/Driver/AnnotationDriverTest.php: -------------------------------------------------------------------------------- 1 | loadMetadataForClass(new \ReflectionClass('Tbbc\CacheBundle\Tests\Metadata\Driver\Fixtures\CacheableService')); 16 | 17 | $this->assertArrayHasKey('findFoo', $metadata->methodMetadata); 18 | 19 | $this->assertEquals(array('foo_cache'), $metadata->methodMetadata['findFoo']->caches); 20 | $this->assertEquals('cacheable', $metadata->methodMetadata['findFoo']->getOperation()); 21 | } 22 | 23 | public function testLoadMetadataWithMethodCacheEvict() 24 | { 25 | $driver = new AnnotationDriver(new AnnotationReader()); 26 | 27 | $metadata = $driver->loadMetadataForClass(new \ReflectionClass('Tbbc\CacheBundle\Tests\Metadata\Driver\Fixtures\CacheableService')); 28 | 29 | $this->assertArrayHasKey('saveFoo', $metadata->methodMetadata); 30 | 31 | $this->assertEquals(array('foo_cache'), $metadata->methodMetadata['saveFoo']->caches); 32 | $this->assertEquals('cache_evict', $metadata->methodMetadata['saveFoo']->getOperation()); 33 | $this->assertEquals(new Expression('#foo'), $metadata->methodMetadata['saveFoo']->key); 34 | } 35 | 36 | public function testLoadMetadataWithMethodCacheEvictAndAllEntriesSetsToTrue() 37 | { 38 | $driver = new AnnotationDriver(new AnnotationReader()); 39 | 40 | $metadata = $driver->loadMetadataForClass(new \ReflectionClass('Tbbc\CacheBundle\Tests\Metadata\Driver\Fixtures\CacheableService')); 41 | 42 | $method = 'saveFooAndEvictAllEntries'; 43 | 44 | $this->assertArrayHasKey('saveFooAndEvictAllEntries', $metadata->methodMetadata); 45 | $this->assertEquals(array('foo_cache'), $metadata->methodMetadata[$method]->caches); 46 | $this->assertEquals('cache_evict', $metadata->methodMetadata[$method]->getOperation()); 47 | $this->assertTrue($metadata->methodMetadata[$method]->allEntries); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /DependencyInjection/CacheFactory/RedisCacheFactory.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | */ 8 | 9 | namespace Tbbc\CacheBundle\DependencyInjection\CacheFactory; 10 | 11 | use Symfony\Component\Config\Definition\Builder\NodeBuilder; 12 | use Symfony\Component\DependencyInjection\ContainerBuilder; 13 | use Symfony\Component\DependencyInjection\Definition; 14 | use Symfony\Component\DependencyInjection\DefinitionDecorator; 15 | use Symfony\Component\DependencyInjection\Reference; 16 | 17 | /** 18 | * Factory for Redis Cache 19 | * 20 | * @author Benjamin Dulau 21 | * @author Armen Mkrtchyan 22 | */ 23 | class RedisCacheFactory implements CacheFactoryInterface 24 | { 25 | /** 26 | * {@inheritDoc} 27 | */ 28 | public function create(ContainerBuilder $container, $id, array $config) 29 | { 30 | 31 | $redis = new Definition('Redis'); 32 | $redis->addMethodCall('connect', array( 33 | $config['server']['host'], 34 | $config['server']['port'] 35 | )); 36 | 37 | $redis->setPublic(false); 38 | $redisId = sprintf('tbbc_cache.%s_redis_instance', $config['name']); 39 | $container->setDefinition($redisId, $redis); 40 | 41 | $doctrineRedisId = sprintf('tbbc_cache.doctrine_cache.%s_redis_instance', $config['name']); 42 | $container 43 | ->setDefinition($doctrineRedisId, new DefinitionDecorator('tbbc_cache.doctrine_cache.redis')) 44 | ->addMethodCall('setRedis', array(new Reference($redisId))) 45 | ->setPublic(false); 46 | 47 | $container 48 | ->setDefinition($id, new DefinitionDecorator('tbbc_cache.cache.doctrine_proxy')) 49 | ->addArgument($config['name']) 50 | ->addArgument(new Reference($doctrineRedisId)) 51 | ->addArgument($config['ttl']); 52 | } 53 | 54 | /** 55 | * {@inheritDoc} 56 | */ 57 | public function getKey() 58 | { 59 | return 'redis'; 60 | } 61 | 62 | /** 63 | * {@inheritDoc} 64 | */ 65 | public function addConfiguration(NodeBuilder $node) 66 | { 67 | $node 68 | ->scalarNode('ttl')->defaultNull()->end() 69 | ->arrayNode('server') 70 | ->children() 71 | ->scalarNode('host')->isRequired()->cannotBeEmpty()->end() 72 | ->scalarNode('port')->defaultValue(6379)->end() 73 | ->end() 74 | ->end(); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Tests/Aop/Interceptor/CacheInterceptorTest.php: -------------------------------------------------------------------------------- 1 | getInterceptor(); 19 | 20 | $this->getInvocation($interceptor)->proceed(); 21 | } 22 | 23 | protected function getInvocation(CacheInterceptor $interceptor, $method = 'findSomething', $arguments = array()) 24 | { 25 | if ('findSomething' === $method && 0 === count($arguments)) { 26 | $arguments = array(new \stdClass(), new \stdClass()); 27 | } 28 | $object = new CacheableService(); 29 | 30 | return new MethodInvocation(new \ReflectionMethod($object, $method), $object, $arguments, array($interceptor)); 31 | } 32 | 33 | protected function getInterceptor(MetadataFactoryInterface $metadataFactory = null) 34 | { 35 | if (null === $metadataFactory) { 36 | $metadataFactory = $this->getMock('Metadata\MetadataFactoryInterface'); 37 | 38 | $metadata = new ClassMetadata('Tbbc\CacheBundle\Tests\Aop\Interceptor\CacheableService'); 39 | $metadata->methodMetadata['findSomething'] = new CacheableMethodMetadata('Tbbc\CacheBundle\Tests\Aop\Interceptor\CacheableService', 'findSomething'); 40 | 41 | $metadataFactory 42 | ->expects($this->once()) 43 | ->method('getMetadataForClass') 44 | ->with($this->equalTo('Tbbc\CacheBundle\Tests\Aop\Interceptor\CacheableService')) 45 | ->will($this->returnValue($metadata)) 46 | ; 47 | } 48 | 49 | $cacheManager = $this->getMock('Tbbc\CacheBundle\Cache\CacheManagerInterface'); 50 | $keyGenerator = $this->getMock('Tbbc\CacheBundle\Cache\KeyGenerator\KeyGeneratorInterface'); 51 | $expressionLanguage = $this->getMock('\Symfony\Component\ExpressionLanguage\ExpressionLanguage'); 52 | $eventDispatcher = $this->getMock('\Symfony\Component\EventDispatcher\EventDispatcherInterface'); 53 | 54 | return array( 55 | new CacheInterceptor($metadataFactory, $cacheManager, $keyGenerator, $expressionLanguage, $eventDispatcher), 56 | $cacheManager, 57 | $keyGenerator, 58 | ); 59 | } 60 | } 61 | 62 | class CacheableService 63 | { 64 | public function findSomething($foo) 65 | { 66 | return $foo; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Aop/Interceptor/CacheableOperation.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | */ 8 | 9 | namespace Tbbc\CacheBundle\Aop\Interceptor; 10 | 11 | use CG\Proxy\MethodInvocation; 12 | use Tbbc\CacheBundle\CacheEvents; 13 | use Tbbc\CacheBundle\Event\CacheHitEvent; 14 | use Tbbc\CacheBundle\Event\CacheUpdateEvent; 15 | use Tbbc\CacheBundle\Exception\InvalidArgumentException; 16 | use Tbbc\CacheBundle\Metadata\CacheMethodMetadataInterface; 17 | use Tbbc\CacheBundle\Metadata\CacheableMethodMetadata; 18 | 19 | /** 20 | * @author Boris Guéry 21 | * @author Benjamin Dulau 22 | */ 23 | class CacheableOperation extends AbstractCacheOperation 24 | { 25 | public function handle(CacheMethodMetadataInterface $methodMetadata, MethodInvocation $methodInvocation) 26 | { 27 | if (!$methodMetadata instanceof CacheableMethodMetadata) { 28 | throw new InvalidArgumentException(sprintf('%s does only support "CacheableMethodMetadata" objects', __CLASS__ )); 29 | } 30 | 31 | $cacheKey = $this->generateCacheKey($methodMetadata, $methodInvocation); 32 | 33 | $this->cacheOperationContext->setTargetClass($methodMetadata->class); 34 | $this->cacheOperationContext->setTargetMethod($methodMetadata->name); 35 | $this->cacheOperationContext->setCaches($methodMetadata->caches); 36 | 37 | $returnValue = null; 38 | foreach ($methodMetadata->caches as $cacheName) { 39 | if (null !== ($returnValue = $this->getCacheManager()->getCache($cacheName)->get($cacheKey))) { 40 | 41 | break; 42 | } 43 | } 44 | 45 | // Cache hit 46 | if (null !== $returnValue) { 47 | $this->cacheOperationContext->setCacheHit($cacheName); 48 | 49 | $event = new CacheHitEvent($methodMetadata, $cacheName, $cacheKey, $returnValue); 50 | $this->dispatcher->dispatch(CacheEvents::AFTER_CACHE_HIT, $event); 51 | 52 | return $returnValue; 53 | } 54 | 55 | // Cache miss 56 | $this->cacheOperationContext->setCacheMiss($methodMetadata->caches); 57 | $returnValue = $methodInvocation->proceed(); 58 | 59 | foreach ($methodMetadata->caches as $cacheName) { 60 | $this->getCacheManager()->getCache($cacheName)->set($cacheKey, $returnValue); 61 | 62 | $this->cacheOperationContext->addCacheUpdate($cacheName); 63 | 64 | $event = new CacheUpdateEvent($methodMetadata, $cacheName, $cacheKey, $returnValue); 65 | $this->dispatcher->dispatch(CacheEvents::AFTER_CACHE_UPDATE, $event); 66 | } 67 | 68 | return $returnValue; 69 | } 70 | 71 | public function getOperationName() 72 | { 73 | return 'cacheable'; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /DataCollector/CacheableOperationDataCollector.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | namespace Tbbc\CacheBundle\DataCollector; 7 | 8 | use Tbbc\CacheBundle\Logger\CacheLoggerInterface; 9 | use Symfony\Component\HttpFoundation\Request; 10 | use Symfony\Component\HttpFoundation\Response; 11 | use Symfony\Component\HttpKernel\DataCollector\DataCollector; 12 | 13 | class CacheableOperationDataCollector extends DataCollector 14 | { 15 | private $cacheLogger; 16 | 17 | public function __construct(CacheLoggerInterface $cacheLogger) 18 | { 19 | $this->cacheLogger = $cacheLogger; 20 | } 21 | 22 | public function collect(Request $request, Response $response, \Exception $exception = null) 23 | { 24 | $cacheableOperations = array(); 25 | $cacheEvictOperations = array(); 26 | $cacheUpdateOperations = array(); 27 | $hits = 0; 28 | $miss = 0; 29 | 30 | foreach ($this->cacheLogger->getCacheOperationContexts() as $context) { 31 | if ('cacheable' == $context->getOperation()) { 32 | $cacheableOperations[] = $context; 33 | if (null !== $context->getCacheHit()) { 34 | $hits++; 35 | } elseif (count($context->getCacheMiss())) { 36 | $miss++; 37 | } 38 | } 39 | if ('cache_evict' == $context->getOperation()) { 40 | $cacheEvictOperations[] = $context; 41 | } 42 | if ('cache_update' == $context->getOperation()) { 43 | $cacheUpdateOperations[] = $context; 44 | } 45 | } 46 | 47 | $this->data = array( 48 | 'hits' => $hits, 49 | 'miss' => $miss, 50 | 'operations' => $this->cacheLogger->getCacheOperationContexts(), 51 | 'cacheableOperations' => $cacheableOperations, 52 | 'cacheEvictOperations' => $cacheEvictOperations, 53 | 'cacheUpdateOperations' => $cacheUpdateOperations, 54 | ); 55 | } 56 | 57 | public function getHits() 58 | { 59 | return $this->data['hits']; 60 | } 61 | 62 | public function getMiss() 63 | { 64 | return $this->data['miss']; 65 | } 66 | 67 | public function getOperations() 68 | { 69 | return $this->data['operations']; 70 | } 71 | 72 | public function getCacheableOperations() 73 | { 74 | return $this->data['cacheableOperations']; 75 | } 76 | 77 | public function getCacheEvictOperations() 78 | { 79 | return $this->data['cacheEvictOperations']; 80 | } 81 | 82 | public function getCacheUpdateOperations() 83 | { 84 | return $this->data['cacheUpdateOperations']; 85 | } 86 | 87 | public function getName() 88 | { 89 | return 'tbbc_cache'; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Resources/config/aop.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | Tbbc\CacheBundle\Aop\Interceptor\CacheInterceptor 9 | Tbbc\CacheBundle\Aop\Pointcut\CachePointcut 10 | Metadata\MetadataFactory 11 | Metadata\Driver\LazyLoadingDriver 12 | Tbbc\CacheBundle\Metadata\Driver\AnnotationDriver 13 | Metadata\Driver\DriverChain 14 | Metadata\Cache\FileCache 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | %tbbc_cache.metadata.cache_dir% 26 | %kernel.debug% 27 | 28 | 29 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /DependencyInjection/CacheFactory/MemcachedCacheFactory.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | */ 8 | 9 | namespace Tbbc\CacheBundle\DependencyInjection\CacheFactory; 10 | 11 | use Symfony\Component\Config\Definition\Builder\NodeBuilder; 12 | use Symfony\Component\DependencyInjection\ContainerBuilder; 13 | use Symfony\Component\DependencyInjection\Definition; 14 | use Symfony\Component\DependencyInjection\DefinitionDecorator; 15 | use Symfony\Component\DependencyInjection\Reference; 16 | 17 | /** 18 | * Factory for Memcached Cache 19 | * 20 | * @author Benjamin Dulau 21 | */ 22 | class MemcachedCacheFactory implements CacheFactoryInterface 23 | { 24 | /** 25 | * {@inheritDoc} 26 | */ 27 | public function create(ContainerBuilder $container, $id, array $config) 28 | { 29 | $memcached = new Definition('Memcached'); 30 | foreach ($config['servers'] as $server) { 31 | $memcached->addMethodCall('addServer', array( 32 | $server['host'], 33 | $server['port'], 34 | )); 35 | } 36 | $memcached->setPublic(false); 37 | $memcachedId = sprintf('tbbc_cache.%s_memcached_instance', $config['name']); 38 | $container->setDefinition($memcachedId, $memcached); 39 | 40 | $doctrineMemcachedId = sprintf('tbbc_cache.doctrine_cache.%s_memcached_instance', $config['name']); 41 | $container 42 | ->setDefinition($doctrineMemcachedId, new DefinitionDecorator('tbbc_cache.doctrine_cache.memcached')) 43 | ->addMethodCall('setMemcached', array(new Reference($memcachedId))) 44 | ->setPublic(false) 45 | ; 46 | 47 | $container 48 | ->setDefinition($id, new DefinitionDecorator('tbbc_cache.cache.doctrine_proxy')) 49 | ->addArgument($config['name']) 50 | ->addArgument(new Reference($doctrineMemcachedId)) 51 | ->addArgument($config['ttl']) 52 | ; 53 | } 54 | 55 | /** 56 | * {@inheritDoc} 57 | */ 58 | public function getKey() 59 | { 60 | return 'memcached'; 61 | } 62 | 63 | /** 64 | * {@inheritDoc} 65 | */ 66 | public function addConfiguration(NodeBuilder $node) 67 | { 68 | $node 69 | ->scalarNode('ttl')->defaultNull()->end() 70 | ->arrayNode('servers') 71 | ->useAttributeAsKey('name') 72 | ->prototype('array') 73 | ->children() 74 | ->scalarNode('host')->isRequired()->cannotBeEmpty()->end() 75 | ->scalarNode('port')->defaultValue(11211)->end() 76 | ->end() 77 | ->end() 78 | ->defaultValue(array( 79 | 'localhost' => array( 80 | 'host' => 'localhost', 81 | 'port' => 11211, 82 | ) 83 | )) 84 | ->end() 85 | ; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Tests/Aop/Interceptor/CacheUpdateOperationTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | */ 8 | 9 | namespace Tbbc\CacheBundle\Tests\Aop\Interceptor; 10 | 11 | use Tbbc\CacheBundle\Aop\Interceptor\CacheUpdateOperation; 12 | 13 | /** 14 | * @author Benjamin Dulau 15 | */ 16 | class CacheUpdateOperationTest extends AbstractCacheOperationTest 17 | { 18 | /** 19 | * @expectedException \Tbbc\CacheBundle\Exception\InvalidArgumentException 20 | */ 21 | public function testHandleWithWrongMethodMetadataThrowsAnException() 22 | { 23 | $methodInvocation = $this->getMethodInvocation(); 24 | $methodInvocation 25 | ->expects($this->never()) 26 | ->method('proceed') 27 | ; 28 | 29 | $incorrectMethodMetadata = $this->getMockBuilder('Tbbc\CacheBundle\Metadata\CacheMethodMetadataInterface') 30 | ->disableOriginalConstructor() 31 | ->getMock() 32 | ; 33 | 34 | $operation = new CacheUpdateOperation( 35 | $this->getCacheManager(), 36 | $this->getKeyGenerator(), 37 | $this->getExpressionLanguage(), 38 | $this->getEventDispatcher() 39 | ); 40 | 41 | $operation->handle($incorrectMethodMetadata, $this->getMethodInvocation()); 42 | } 43 | 44 | public function testCacheUpdateOperationProperlySetValueToCache() 45 | { 46 | $methodInvocation = $this->getMethodInvocation(); 47 | $methodInvocation 48 | ->expects($this->once()) 49 | ->method('proceed') 50 | ->will($this->returnValue('toBeCachedValue')) 51 | ; 52 | 53 | $keyGenerator = $this->getKeyGenerator(); 54 | $keyGenerator 55 | ->expects($this->once()) 56 | ->method('generateKey') 57 | ->withAnyParameters() 58 | ->will($this->returnValue('toBeCachedValue')) 59 | ; 60 | 61 | $cache = $this->getCache(); 62 | $cache 63 | ->expects($this->once()) 64 | ->method('set') 65 | ->with($this->equalTo('toBeCachedValue'), $this->equalTo('toBeCachedValue')) 66 | ->will($this->returnValue(true)) 67 | ; 68 | 69 | $cacheManager = $this->getCacheManager(); 70 | $cacheManager 71 | ->expects($this->once()) 72 | ->method('getCache') 73 | ->with($this->equalTo('cache_name')) // @see MethodMetadata mock 74 | ->will($this->returnValue($cache)) 75 | ; 76 | 77 | $operation = new CacheUpdateOperation( 78 | $cacheManager, 79 | $keyGenerator, 80 | $this->getExpressionLanguage(), 81 | $this->getEventDispatcher() 82 | ); 83 | 84 | $actualResult = $operation->handle($this->getMethodMetadata(), $methodInvocation); 85 | 86 | $this->assertSame('toBeCachedValue', $actualResult); 87 | } 88 | 89 | protected function getMethodMetadata() 90 | { 91 | $metadata = $this->getMockBuilder('Tbbc\CacheBundle\Metadata\CacheUpdateMethodMetadata') 92 | ->disableOriginalConstructor() 93 | ->getMock() 94 | ; 95 | 96 | $metadata->caches = array('cache_name'); 97 | $metadata->key = null; // @see KeyGenerator Mock, always returns foo 98 | 99 | return $metadata; 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /Metadata/Driver/AnnotationDriver.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | */ 8 | 9 | namespace Tbbc\CacheBundle\Metadata\Driver; 10 | 11 | use Symfony\Component\ExpressionLanguage\Expression; 12 | use Tbbc\CacheBundle\Annotation\Cache; 13 | use Tbbc\CacheBundle\Annotation\CacheEvict; 14 | use Tbbc\CacheBundle\Annotation\CacheUpdate; 15 | use Tbbc\CacheBundle\Annotation\Cacheable; 16 | use Tbbc\CacheBundle\Metadata\CacheEvictMethodMetadata; 17 | use Tbbc\CacheBundle\Metadata\CacheUpdateMethodMetadata; 18 | use Tbbc\CacheBundle\Metadata\CacheableMethodMetadata; 19 | use Tbbc\CacheBundle\Metadata\ClassMetadata; 20 | use Metadata\Driver\DriverInterface; 21 | use Doctrine\Common\Annotations\Reader; 22 | use \ReflectionClass; 23 | use \ReflectionMethod; 24 | 25 | /** 26 | * Loads cache annotations and converts them to metadata 27 | * 28 | * @author Benjamin Dulau 29 | */ 30 | class AnnotationDriver implements DriverInterface 31 | { 32 | private $reader; 33 | 34 | public function __construct(Reader $reader) 35 | { 36 | $this->reader = $reader; 37 | } 38 | 39 | public function loadMetadataForClass(ReflectionClass $reflection) 40 | { 41 | $classMetadata = new ClassMetadata($reflection->getName()); 42 | 43 | foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_PROTECTED) as $method) { 44 | // check if the method was defined on this class (vs abstract) 45 | if ($method->getDeclaringClass()->getName() !== $reflection->getName()) { 46 | continue; 47 | } 48 | 49 | $annotations = $this->reader->getMethodAnnotations($method); 50 | if (!$annotations) { 51 | continue; 52 | } 53 | 54 | if (null !== $methodMetadata = $this->convertMethodAnnotations($method, $annotations)) { 55 | $classMetadata->addMethodMetadata($methodMetadata); 56 | } 57 | } 58 | 59 | return $classMetadata; 60 | } 61 | 62 | protected function convertMethodAnnotations(\ReflectionMethod $method, array $annotations) 63 | { 64 | $parameters = array(); 65 | foreach ($method->getParameters() as $index => $parameter) { 66 | $parameters[$parameter->getName()] = $index; 67 | } 68 | 69 | $hasCacheMetadata = false; 70 | $methodMetadata = null; 71 | 72 | foreach ($annotations as $annotation) { 73 | if ($annotation instanceof Cache) { 74 | if ($annotation instanceof Cacheable) { 75 | $methodMetadata = new CacheableMethodMetadata($method->class, $method->name); 76 | } elseif ($annotation instanceof CacheEvict) { 77 | $methodMetadata = new CacheEvictMethodMetadata($method->class, $method->name); 78 | $methodMetadata->allEntries = $annotation->allEntries; 79 | } elseif ($annotation instanceof CacheUpdate) { 80 | $methodMetadata = new CacheUpdateMethodMetadata($method->class, $method->name); 81 | } else { 82 | 83 | continue; 84 | } 85 | 86 | $methodMetadata->caches = $annotation->caches; 87 | if (!empty($annotation->key)) { 88 | $methodMetadata->key = new Expression($annotation->key); 89 | } 90 | 91 | $hasCacheMetadata = true; 92 | } 93 | } 94 | 95 | return $hasCacheMetadata ? $methodMetadata : null; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Tests/Functional/CacheAnnotationTest.php: -------------------------------------------------------------------------------- 1 | bookService = $this->getContainer()->get('book_service'); 32 | $this->keyGenerator = $this->getContainer()->get('tbbc_cache.key_generator.simple_hash'); 33 | $this->cacheManager = $this->getContainer()->get('tbbc_cache.simple_cache_manager'); 34 | } 35 | 36 | public function testCacheableCachesTheValue() 37 | { 38 | $isbn = 'foobar123'; 39 | $cacheKey = $this->keyGenerator->generateKey($isbn); 40 | 41 | $book = $this->bookService->getBookByIsbn($isbn); 42 | $cachedBook = $this->cacheManager->getCache('books')->get($cacheKey); 43 | 44 | $this->assertEquals($book, $cachedBook); 45 | } 46 | 47 | public function testCacheableCachesTheValueAndUsesTheCachedValueOnNextCalls() 48 | { 49 | $isbn = 'baz456'; 50 | $cacheKey = $this->keyGenerator->generateKey($isbn); 51 | 52 | $book = $this->bookService->getBookByIsbn($isbn); 53 | $cachedBook = $this->cacheManager->getCache('books')->get($cacheKey); 54 | $supposedCachedBook = $this->bookService->getBookByIsbn($isbn); 55 | 56 | $this->assertSame($cachedBook, $supposedCachedBook); 57 | } 58 | 59 | public function testCacheUpdateCachesTheValue() 60 | { 61 | $book = new Book('foo123'); 62 | $cacheKey = $this->keyGenerator->generateKey($book->isbn); 63 | 64 | $this->bookService->saveBook($book); 65 | $cachedBook = $this->cacheManager->getCache('books')->get($cacheKey); 66 | 67 | $this->assertEquals($book, $cachedBook); 68 | } 69 | 70 | public function testCacheEvictRemoveTheCachedValue() 71 | { 72 | $book = new Book('foobarbaz789'); 73 | $cacheKey = $this->keyGenerator->generateKey($book->isbn); 74 | 75 | $this->bookService->saveBook($book); 76 | $cachedBook = $this->cacheManager->getCache('books')->get($cacheKey); 77 | $this->assertEquals($book, $cachedBook); 78 | 79 | $this->bookService->removeBook($book); 80 | $cachedBook = $this->cacheManager->getCache('books')->get($cacheKey); 81 | $this->assertNull($cachedBook); 82 | } 83 | 84 | /** 85 | * @depends testCacheUpdateCachesTheValue 86 | */ 87 | public function testCacheEvictWithAllEntriesSetsToTrueRemoveAllTheCachedValues() 88 | { 89 | 90 | $bookISBNs = array('book1', 'book2', 'book3'); 91 | $cacheKeys = array(); 92 | foreach ($bookISBNs as $isbn) { 93 | $book = new Book($isbn); 94 | $cacheKeys[] = $this->keyGenerator->generateKey($book->isbn); 95 | $this->bookService->saveBook($book); 96 | } 97 | 98 | $this->bookService->removeAllBooks(); 99 | 100 | foreach ($cacheKeys as $cacheKey) { 101 | $thisVariableShouldBeNull = $this->cacheManager->getCache('books')->get($cacheKey); 102 | $this->assertNull($thisVariableShouldBeNull); 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Aop/Interceptor/CacheInterceptor.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | */ 9 | 10 | namespace Tbbc\CacheBundle\Aop\Interceptor; 11 | 12 | use CG\Proxy\MethodInterceptorInterface; 13 | use CG\Proxy\MethodInvocation; 14 | use Symfony\Component\ExpressionLanguage\ExpressionLanguage; 15 | use Tbbc\CacheBundle\Cache\CacheManagerInterface; 16 | use Tbbc\CacheBundle\Cache\KeyGenerator\KeyGeneratorInterface; 17 | use Tbbc\CacheBundle\Logger\CacheLoggerInterface; 18 | use Tbbc\CacheBundle\Metadata\CacheMethodMetadataInterface; 19 | use Metadata\MetadataFactoryInterface; 20 | use Symfony\Component\EventDispatcher\EventDispatcherInterface; 21 | 22 | /** 23 | * Class CacheInterceptor 24 | * 25 | * @author Benjamin Dulau 26 | * @author Boris Guéry 27 | */ 28 | class CacheInterceptor implements MethodInterceptorInterface 29 | { 30 | const CACHEABLE = 'cacheable'; 31 | const EVICT = 'cache_evict'; 32 | const UPDATE = 'cache_update'; 33 | 34 | private $metadataFactory; 35 | private $cacheManager; 36 | private $keyGenerator; 37 | private $expressionLanguage; 38 | private $dispatcher; 39 | private $logger; 40 | 41 | public function __construct( 42 | MetadataFactoryInterface $metadataFactory, 43 | CacheManagerInterface $cacheManager, 44 | KeyGeneratorInterface $keyGenerator, 45 | ExpressionLanguage $expressionLanguage, 46 | EventDispatcherInterface $dispatcher, 47 | CacheLoggerInterface $cacheLogger = null 48 | ) 49 | { 50 | $this->metadataFactory = $metadataFactory; 51 | $this->cacheManager = $cacheManager; 52 | $this->keyGenerator = $keyGenerator; 53 | $this->expressionLanguage = $expressionLanguage; 54 | $this->dispatcher = $dispatcher; 55 | $this->logger = $cacheLogger; 56 | } 57 | 58 | public function intercept(MethodInvocation $method) 59 | { 60 | $metadata = $this->metadataFactory->getMetadataForClass($method->reflection->class); 61 | 62 | // no cache metadata, proceed 63 | if (empty($metadata) || !isset($metadata->methodMetadata[$method->reflection->name])) { 64 | return $method->proceed(); 65 | } 66 | 67 | $metadata = $metadata->methodMetadata[$method->reflection->name]; 68 | 69 | if (!$metadata instanceof CacheMethodMetadataInterface) { 70 | return $method->proceed(); 71 | } 72 | 73 | if (empty($metadata->caches)) { 74 | 75 | throw new \LogicException('No caches set'); 76 | } 77 | 78 | if (self::CACHEABLE == $metadata->getOperation()) { 79 | 80 | $operation = new CacheableOperation( 81 | $this->cacheManager, 82 | $this->keyGenerator, 83 | $this->expressionLanguage, 84 | $this->dispatcher, 85 | $this->logger 86 | ); 87 | 88 | return $operation->handle($metadata, $method); 89 | } 90 | 91 | if (self::EVICT == $metadata->getOperation()) { 92 | 93 | $operation = new CacheEvictOperation( 94 | $this->cacheManager, 95 | $this->keyGenerator, 96 | $this->expressionLanguage, 97 | $this->dispatcher, 98 | $this->logger 99 | ); 100 | 101 | return $operation->handle($metadata, $method); 102 | } 103 | 104 | if (self::UPDATE == $metadata->getOperation()) { 105 | 106 | $operation = new CacheUpdateOperation( 107 | $this->cacheManager, 108 | $this->keyGenerator, 109 | $this->expressionLanguage, 110 | $this->dispatcher, 111 | $this->logger 112 | ); 113 | 114 | return $operation->handle($metadata, $method); 115 | } 116 | 117 | return $method->proceed(); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /Aop/Interceptor/AbstractCacheOperation.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | */ 8 | 9 | namespace Tbbc\CacheBundle\Aop\Interceptor; 10 | 11 | use CG\Proxy\MethodInvocation; 12 | use Symfony\Component\ExpressionLanguage\ExpressionLanguage; 13 | use Tbbc\CacheBundle\Cache\CacheManagerInterface; 14 | use Tbbc\CacheBundle\Cache\KeyGenerator\KeyGeneratorInterface; 15 | use Tbbc\CacheBundle\Logger\CacheLoggerInterface; 16 | use Tbbc\CacheBundle\Metadata\CacheMethodMetadataInterface; 17 | use Symfony\Component\ExpressionLanguage\Expression; 18 | use Symfony\Component\EventDispatcher\EventDispatcherInterface; 19 | 20 | /** 21 | * @author Boris Guéry 22 | * @author Benjamin Dulau 23 | */ 24 | abstract class AbstractCacheOperation implements CacheOperationInterface 25 | { 26 | protected $cacheOperationContext; 27 | protected $dispatcher; 28 | 29 | private $expressionLanguage; 30 | private $cacheManager; 31 | private $keyGenerator; 32 | private $cacheLogger; 33 | 34 | public function __construct( 35 | CacheManagerInterface $cacheManager, 36 | KeyGeneratorInterface $keyGenerator, 37 | ExpressionLanguage $expressionLanguage, 38 | EventDispatcherInterface $dispatcher, 39 | CacheLoggerInterface $logger = null 40 | ) 41 | { 42 | $this->cacheManager = $cacheManager; 43 | $this->keyGenerator = $keyGenerator; 44 | $this->expressionLanguage = $expressionLanguage; 45 | $this->dispatcher = $dispatcher; 46 | $this->cacheLogger = $logger; 47 | 48 | $this->cacheOperationContext = new CacheOperationContext($this->getOperationName()); 49 | } 50 | 51 | protected function getCacheManager() 52 | { 53 | return $this->cacheManager; 54 | } 55 | 56 | protected function getKeyGenerator() 57 | { 58 | return $this->keyGenerator; 59 | } 60 | 61 | protected function getCacheLogger() 62 | { 63 | return $this->cacheLogger; 64 | } 65 | 66 | protected function generateCacheKey(CacheMethodMetadataInterface $metadata, MethodInvocation $method) 67 | { 68 | $keyGeneratorArguments = array(); 69 | 70 | if (!empty($metadata->key)) { 71 | if ($metadata->key instanceof Expression) { 72 | // TODO Add some cache here! 73 | $values = array(); 74 | foreach ($method->reflection->getParameters() as $param) { 75 | /** @see https://github.com/TheBigBrainsCompany/TbbcCacheBundle/issues/9 */ 76 | // the jms/cg library has not been tagged for a while so instead of 77 | // adding a hard dependency on a specific version we do a runtime check. 78 | if (method_exists($method, 'getNamedArgument')) { 79 | $values[$param->name] = $method->getNamedArgument($param->name); 80 | } else { 81 | foreach ($method->reflection->getParameters() as $i => $reflectionParam) { 82 | if ($reflectionParam->name !== $param->name) { 83 | continue; 84 | } 85 | 86 | if (!array_key_exists($i, $method->arguments)) { 87 | if ($reflectionParam->isDefaultValueAvailable()) { 88 | return $reflectionParam->getDefaultValue(); 89 | } 90 | 91 | throw new \RuntimeException(sprintf('There was no value given for parameter "%s".', $reflectionParam->name)); 92 | } 93 | 94 | $values[$param->name] = $method->arguments[$i]; 95 | } 96 | } 97 | } 98 | 99 | $key = $this->expressionLanguage->evaluate($metadata->key, $values); 100 | 101 | $keyGeneratorArguments[] = $key; 102 | } 103 | } 104 | 105 | if (empty($keyGeneratorArguments)) { 106 | $keyGeneratorArguments = $method->arguments; 107 | } 108 | 109 | $key = $this->keyGenerator->generateKey($keyGeneratorArguments); 110 | 111 | $this->cacheOperationContext->setKey($key); 112 | 113 | return $key; 114 | } 115 | 116 | abstract public function getOperationName(); 117 | 118 | public function __destruct() 119 | { 120 | if (null !== $this->cacheLogger) { 121 | $this->cacheLogger->log($this->cacheOperationContext); 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | */ 8 | 9 | namespace Tbbc\CacheBundle\DependencyInjection; 10 | 11 | use Tbbc\CacheBundle\DependencyInjection\CacheFactory\CacheFactoryInterface; 12 | use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; 13 | use Symfony\Component\Config\Definition\Builder\TreeBuilder; 14 | use Symfony\Component\Config\Definition\ConfigurationInterface; 15 | 16 | /** 17 | * This is the class that validates and merges configuration from your app/config files 18 | * 19 | * To learn more see {@link http://symfony.com/doc/current/cookbook/bundles/extension.html#cookbook-bundles-extension-config-class} 20 | */ 21 | class Configuration implements ConfigurationInterface 22 | { 23 | private $cacheFactories; 24 | 25 | /** 26 | * Constructor 27 | * 28 | * @param array|CacheFactoryInterface[] $cacheFactories 29 | */ 30 | public function __construct(array $cacheFactories) 31 | { 32 | $this->cacheFactories = $cacheFactories; 33 | } 34 | 35 | /** 36 | * {@inheritDoc} 37 | */ 38 | public function getConfigTreeBuilder() 39 | { 40 | $treeBuilder = new TreeBuilder(); 41 | $rootNode = $treeBuilder->root('tbbc_cache'); 42 | 43 | $rootNode 44 | ->children() 45 | ->scalarNode('key_generator') 46 | ->end() 47 | ; 48 | 49 | $this->addAnnotationsSection($rootNode); 50 | $this->addManagerSection($rootNode); 51 | $this->addCacheSection($rootNode, $this->cacheFactories); 52 | $this->addMetadataSection($rootNode); 53 | 54 | return $treeBuilder; 55 | } 56 | 57 | /** 58 | * Parses the tbbc_cache.manager config section 59 | * Example for yaml driver: 60 | * tbbc_cache: 61 | * annotations: 62 | * enabled: true 63 | * 64 | * @param ArrayNodeDefinition $node 65 | * @return void 66 | */ 67 | private function addAnnotationsSection(ArrayNodeDefinition $node) 68 | { 69 | $node 70 | ->children() 71 | ->arrayNode('annotations') 72 | ->addDefaultsIfNotSet() 73 | ->children() 74 | ->scalarNode('enabled')->defaultFalse()->cannotBeEmpty()->end() 75 | ->end() 76 | ->end() 77 | ->end() 78 | ; 79 | } 80 | 81 | /** 82 | * Parses the tbbc_cache.manager config section 83 | * Example for yaml driver: 84 | * tbbc_cache: 85 | * manager: 86 | * 87 | * @param ArrayNodeDefinition $node 88 | * @return void 89 | */ 90 | private function addManagerSection(ArrayNodeDefinition $node) 91 | { 92 | $supportedManagers = array('simple_cache'); 93 | 94 | $node 95 | ->children() 96 | ->scalarNode('manager') 97 | ->defaultValue('simple_cache') 98 | ->validate() 99 | ->ifNotInArray($supportedManagers) 100 | ->thenInvalid('The cache manager "%s" is not supported. Please choose one of '.json_encode($supportedManagers)) 101 | ->end() 102 | ->cannotBeEmpty() 103 | ->end() 104 | ->end() 105 | ; 106 | } 107 | 108 | /** 109 | * Parses the tbbc_cache.manager config section 110 | * Example for yaml driver: 111 | * tbbc_cache: 112 | * cache: 113 | * type: memcached 114 | * servers: .... 115 | * 116 | * @param ArrayNodeDefinition $node 117 | * @param array|CacheFactoryInterface[] $cacheFactories 118 | */ 119 | private function addCacheSection(ArrayNodeDefinition $node, array $cacheFactories) 120 | { 121 | $cacheNodeBuilder = $node 122 | ->children() 123 | ->arrayNode('cache') 124 | ->useAttributeAsKey('name') 125 | ->prototype('array') 126 | ->children() 127 | ->scalarNode('type')->isRequired()->cannotBeEmpty()->end() 128 | ; 129 | 130 | foreach ($cacheFactories as $factory) { 131 | $factory->addConfiguration($cacheNodeBuilder); 132 | } 133 | } 134 | 135 | /** 136 | * Parses the tbbc_cache.metadata config section 137 | * Example for yaml driver: 138 | * tbbc_cache: 139 | * metadata: 140 | * use_cache: true 141 | * cache_dir: %kernel.cache_dir%/tbbc_cache 142 | * 143 | * @param ArrayNodeDefinition $node 144 | */ 145 | private function addMetadataSection(ArrayNodeDefinition $node) 146 | { 147 | $node 148 | ->children() 149 | ->arrayNode('metadata') 150 | ->addDefaultsIfNotSet() 151 | ->children() 152 | ->booleanNode('use_cache')->defaultTrue()->end() 153 | ->scalarNode('cache_dir')->cannotBeEmpty()->defaultValue('%kernel.cache_dir%/tbbc_cache')->end() 154 | ->end() 155 | ->end() 156 | ->end() 157 | ; 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /Tests/Aop/Interceptor/CacheableOperationTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | */ 8 | 9 | namespace Tbbc\CacheBundle\Tests\Aop\Interceptor; 10 | 11 | use Tbbc\CacheBundle\Aop\Interceptor\CacheableOperation; 12 | 13 | /** 14 | * @author Boris Guéry 15 | */ 16 | class CacheableOperationTest extends AbstractCacheOperationTest 17 | { 18 | /** 19 | * @expectedException \Tbbc\CacheBundle\Exception\InvalidArgumentException 20 | */ 21 | public function testHandleWithWrongMethodMetadataThrowsAnException() 22 | { 23 | $methodInvocation = $this->getMethodInvocation(); 24 | $methodInvocation 25 | ->expects($this->never()) 26 | ->method('proceed') 27 | ; 28 | 29 | $incorrectMethodMetadata = $this->getMockBuilder('Tbbc\CacheBundle\Metadata\CacheMethodMetadataInterface') 30 | ->disableOriginalConstructor() 31 | ->getMock() 32 | ; 33 | 34 | $operation = new CacheableOperation( 35 | $this->getCacheManager(), 36 | $this->getKeyGenerator(), 37 | $this->getExpressionLanguage(), 38 | $this->getEventDispatcher() 39 | ); 40 | 41 | $operation->handle($incorrectMethodMetadata, $this->getMethodInvocation()); 42 | } 43 | 44 | public function testCacheableOperationProperlyGetValueFromCacheIfItExists() 45 | { 46 | $methodInvocation = $this->getMethodInvocation(); 47 | $methodInvocation 48 | ->expects($this->never()) 49 | ->method('proceed') 50 | ; 51 | 52 | $keyGenerator = $this->getKeyGenerator(); 53 | $keyGenerator 54 | ->expects($this->once()) 55 | ->method('generateKey') 56 | ->withAnyParameters() 57 | ->will($this->returnValue('cachedValue')) 58 | ; 59 | 60 | $cache = $this->getCache(); 61 | $cache 62 | ->expects($this->once()) 63 | ->method('get') 64 | ->with($this->equalTo('cachedValue')) 65 | ->will($this->returnValue('cachedValue')) 66 | ; 67 | 68 | $cache 69 | ->expects($this->never()) 70 | ->method('set') 71 | ; 72 | 73 | $cacheManager = $this->getCacheManager(); 74 | $cacheManager 75 | ->expects($this->once()) 76 | ->method('getCache') 77 | ->withAnyParameters('cache_name') // @see MethodMetadata mock 78 | ->will($this->returnValue($cache)) 79 | ; 80 | 81 | $operation = new CacheableOperation( 82 | $cacheManager, 83 | $keyGenerator, 84 | $this->getExpressionLanguage(), 85 | $this->getEventDispatcher() 86 | ); 87 | 88 | $actualResult = $operation->handle($this->getMethodMetadata(), $methodInvocation); 89 | 90 | $this->assertSame('cachedValue', $actualResult); 91 | } 92 | 93 | public function testCacheableOperationProperlySetValueToCacheIfItIsMissing() 94 | { 95 | $methodInvocation = $this->getMethodInvocation(); 96 | $methodInvocation 97 | ->expects($this->once()) 98 | ->method('proceed') 99 | ->will($this->returnValue('notCachedValue')) 100 | ; 101 | 102 | $keyGenerator = $this->getKeyGenerator(); 103 | $keyGenerator 104 | ->expects($this->once()) 105 | ->method('generateKey') 106 | ->withAnyParameters() 107 | ->will($this->returnValue('notCachedValue')) 108 | ; 109 | 110 | $cache = $this->getCache(); 111 | $cache 112 | ->expects($this->once()) 113 | ->method('get') 114 | ->with($this->equalTo('notCachedValue')) 115 | ->will($this->returnValue(null)) 116 | ; 117 | 118 | $cache 119 | ->expects($this->once()) 120 | ->method('set') 121 | ->with($this->equalTo('notCachedValue'), $this->equalTo('notCachedValue')) 122 | ->will($this->returnValue(true)) 123 | ; 124 | 125 | $cacheManager = $this->getCacheManager(); 126 | $cacheManager 127 | ->expects($this->exactly(2)) 128 | ->method('getCache') 129 | ->with($this->equalTo('cache_name')) // @see MethodMetadata mock 130 | ->will($this->returnValue($cache)) 131 | ; 132 | 133 | $operation = new CacheableOperation( 134 | $cacheManager, 135 | $keyGenerator, 136 | $this->getExpressionLanguage(), 137 | $this->getEventDispatcher() 138 | ); 139 | 140 | $actualResult = $operation->handle($this->getMethodMetadata(), $methodInvocation); 141 | 142 | $this->assertSame('notCachedValue', $actualResult); 143 | } 144 | 145 | protected function getMethodMetadata() 146 | { 147 | $metadata = $this->getMockBuilder('Tbbc\CacheBundle\Metadata\CacheableMethodMetadata') 148 | ->disableOriginalConstructor() 149 | ->getMock() 150 | ; 151 | 152 | $metadata->caches = array('cache_name'); 153 | $metadata->key = null; // @see KeyGenerator Mock, always returns foo 154 | 155 | return $metadata; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /Tests/Aop/Interceptor/CacheEvictOperationTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | */ 8 | 9 | namespace Tbbc\CacheBundle\Tests\Aop\Interceptor; 10 | 11 | use Tbbc\CacheBundle\Aop\Interceptor\CacheEvictOperation; 12 | 13 | /** 14 | * @author Boris Guéry 15 | */ 16 | class CacheEvictOperationTest extends AbstractCacheOperationTest 17 | { 18 | /** 19 | * @expectedException \Tbbc\CacheBundle\Exception\InvalidArgumentException 20 | */ 21 | public function testHandleWithWrongMethodMetadataThrowsAnException() 22 | { 23 | $methodInvocation = $this->getMethodInvocation(); 24 | $methodInvocation 25 | ->expects($this->never()) 26 | ->method('proceed') 27 | ; 28 | 29 | $incorrectMethodMetadata = $this->getMockBuilder('Tbbc\CacheBundle\Metadata\CacheMethodMetadataInterface') 30 | ->disableOriginalConstructor() 31 | ->getMock() 32 | ; 33 | 34 | $operation = new CacheEvictOperation( 35 | $this->getCacheManager(), 36 | $this->getKeyGenerator(), 37 | $this->getExpressionLanguage(), 38 | $this->getEventDispatcher() 39 | ); 40 | 41 | $operation->handle($incorrectMethodMetadata, $this->getMethodInvocation()); 42 | } 43 | 44 | public function testCacheEvictOperationProperlyFlushAllEntriesIfDefined() 45 | { 46 | $metadata = $this->getMethodMetadata(); 47 | $metadata->allEntries = true; 48 | 49 | $methodInvocation = $this->getMethodInvocation(); 50 | $methodInvocation 51 | ->expects($this->once()) 52 | ->method('proceed') 53 | ->will($this->returnValue('evictedValue')) 54 | ; 55 | 56 | $keyGenerator = $this->getKeyGenerator(); 57 | $keyGenerator 58 | ->expects($this->never()) 59 | ->method('generateKey') 60 | ; 61 | 62 | $cache = $this->getCache(); 63 | $cache 64 | ->expects($this->never()) 65 | ->method('get') 66 | ; 67 | 68 | $cache 69 | ->expects($this->never()) 70 | ->method('set') 71 | ; 72 | 73 | $cache 74 | ->expects($this->once()) 75 | ->method('flush') 76 | ; 77 | 78 | $cacheManager = $this->getCacheManager(); 79 | $cacheManager 80 | ->expects($this->once()) 81 | ->method('getCache') 82 | ->withAnyParameters() 83 | ->will($this->returnValue($cache)) 84 | ; 85 | 86 | $operation = new CacheEvictOperation( 87 | $cacheManager, 88 | $keyGenerator, 89 | $this->getExpressionLanguage(), 90 | $this->getEventDispatcher() 91 | ); 92 | 93 | $actualResult = $operation->handle($metadata, $methodInvocation); 94 | $this->assertEquals('evictedValue', $actualResult); 95 | } 96 | 97 | public function testCacheEvictOperationDoesNotFlushAllEntriesIfNotDefined() 98 | { 99 | $metadata = $this->getMethodMetadata(); 100 | $metadata->allEntries = false; 101 | 102 | $methodInvocation = $this->getMethodInvocation(); 103 | $methodInvocation 104 | ->expects($this->once()) 105 | ->method('proceed') 106 | ->will($this->returnValue('evictedValue')) 107 | ; 108 | 109 | $keyGenerator = $this->getKeyGenerator(); 110 | $keyGenerator 111 | ->expects($this->once()) 112 | ->method('generateKey') 113 | ->withAnyParameters() 114 | ->will($this->returnValue('evictedValueKey')) 115 | ; 116 | 117 | $cache = $this->getCache(); 118 | $cache 119 | ->expects($this->never()) 120 | ->method('get') 121 | ; 122 | 123 | $cache 124 | ->expects($this->never()) 125 | ->method('set') 126 | ; 127 | 128 | $cache 129 | ->expects($this->never()) 130 | ->method('flush') 131 | ; 132 | 133 | $cache 134 | ->expects($this->once()) 135 | ->method('delete') 136 | ->with($this->equalTo('evictedValueKey')) 137 | ->will($this->returnValue(true)) 138 | ; 139 | 140 | $cacheManager = $this->getCacheManager(); 141 | $cacheManager 142 | ->expects($this->once()) 143 | ->method('getCache') 144 | ->withAnyParameters() 145 | ->will($this->returnValue($cache)) 146 | ; 147 | 148 | $operation = new CacheEvictOperation( 149 | $cacheManager, 150 | $keyGenerator, 151 | $this->getExpressionLanguage(), 152 | $this->getEventDispatcher() 153 | ); 154 | 155 | $actualResult = $operation->handle($metadata, $methodInvocation); 156 | $this->assertEquals('evictedValue', $actualResult); 157 | } 158 | 159 | protected function getMethodMetadata() 160 | { 161 | $metadata = $this->getMockBuilder('Tbbc\CacheBundle\Metadata\CacheEvictMethodMetadata') 162 | ->disableOriginalConstructor() 163 | ->getMock() 164 | ; 165 | 166 | $metadata->caches = array('cache_name'); 167 | $metadata->key = null; // @see KeyGenerator Mock, always returns foo 168 | 169 | return $metadata; 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /Aop/Interceptor/CacheOperationContext.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | namespace Tbbc\CacheBundle\Aop\Interceptor; 7 | 8 | final class CacheOperationContext 9 | { 10 | /** 11 | * The operation name/type 12 | * One of 'cacheable', 'cache_evict', or 'cache_update' 13 | * @var string 14 | */ 15 | private $operation; 16 | 17 | /** 18 | * The cache key that was generated for this operation 19 | * @var string 20 | */ 21 | private $key; 22 | 23 | /** 24 | * Hit cache name 25 | * @var string 26 | */ 27 | private $cacheHit; 28 | 29 | /** 30 | * Missed cache list 31 | * @var array 32 | */ 33 | private $cacheMiss = array(); 34 | 35 | /** 36 | * Cache list that were updated 37 | * @var array 38 | */ 39 | private $cacheUpdates = array(); 40 | 41 | /** 42 | * Cache name list target for this operation 43 | * @var array|string[] 44 | */ 45 | private $caches = array(); 46 | 47 | /** 48 | * Class name for which cache operation was executed 49 | * @var string 50 | */ 51 | private $targetClass; 52 | 53 | /** 54 | * Method name for which cache operation was executed 55 | * @var string 56 | */ 57 | private $targetMethod; 58 | 59 | /** 60 | * @var array|string[] 61 | */ 62 | private $messages = array(); 63 | 64 | /** 65 | * Whether or not this was a flush operation 66 | * @var bool 67 | */ 68 | private $flush = false; 69 | 70 | /** 71 | * Whether or not the cache operation ran before method invocation 72 | * @var bool 73 | */ 74 | private $beforeInvocation = false; 75 | 76 | /** 77 | * Constructor 78 | * 79 | * @param string $operation 80 | */ 81 | public function __construct($operation) 82 | { 83 | $this->operation = $operation; 84 | } 85 | 86 | /** 87 | * Adds an info message for this operation 88 | * 89 | * @param string $message 90 | * @return CacheOperationContext 91 | */ 92 | public function addMessage($message) 93 | { 94 | $this->messages[] = $message; 95 | 96 | return $this; 97 | } 98 | 99 | /** 100 | * Returns all the collected messages for this operation 101 | * 102 | * @return array|string[] 103 | */ 104 | public function getMessages() 105 | { 106 | return $this->messages; 107 | } 108 | 109 | /** 110 | * Returns cache name list 111 | * 112 | * @return array|string[] 113 | */ 114 | public function getCaches() 115 | { 116 | return $this->caches; 117 | } 118 | 119 | /** 120 | * Sets cache name list 121 | * 122 | * @param array $caches 123 | * @return CacheOperationContext 124 | */ 125 | public function setCaches(array $caches) 126 | { 127 | $this->caches = $caches; 128 | 129 | return $this; 130 | } 131 | 132 | /** 133 | * Get generated key for this operation 134 | * 135 | * @return string 136 | */ 137 | public function getKey() 138 | { 139 | return $this->key; 140 | } 141 | 142 | /** 143 | * Sets generated key for this operation 144 | * 145 | * @param string $key 146 | * @return CacheOperationContext 147 | */ 148 | public function setKey($key) 149 | { 150 | $this->key = $key; 151 | 152 | return $this; 153 | } 154 | 155 | /** 156 | * Returns operation name 157 | * 158 | * @return string 159 | */ 160 | public function getOperation() 161 | { 162 | return $this->operation; 163 | } 164 | 165 | /** 166 | * @return string 167 | */ 168 | public function getTargetClass() 169 | { 170 | return $this->targetClass; 171 | } 172 | 173 | /** 174 | * @param string $targetClass 175 | * @return CacheOperationContext 176 | */ 177 | public function setTargetClass($targetClass) 178 | { 179 | $this->targetClass = $targetClass; 180 | 181 | return $this; 182 | } 183 | 184 | /** 185 | * @return string 186 | */ 187 | public function getTargetMethod() 188 | { 189 | return $this->targetMethod; 190 | } 191 | 192 | /** 193 | * @param string $targetMethod 194 | * @return CacheOperationContext 195 | */ 196 | public function setTargetMethod($targetMethod) 197 | { 198 | $this->targetMethod = $targetMethod; 199 | 200 | return $this; 201 | } 202 | 203 | /** 204 | * @param string $hit 205 | * @return CacheOperationContext 206 | */ 207 | public function setCacheHit($hit) 208 | { 209 | $this->cacheHit = $hit; 210 | 211 | return $this; 212 | } 213 | 214 | /** 215 | * @return string 216 | */ 217 | public function getCacheHit() 218 | { 219 | return $this->cacheHit; 220 | } 221 | 222 | /** 223 | * @param array $miss 224 | * @return CacheOperationContext 225 | */ 226 | public function setCacheMiss(array $miss) 227 | { 228 | $this->cacheMiss = $miss; 229 | 230 | return $this; 231 | } 232 | 233 | /** 234 | * @return array 235 | */ 236 | public function getCacheMiss() 237 | { 238 | return $this->cacheMiss; 239 | } 240 | 241 | /** 242 | * @param string $name 243 | * @return CacheOperationContext 244 | */ 245 | public function addCacheUpdate($name) 246 | { 247 | $this->cacheUpdates[] = $name; 248 | 249 | return $this; 250 | } 251 | 252 | /** 253 | * @return array 254 | */ 255 | public function getCacheUpdates() 256 | { 257 | return $this->cacheUpdates; 258 | } 259 | 260 | /** 261 | * @param boolean $flush 262 | * @return CacheOperationContext 263 | */ 264 | public function setFlush($flush) 265 | { 266 | $this->flush = (bool) $flush; 267 | 268 | return $this; 269 | } 270 | 271 | /** 272 | * @return boolean 273 | */ 274 | public function isFlush() 275 | { 276 | return $this->flush; 277 | } 278 | 279 | /** 280 | * @param boolean $beforeInvocation 281 | * @return CacheOperationContext 282 | */ 283 | public function setBeforeInvocation($beforeInvocation) 284 | { 285 | $this->beforeInvocation = $beforeInvocation; 286 | 287 | return $this; 288 | } 289 | 290 | /** 291 | * @return boolean 292 | */ 293 | public function isBeforeInvocation() 294 | { 295 | return $this->beforeInvocation; 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /DependencyInjection/TbbcCacheExtension.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | */ 8 | 9 | namespace Tbbc\CacheBundle\DependencyInjection; 10 | 11 | use Tbbc\CacheBundle\DependencyInjection\CacheFactory\CacheFactoryInterface; 12 | use Symfony\Component\Config\Definition\Processor; 13 | use Symfony\Component\DependencyInjection\ContainerBuilder; 14 | use Symfony\Component\Config\FileLocator; 15 | use Symfony\Component\DependencyInjection\Exception\RuntimeException; 16 | use Symfony\Component\DependencyInjection\Reference; 17 | use Symfony\Component\HttpKernel\DependencyInjection\Extension; 18 | use Symfony\Component\DependencyInjection\Loader; 19 | 20 | /** 21 | * Class TbbcCacheExtension 22 | * 23 | * @author Benjamin Dulau 24 | */ 25 | class TbbcCacheExtension extends Extension 26 | { 27 | /** 28 | * @var array|CacheFactoryInterface[] 29 | */ 30 | protected $cacheFactories; 31 | 32 | /** 33 | * {@inheritDoc} 34 | */ 35 | public function load(array $configs, ContainerBuilder $container) 36 | { 37 | $processor = new Processor(); 38 | 39 | $cacheFactories = $this->createCacheFactories(); 40 | 41 | // Main configuration 42 | $configuration = new Configuration($cacheFactories); 43 | $config = $processor->processConfiguration($configuration, $configs); 44 | 45 | $loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); 46 | $loader->load('cache.xml'); 47 | $loader->load('doctrine_caches.xml'); 48 | $loader->load('key_generators.xml'); 49 | $loader->load('logger.xml'); 50 | 51 | if (true === $container->getParameter('kernel.debug')) { 52 | 53 | $loader->load('data_collectors.xml'); 54 | $dataCollector = $container->getDefinition('tbbc_cache.data_collector.cacheable_operation'); 55 | $dataCollector->replaceArgument(0, new Reference('tbbc_cache.logger.cache_logger')); 56 | } 57 | 58 | $managerId = $this->createCacheManager($config, $container); 59 | 60 | $container->setParameter($this->getAlias() . '.annotations.enabled', (bool)$config['annotations']['enabled']); 61 | 62 | if (true === (bool) $config['annotations']['enabled']) { 63 | $bundles = $container->getParameter('kernel.bundles'); 64 | if (!isset($bundles['JMSAopBundle'])) { 65 | throw new RuntimeException('The TbbcCacheBundle requires the JMSAopBundle for using annotations, please make sure to enable it in your AppKernel.'); 66 | } 67 | 68 | $loader->load('aop.xml'); 69 | 70 | if ($config['metadata']['use_cache']) { 71 | $container->getDefinition("tbbc_cache.metadata.metadata_factory") 72 | ->addMethodCall('setCache', array(new Reference('tbbc_cache.metadata.file_cache'))) 73 | ; 74 | } 75 | 76 | $cacheDir = $container->getParameterBag()->resolveValue($config['metadata']['cache_dir']); 77 | if (!is_dir($cacheDir)) { 78 | if (false === @mkdir($cacheDir, 0777, true)) { 79 | throw new RuntimeException(sprintf('Could not create cache directory "%s".', $cacheDir)); 80 | } 81 | } 82 | $container->setParameter('tbbc_cache.metadata.cache_dir', $cacheDir); 83 | 84 | $interceptor = $container->getDefinition('tbbc_cache.aop.interceptor.cache'); 85 | $interceptor->replaceArgument(1, new Reference($managerId)); 86 | if (isset($config['key_generator'])) { 87 | if ($container->has('tbbc_cache.key_generator.' . $config['key_generator'])) { 88 | $interceptor->replaceArgument(2, new Reference('tbbc_cache.key_generator.' . $config['key_generator'])); 89 | } else { 90 | $interceptor->replaceArgument(2, new Reference($config['key_generator'])); 91 | } 92 | } else { 93 | $interceptor->replaceArgument(2, new Reference($container->getParameter('tbbc_cache.key_generator.default'))); 94 | } 95 | // TODO not hardcoded 96 | $interceptor->replaceArgument(5, new Reference('tbbc_cache.logger.cache_logger')); 97 | } 98 | } 99 | 100 | private function createCacheManager(array $config, ContainerBuilder $container) 101 | { 102 | $managerId = sprintf('tbbc_cache.%s_manager', strtolower($config['manager'])); 103 | $managerReference = $container->getDefinition($managerId); 104 | 105 | foreach ($config['cache'] as $name => $cache) { 106 | $cache['name'] = $name; 107 | $cacheId = $this->createCache($cache, $container); 108 | 109 | $managerReference->addMethodCall('addCache', array(new Reference($cacheId))); 110 | } 111 | 112 | return $managerId; 113 | } 114 | 115 | /** 116 | * Creates a single cache service 117 | * 118 | * @param array $config 119 | * @param ContainerBuilder $container 120 | * 121 | * @return string 122 | */ 123 | private function createCache(array $config, ContainerBuilder $container) 124 | { 125 | $type = $config['type']; 126 | if (array_key_exists($type, $this->cacheFactories)) { 127 | $id = sprintf('tbbc_cache.%s_cache', $config['name']); 128 | $this->cacheFactories[$type]->create($container, $id, $config); 129 | 130 | return $id; 131 | } 132 | 133 | // TODO: throw exception ? 134 | } 135 | 136 | /** 137 | * Creates the cache factories 138 | * 139 | * @return array|CacheFactoryInterface[] 140 | */ 141 | private function createCacheFactories() 142 | { 143 | if (null !== $this->cacheFactories) { 144 | return $this->cacheFactories; 145 | } 146 | 147 | // load bundled cache factories 148 | $tempContainer = new ContainerBuilder(); 149 | $loader = new Loader\XmlFileLoader($tempContainer, new FileLocator(__DIR__.'/../Resources/config')); 150 | 151 | $loader->load('cache_factories.xml'); 152 | 153 | $cacheFactories = array(); 154 | foreach ($tempContainer->findTaggedServiceIds('tbbc_cache.cache_factory') as $id => $factories) { 155 | foreach ($factories as $factory) { 156 | if (!isset($factory['cache_type'])) { 157 | throw new \InvalidArgumentException(sprintf( 158 | 'Service "%s" must define a "cache_type" attribute for "tbbc_cache.cache_factory" tag', 159 | $id 160 | )); 161 | } 162 | 163 | $factoryService = $tempContainer->get($id); 164 | $cacheFactories[str_replace('-', '_', strtolower($factory['cache_type']))] = $factoryService; 165 | } 166 | } 167 | 168 | return $this->cacheFactories = $cacheFactories; 169 | } 170 | 171 | /** 172 | * {@inheritDoc} 173 | */ 174 | public function getConfiguration(array $config, ContainerBuilder $container) 175 | { 176 | $cacheFactories = $this->createCacheFactories(); 177 | 178 | return new Configuration($cacheFactories); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /Resources/views/Collector/cache_operations.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'WebProfilerBundle:Profiler:layout.html.twig' %} 2 | 3 | {% block toolbar %} 4 | {% set icon %} 5 | Tbbc Cache 7 | {{ collector.operations | length }} 8 | {% endset %} 9 | 10 | {% set text %} 11 |
12 | Cache operations 13 | 14 | {{ collector.operations | length }} 15 | 16 |
17 |
18 | Cache hits 19 | 20 | {{- collector.hits -}} 21 | 22 |
23 |
24 | Cache miss 25 | 26 | {{- collector.miss -}} 27 | 28 |
29 | {% endset %} 30 | 31 | {% include 'WebProfilerBundle:Profiler:toolbar_item.html.twig' with { 'link': profiler_url } %} 32 | {% endblock %} 33 | 34 | {% block menu %} 35 | {# the menu content #} 36 | 37 | {% spaceless %} 38 | 39 | {# 40 | Icons License 41 | ============= 42 | 43 | Glyphish icons (http://glyphish.com/) created by Joseph Wain 44 | (http://www.penandthink.com/) are shared under a Creative Commons Attribution 45 | license (http://creativecommons.org/licenses/by/3.0/us/). 46 | #} 47 | Tbbc Cache 48 | 49 | {% endspaceless %} 50 | Tbbc Cache 51 | 52 | {{ collector.operations | length }} 53 | 54 | 55 | {% endblock %} 56 | 57 | {% block panel %} 58 | {{ block('overview') }} 59 | {{ block('cacheable') }} 60 | {{ block('cache_evict') }} 61 | {{ block('cache_update') }} 62 | {% endblock %} 63 | 64 | {% block overview %} 65 |

Overview

66 | 67 | {% if collector.operations %} 68 | {% 69 | set data = { 70 | 'Total cache operations': collector.operations|length, 71 | 'Cacheable operations': collector.cacheableOperations|length, 72 | 'CacheEvict operations': collector.cacheEvictOperations|length, 73 | 'CacheUpdate operations': collector.cacheUpdateOperations|length 74 | } 75 | %} 76 | 77 | {% include 'WebProfilerBundle:Profiler:table.html.twig' with {'data': data} only %} 78 | {% else %} 79 |

80 | No cache operations 81 |

82 | {% endif %} 83 | {% endblock overview %} 84 | 85 | {% block cacheable %} 86 |

Cacheable Operations

87 | 88 | {% if collector.cacheableOperations %} 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | {% for operation in collector.cacheableOperations %} 102 | 103 | 104 | 105 | 106 | 107 | 108 | 115 | 116 | {% endfor %} 117 | 118 |
ClassMethodKeyHitMissCaches
{{ operation.targetClass }}{{ operation.targetMethod }}{{ operation.key }}{{ (operation.cacheHit is not empty) ? '1 (' ~ operation.cacheHit ~ ')' : '-' }}{{ operation.cacheMiss|length }} 109 |
    110 | {% for cache in operation.caches %} 111 |
  • {{ cache }}
  • 112 | {% endfor %} 113 |
114 |
119 | {% else %} 120 |

121 | No Cacheable operations 122 |

123 | {% endif %} 124 | {% endblock cacheable %} 125 | 126 | {% block cache_evict %} 127 |

CacheEvict Operations

128 | 129 | {% if collector.cacheEvictOperations %} 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | {% for operation in collector.cacheEvictOperations %} 143 | 144 | 145 | 146 | 147 | 148 | 149 | 156 | 157 | {% endfor %} 158 | 159 |
ClassMethodKeyAll entries ?Before invocationCaches
{{ operation.targetClass }}{{ operation.targetMethod }}{{ (operation.key is not empty) ? operation.key : ' - ' }}{{ operation.flush ? 'Yes' : 'No' }}{{ operation.beforeInvocation ? 'Yes' : 'No' }} 150 |
    151 | {% for cache in operation.caches %} 152 |
  • {{ cache }}
  • 153 | {% endfor %} 154 |
155 |
160 | {% else %} 161 |

162 | No CacheEvict operations 163 |

164 | {% endif %} 165 | {% endblock cache_evict %} 166 | 167 | {% block cache_update %} 168 |

CacheUpdate Operations

169 | 170 | {% if collector.cacheUpdateOperations %} 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | {% for operation in collector.cacheUpdateOperations %} 182 | 183 | 184 | 185 | 186 | 193 | 194 | {% endfor %} 195 | 196 |
ClassMethodKeyCaches
{{ operation.targetClass }}{{ operation.targetMethod }}{{ (operation.key is not empty) ? operation.key : ' - ' }} 187 |
    188 | {% for cache in operation.cacheUpdates %} 189 |
  • {{ cache }}
  • 190 | {% endfor %} 191 |
192 |
197 | {% else %} 198 |

199 | No CacheUpdate operations 200 |

201 | {% endif %} 202 | {% endblock cache_update %} 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CacheBundle 2 | 3 | Add **cache abstraction** and **method annotations** for controlling cache. 4 | The current implementation of the Cache component is a wrapper (proxy) for Doctrine\Common\Cache. 5 | 6 | ## State 7 | 8 | [![Build Status](https://travis-ci.org/TheBigBrainsCompany/TbbcCacheBundle.png?branch=master)](https://travis-ci.org/TheBigBrainsCompany/TbbcCacheBundle) 9 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/TheBigBrainsCompany/TbbcCacheBundle/badges/quality-score.png?s=7f9dd7e70a3dc68f4b9e9823609e71d6e18a1a42)](https://scrutinizer-ci.com/g/TheBigBrainsCompany/TbbcCacheBundle/) 10 | [![Downloads](https://poser.pugx.org/tbbc/cache-bundle/downloads.png)](https://packagist.org/packages/tbbc/cache-bundle) 11 | [![Stable release](https://poser.pugx.org/tbbc/cache-bundle/v/stable.png)](https://packagist.org/packages/tbbc/cache-bundle) 12 | 13 | ## Overview 14 | 15 | The TbbcCacheBundle integrates Symfony with a non-instrusive applicative cache management system. 16 | It gives to the developer annotation driven cache control by using AOP mechanisms and PHP language expressions. 17 | 18 | ```PHP 19 | productRepository->getByType($sku, 'book'); 37 | 38 | return $product; 39 | } 40 | 41 | /** 42 | * @CacheUpdate(caches="products", key="product.getSku()") 43 | */ 44 | public function updateProduct(Product $product) 45 | { 46 | $product = $this->productRepository->save($product); 47 | 48 | return $product; 49 | } 50 | 51 | /** 52 | * @CacheEvict(caches="products", key="product.getSku()") 53 | */ 54 | public function removeProduct(Product $product) 55 | { 56 | $product = $this->productRepository->remove($product); 57 | } 58 | } 59 | ``` 60 | 61 | ## Features 62 | 63 | * `@Cacheable`, `@CacheUpdate`, `@CacheEvict` annotation support 64 | * TTL strategy, allow you to customize cache retention 65 | * Namespaced cache manager 66 | * Multiple cache managers: 67 | * Doctrine/ArrayCache 68 | * Doctrine/ApcCache 69 | * Doctrine/MemcachedCache 70 | * Doctrine/RedisCache 71 | * Symfony Debug Toolbar integration 72 | 73 | 74 | ## Documentation 75 | 76 | * [Installation](#installation) 77 | * [Configuration](#configuration) 78 | * [Usage](#usage) 79 | * [Annotation based caching (recommanded)](#annotation-based-caching-recommanded) 80 | * [@Cacheable annotation](#cacheable-annotation) 81 | * [@CacheEvict annotation](#cacheevict-annotation) 82 | * [@CacheUpdate annotation](#cacheupdate-annotation) 83 | * [Expression Language](#expression-language) 84 | * [Standard cache usage (without annotations)](#standard-cache-usage-without-annotations) 85 | * [Custom Cache Manager](#custom-cache-manager) 86 | * [Key generation](#key-generation) 87 | * [Custom Key generation](#custom-key-generation) 88 | * [TTL Strategy](#ttl-strategy) 89 | * [Symfony debug toolbar integration](#symfony-debug-toolbar-integration) 90 | * [Known limitations](#known-limitations) 91 | * [Testing](#testing) 92 | * [License](#license) 93 | 94 | 95 | ## Installation 96 | 97 | First, install the bundle package with composer: 98 | 99 | ```bash 100 | $ php composer.phar require tbbc/cache-bundle 101 | ``` 102 | 103 | Next, activate the bundle into `app/AppKernel.php`: 104 | 105 | ```PHP 106 | productRepository->getByType($sku, 'book'); 187 | 188 | return $product; 189 | } 190 | } 191 | ``` 192 | 193 | #### @CacheEvict annotation 194 | 195 | @CacheEvict annotation allows methods to trigger cache population or cache eviction. 196 | 197 | When a method is demarcated with @CacheEvict annotation, the bundle will execute the method and then will automatically 198 | try to delete the cache entry with the provided key. 199 | 200 | ```PHP 201 | cacheManager = $cacheManager; 322 | $this->keyGenerator = $keyGenerator; 323 | } 324 | 325 | 326 | public function getProduct($sku, $type = 'book') 327 | { 328 | $cacheKey = $this->keyGenerator->generateKey($sku); 329 | $cache = $this->cacheManager->getCache('products'); 330 | 331 | if ($product = $cache->get($cacheKey)) { 332 | return $product; 333 | } 334 | 335 | $product = $this->productRepository->findProductBySkuAndType($sku, $type); 336 | 337 | $cache->set($cacheKey, $product); 338 | 339 | return $product; 340 | } 341 | 342 | public function saveProduct(Product $product) 343 | { 344 | $this->productRepository->save($product); 345 | 346 | $cacheKey = $this->keyGenerator->generateKey($product->getSku()); 347 | $cache = $this->cacheManager->getCache('products'); 348 | 349 | $cache->delete($cacheKey); 350 | } 351 | } 352 | ``` 353 | 354 | ### Custom Cache Manager 355 | 356 | Out of the box, the bundle provides a 357 | [SimpleCacheManager](https://github.com/TheBigBrainsCompany/TbbcCacheBundle/tree/master/Cache/SimpleCacheManager.php), but 358 | custom cache managers can be used instead of the default one and must implement the 359 | [CacheManagerInterface](https://github.com/TheBigBrainsCompany/TbbcCacheBundle/tree/master/Cache/CacheManagerInterface.php). 360 | 361 | 362 | ### Key generation 363 | 364 | Key generation is up to the developer, but for convenience, the bundle comes with some key generation logic. 365 | 366 | **Note**: When using [Annotation based caching](#annotation-usage), usage of Key generators is mandatory. 367 | 368 | Out of the box, the bundle provides a 369 | [SimpleHashKeyGenerator](https://github.com/TheBigBrainsCompany/TbbcCacheBundle/tree/master/Cache/KeyGenerator/SimpleHashKeyGenerator.php) 370 | which basically adds each param encoded using md5 algorithm, and returned a md5 hash of the result. 371 | 372 | For testing purpose you may also use 373 | [LiteralKeyGenerator](https://github.com/TheBigBrainsCompany/TbbcCacheBundle/tree/master/Cache/KeyGenerator/LiteralKeyGenerator.php) 374 | which build a slug-like key. 375 | 376 | **Note**: Both generators does **not** support non-scalar keys such as objects. 377 | 378 | You can override the Key Generator by setting the `key_generator` key in your `config.yml` 379 | 380 | Allowed values are: `simple_hash`, `literal` or the id of the service of your custom Key generator 381 | 382 | ### Custom Key generation 383 | 384 | Custom key generators can be used instead of the default one and must implement the 385 | [KeyGeneratorInterface](https://github.com/TheBigBrainsCompany/TbbcCacheBundle/tree/master/Cache/KeyGenerator/KeyGeneratorInterface.php). 386 | 387 | 388 | ### TTL Strategy 389 | 390 | Since this bundle provides a cache abstraction and not all cache providers support or handle TTL the same way, 391 | TTL strategy must be defined in each cache configuration options (when option is supported). 392 | 393 | Example: 394 | ```YAML 395 | tbbc_cache: 396 | annotations: { enabled: true } 397 | manager: simple_cache 398 | cache: 399 | products: 400 | type: memcached 401 | ttl: 86400 # 1 day 402 | servers: 403 | memcached-01: { host: localhost, port: 11211 } 404 | user_feeds: 405 | type: memcached 406 | ttl: 0 # infinite (same as omitting the option) 407 | followers_list: 408 | type: apc 409 | ttl: 1296000 # 15 days 410 | activity_counters: 411 | type: redis 412 | ttl: 3600 # 1 hour 413 | server: 414 | host: 127.0.0.1 415 | port: 6379 416 | ``` 417 | 418 | ## Symfony debug toolbar integration 419 | 420 | Debugging cache operations is often a pain is the ass. 421 | In order to facilitate this work, the bundle adds some useful live information directly in the Symfony Debug Toolbar. 422 | 423 | Here are some screenshots about the kind of information it will show: 424 | 425 | ![](https://raw.githubusercontent.com/TheBigBrainsCompany/TbbcCacheBundle/master/Resources/doc/debug-bar-01.png "") 426 | 427 | ![](https://raw.githubusercontent.com/TheBigBrainsCompany/TbbcCacheBundle/master/Resources/doc/debug-bar-03.png "") 428 | 429 | ![](https://raw.githubusercontent.com/TheBigBrainsCompany/TbbcCacheBundle/master/Resources/doc/debug-bar-02.png "") 430 | 431 | 432 | ## Known limitations 433 | 434 | ### CacheEvict operation with "allEntries" option 435 | 436 | Due to the way cache is managed in Doctrine and especially the way it is handled by the different cache systems, usage 437 | of the "CacheEvict" operation with the "allEntries" option can lead to undesired behaviour. 438 | 439 | **Be warned** that if you use different cache namespaces but within the same cache instance (like a single memcached 440 | server for instance), the "allEntries" option will flush **all** cache entries in **all** namespaces. 441 | Meaning it will flush the **entire** instance cache. 442 | 443 | ### Doctrine entities caching 444 | 445 | Automatic caching of doctrine entities is not supported at this time. 446 | If you need to cache entities, you have to implement your own logic. 447 | 448 | One way to do it would be to override annotations and metadatas for adding a serialization "type" option, 449 | and then hook in cache events to manually manage serialization/de-serialization operations: 450 | 451 | ```php 452 | serializer = $serializer; 471 | $this->cacheManager = $cacheManager; 472 | } 473 | 474 | public function onAfterCacheHit(CacheHitEvent $event) 475 | { 476 | $value = $event->getValue(); 477 | if ($value instanceof SerializedCacheValue) { 478 | $value = $this->serializer->deserialize($value->data, $value->type, 'json'); 479 | $event->setValue($value); 480 | } 481 | } 482 | 483 | public function onAfterCacheUpdate(CacheUpdateEvent $event) 484 | { 485 | $cache = $event->getCache(); 486 | $key = $event->getKey(); 487 | $metadata = $event->getMetadata(); 488 | 489 | if (null !== $metadata->type) { 490 | $serializedValue = $this->serializer->serialize($event->getValue(), 'json'); 491 | $value = new SerializedCacheValue($metadata->type, $serializedValue); 492 | $this->cacheManager->getCache($cache)->set($key, $value); 493 | } 494 | } 495 | } 496 | ``` 497 | 498 | ## Testing 499 | 500 | Install development dependencies 501 | 502 | ```bash 503 | $ composer install --dev 504 | ``` 505 | 506 | Run the test suite 507 | 508 | ```bash 509 | $ vendor/bin/phpunit 510 | ``` 511 | 512 | ## License 513 | 514 | This bundle is under the MIT license. See the complete license in the bundle: 515 | 516 | Resources/meta/LICENSE 517 | 518 | [![The Big Brains Company - Logo](http://tbbc-valid.thebigbrainscompany.com/assets/images/logo-tbbc.png)](http://thebigbrainscompany.com) 519 | 520 | --------------------------------------------------------------------------------