diff --git a/composer.json b/composer.json index 216b9c0..d2abeee 100644 --- a/composer.json +++ b/composer.json @@ -33,7 +33,8 @@ "phpunit/phpunit": "^5.1 || ^4.0", "symfony/symfony": "^2.7 || ^3.0", "cache/psr-6-doctrine-bridge": "^2.0", - "cache/array-adapter": "^0.4" + "cache/array-adapter": "^0.4", + "matthiasnoback/symfony-dependency-injection-test": "^1.0" }, "suggest": { "cache/adapter-bundle": "To register PSR-6 compliant cache implementations as services.", diff --git a/src/Cache/LoggingCachePool.php b/src/Cache/LoggingCachePool.php deleted file mode 100644 index 2c00b35..0000000 --- a/src/Cache/LoggingCachePool.php +++ /dev/null @@ -1,101 +0,0 @@ -, Tobias Nyholm - * - * This source file is subject to the MIT license that is bundled - * with this source code in the file LICENSE. - */ - -namespace Cache\CacheBundle\Cache; - -use Psr\Log\LoggerInterface; - -/** - * Logg all calls to the cache. - * - * @author Tobias Nyholm - */ -class LoggingCachePool extends RecordingCachePool -{ - /** - * @type LoggerInterface - */ - private $logger; - - /** - * @type string - */ - private $name; - - /** - * @type string - */ - private $level = 'info'; - - /** - * @param LoggerInterface $logger - * - * @return LoggingCachePool - */ - public function setLogger($logger) - { - $this->logger = $logger; - - return $this; - } - - /** - * @param string $name - * - * @return LoggingCachePool - */ - public function setName($name) - { - $this->name = $name; - - return $this; - } - - /** - * @param string $level - * - * @return LoggingCachePool - */ - public function setLevel($level) - { - $this->level = $level; - - return $this; - } - - /** - * @param $call - */ - protected function addCall($call) - { - $data = [ - 'name' => $this->name, - 'method' => $call->name, - 'arguments' => json_encode($call->arguments), - 'hit' => isset($call->isHit) ? $call->isHit ? 'True' : 'False' : 'Invalid', - 'time' => round($call->time * 1000, 2), - 'result' => $call->result, - ]; - - $this->logger->log( - $this->level, - sprintf('[Cache] Provider: %s. Method: %s(%s). Hit: %s. Time: %sms. Result: %s', - $data['name'], - $data['method'], - $data['arguments'], - $data['hit'], - $data['time'], - $data['result'] - ), - $data - ); - } -} diff --git a/src/Cache/RecordingCachePool.php b/src/Cache/Recording/CachePool.php similarity index 70% rename from src/Cache/RecordingCachePool.php rename to src/Cache/Recording/CachePool.php index e0875f1..a4d0d6c 100644 --- a/src/Cache/RecordingCachePool.php +++ b/src/Cache/Recording/CachePool.php @@ -9,17 +9,20 @@ * with this source code in the file LICENSE. */ -namespace Cache\CacheBundle\Cache; +namespace Cache\CacheBundle\Cache\Recording; use Cache\Taggable\TaggablePoolInterface; -use Cache\Taggable\TaggablePSR6PoolAdapter; use Psr\Cache\CacheItemInterface; use Psr\Cache\CacheItemPoolInterface; +use Psr\Log\LoggerInterface; /** + * A pool that logs and collects all your cache calls. + * * @author Aaron Scherer + * @author Tobias Nyholm */ -class RecordingCachePool implements CacheItemPoolInterface, TaggablePoolInterface +class CachePool implements CacheItemPoolInterface, TaggablePoolInterface { /** * @type array @@ -31,6 +34,21 @@ class RecordingCachePool implements CacheItemPoolInterface, TaggablePoolInterfac */ private $cachePool; + /** + * @type LoggerInterface + */ + private $logger; + + /** + * @type string + */ + private $name; + + /** + * @type string + */ + private $level = 'info'; + /** * LoggingCachePool constructor. * @@ -38,7 +56,7 @@ class RecordingCachePool implements CacheItemPoolInterface, TaggablePoolInterfac */ public function __construct(CacheItemPoolInterface $cachePool) { - $this->cachePool = TaggablePSR6PoolAdapter::makeTaggable($cachePool); + $this->cachePool = $cachePool; } /** @@ -49,6 +67,8 @@ public function __construct(CacheItemPoolInterface $cachePool) protected function addCall($call) { $this->calls[] = $call; + + $this->writeLog($call); } /** @@ -57,7 +77,7 @@ protected function addCall($call) * * @return object */ - private function timeCall($name, array $arguments = []) + protected function timeCall($name, array $arguments = []) { $start = microtime(true); $result = call_user_func_array([$this->cachePool, $name], $arguments); @@ -198,4 +218,61 @@ private function getValueRepresentation($value) return $rep; } + + protected function writeLog($call) + { + if (!$this->logger) { + return; + } + + $data = [ + 'name' => $this->name, + 'method' => $call->name, + 'arguments' => json_encode($call->arguments), + 'hit' => isset($call->isHit) ? $call->isHit ? 'True' : 'False' : 'Invalid', + 'time' => round($call->time * 1000, 2), + 'result' => $call->result, + ]; + + $this->logger->log( + $this->level, + sprintf('[Cache] Provider: %s. Method: %s(%s). Hit: %s. Time: %sms. Result: %s', + $data['name'], + $data['method'], + $data['arguments'], + $data['hit'], + $data['time'], + $data['result'] + ), + $data + ); + } + + /** + * @param LoggerInterface $logger + */ + public function setLogger(LoggerInterface $logger = null) + { + $this->logger = $logger; + } + + /** + * @param string $name + */ + public function setName($name) + { + $this->name = $name; + + return $this; + } + + /** + * @param string $level + */ + public function setLevel($level) + { + $this->level = $level; + + return $this; + } } diff --git a/src/Cache/Recording/Factory.php b/src/Cache/Recording/Factory.php new file mode 100644 index 0000000..c09ec4f --- /dev/null +++ b/src/Cache/Recording/Factory.php @@ -0,0 +1,72 @@ +, Tobias Nyholm + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Cache\CacheBundle\Cache\Recording; + +use Cache\Hierarchy\HierarchicalPoolInterface; +use Cache\Taggable\TaggablePoolInterface; +use Psr\Cache\CacheItemPoolInterface; +use Psr\Log\LoggerInterface; + +/** + * Create a recording CachePool. + * + * @author Tobias Nyholm + */ +class Factory +{ + /** + * @type int|string + */ + private $level; + + /** + * @type LoggerInterface + */ + private $logger; + + /** + * @param LoggerInterface $logger + * @param string|int $level + */ + public function __construct(LoggerInterface $logger = null, $level = null) + { + $this->level = $level; + $this->logger = $logger; + } + + /** + * Decorate a CachePool with a recorder. Make sure we use a recorder that implements the same functionality + * as the underling pool. + * + * @param CacheItemPoolInterface $pool + * + * @return CachePool|HierarchyAndTaggablePool|HierarchyPool|TaggablePool + */ + public function create($name, CacheItemPoolInterface $pool) + { + if ($pool instanceof TaggablePoolInterface && $pool instanceof HierarchicalPoolInterface) { + $recorder = new HierarchyAndTaggablePool($pool); + } elseif ($pool instanceof TaggablePoolInterface) { + $recorder = new TaggablePool($pool); + } elseif ($pool instanceof HierarchicalPoolInterface) { + $recorder = new HierarchyPool($pool); + } else { + $recorder = new CachePool($pool); + } + + $recorder->setName($name); + $recorder->setLevel($this->level); + $recorder->setLogger($this->logger); + + return $recorder; + } +} diff --git a/src/Cache/Recording/HierarchyAndTaggablePool.php b/src/Cache/Recording/HierarchyAndTaggablePool.php new file mode 100644 index 0000000..3cf65d9 --- /dev/null +++ b/src/Cache/Recording/HierarchyAndTaggablePool.php @@ -0,0 +1,21 @@ +, Tobias Nyholm + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Cache\CacheBundle\Cache\Recording; + +use Cache\Hierarchy\HierarchicalPoolInterface; + +/** + * @author Tobias Nyholm + */ +class HierarchyAndTaggablePool extends TaggablePool implements HierarchicalPoolInterface +{ +} diff --git a/src/Cache/Recording/HierarchyPool.php b/src/Cache/Recording/HierarchyPool.php new file mode 100644 index 0000000..82ca7b8 --- /dev/null +++ b/src/Cache/Recording/HierarchyPool.php @@ -0,0 +1,21 @@ +, Tobias Nyholm + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Cache\CacheBundle\Cache\Recording; + +use Cache\Hierarchy\HierarchicalPoolInterface; + +/** + * @author Tobias Nyholm + */ +class HierarchyPool extends CachePool implements HierarchicalPoolInterface +{ +} diff --git a/src/Cache/Recording/TaggablePool.php b/src/Cache/Recording/TaggablePool.php new file mode 100644 index 0000000..b0f2a10 --- /dev/null +++ b/src/Cache/Recording/TaggablePool.php @@ -0,0 +1,28 @@ +, Tobias Nyholm + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Cache\CacheBundle\Cache\Recording; + +use Cache\Taggable\TaggablePoolInterface; + +/** + * @author Tobias Nyholm + */ +class TaggablePool extends CachePool implements TaggablePoolInterface +{ + public function clearTags(array $tags) + { + $call = $this->timeCall(__FUNCTION__, [$tags]); + $this->addCall($call); + + return $call->result; + } +} diff --git a/src/CacheBundle.php b/src/CacheBundle.php index 0a3f30b..5780815 100644 --- a/src/CacheBundle.php +++ b/src/CacheBundle.php @@ -16,8 +16,6 @@ use Symfony\Component\HttpKernel\Bundle\Bundle; /** - * Class AequasiCacheBundle. - * * @author Aaron Scherer */ class CacheBundle extends Bundle @@ -30,7 +28,6 @@ public function build(ContainerBuilder $container) parent::build($container); $container->addCompilerPass(new Compiler\CacheTaggingPass()); - $container->addCompilerPass(new Compiler\LoggingCompilerPass()); $container->addCompilerPass(new Compiler\SessionSupportCompilerPass()); $container->addCompilerPass(new Compiler\DoctrineCompilerPass()); diff --git a/src/DataCollector/CacheDataCollector.php b/src/DataCollector/CacheDataCollector.php index 04e5984..282421b 100644 --- a/src/DataCollector/CacheDataCollector.php +++ b/src/DataCollector/CacheDataCollector.php @@ -11,7 +11,7 @@ namespace Cache\CacheBundle\DataCollector; -use Cache\CacheBundle\Cache\RecordingCachePool; +use Cache\CacheBundle\Cache\Recording\CachePool; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\DataCollector\DataCollector; @@ -31,15 +31,15 @@ class CacheDataCollector extends DataCollector const TEMPLATE = 'CacheBundle:Collector:cache.html.twig'; /** - * @type RecordingCachePool[] + * @type CachePool[] */ private $instances = []; /** - * @param $name - * @param RecordingCachePool $instance + * @param string $name + * @param CachePool $instance */ - public function addInstance($name, RecordingCachePool $instance) + public function addInstance($name, CachePool $instance) { $this->instances[$name] = $instance; } diff --git a/src/DependencyInjection/Compiler/DataCollectorCompilerPass.php b/src/DependencyInjection/Compiler/DataCollectorCompilerPass.php index 48920cc..d86fe15 100644 --- a/src/DependencyInjection/Compiler/DataCollectorCompilerPass.php +++ b/src/DependencyInjection/Compiler/DataCollectorCompilerPass.php @@ -11,7 +11,8 @@ namespace Cache\CacheBundle\DependencyInjection\Compiler; -use Cache\CacheBundle\Cache\RecordingCachePool; +use Cache\AdapterBundle\DummyAdapter; +use Cache\CacheBundle\Cache\Recording\Factory; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; @@ -29,18 +30,39 @@ class DataCollectorCompilerPass implements CompilerPassInterface */ public function process(ContainerBuilder $container) { + if (!$container->hasDefinition('cache.data_collector')) { + return; + } + + // Create a factory service + $factoryId = 'cache.recorder_factory'; + $factory = $container->register($factoryId, Factory::class); + // Check if logging support is enabled + if ($container->hasParameter('cache.logging')) { + $config = $container->getParameter('cache.logging'); + $factory->addArgument(new Reference($config['logger'])); + $factory->addArgument($config['level']); + } + $collectorDefinition = $container->getDefinition('cache.data_collector'); $serviceIds = $container->findTaggedServiceIds('cache.provider'); foreach (array_keys($serviceIds) as $id) { - // Creating a LoggingCachePool instance, and passing it the new definition from above - $def = $container->register($id.'.recorder', RecordingCachePool::class); - $def->addArgument(new Reference($id.'.recorder.inner')) - ->setDecoratedService($id, null, 10); + // Get the pool definition and rename it. + $poolDefinition = $container->getDefinition($id); + $poolDefinition->setPublic(false); + $container->setDefinition($id.'.inner', $poolDefinition); + + // Create a recording pool with a factory + $recorderDefinition = $container->register($id, DummyAdapter::class); + $recorderDefinition->setFactory([new Reference($factoryId), 'create']); + $recorderDefinition->addArgument($id); + $recorderDefinition->addArgument(new Reference($id.'.inner')); + $recorderDefinition->setTags($poolDefinition->getTags()); - // Tell the collector to add the new logger - $collectorDefinition->addMethodCall('addInstance', [$id, new Reference($id.'.recorder')]); + // Tell the collector to add the new instance + $collectorDefinition->addMethodCall('addInstance', [$id, new Reference($id)]); } } } diff --git a/src/DependencyInjection/Compiler/LoggingCompilerPass.php b/src/DependencyInjection/Compiler/LoggingCompilerPass.php deleted file mode 100644 index 6c3ef06..0000000 --- a/src/DependencyInjection/Compiler/LoggingCompilerPass.php +++ /dev/null @@ -1,48 +0,0 @@ -, Tobias Nyholm - * - * This source file is subject to the MIT license that is bundled - * with this source code in the file LICENSE. - */ - -namespace Cache\CacheBundle\DependencyInjection\Compiler; - -use Cache\CacheBundle\Cache\LoggingCachePool; -use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; -use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Reference; - -/** - * Decorate our cache.providers with a logger. - * - * @author Tobias Nyholm - */ -class LoggingCompilerPass implements CompilerPassInterface -{ - /** - * {@inheritdoc} - */ - public function process(ContainerBuilder $container) - { - // Check if logging support is enabled - if (!$container->hasParameter('cache.logging')) { - return; - } - - $config = $container->getParameter('cache.logging'); - $serviceIds = $container->findTaggedServiceIds('cache.provider'); - - foreach (array_keys($serviceIds) as $id) { - $def = $container->register($id.'.logger', LoggingCachePool::class); - $def->addArgument(new Reference($id.'.logger.inner')) - ->setDecoratedService($id, null, 10) - ->addMethodCall('setLogger', [new Reference($config['logger'])]) - ->addMethodCall('setName', [$id]) - ->addMethodCall('setLevel', [$config['level']]); - } - } -} diff --git a/tests/Unit/DependencyInjection/DataCollectorCompilerPassTest.php b/tests/Unit/DependencyInjection/DataCollectorCompilerPassTest.php new file mode 100644 index 0000000..835090b --- /dev/null +++ b/tests/Unit/DependencyInjection/DataCollectorCompilerPassTest.php @@ -0,0 +1,70 @@ +, Tobias Nyholm + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Cache\CacheBundle\Tests\Unit\DependencyInjection; + +use Cache\CacheBundle\DependencyInjection\Compiler\DataCollectorCompilerPass; +use Matthias\SymfonyDependencyInjectionTest\PhpUnit\AbstractCompilerPassTestCase; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Reference; + +class DataCollectorCompilerPassTest extends AbstractCompilerPassTestCase +{ + protected function registerCompilerPass(ContainerBuilder $container) + { + $container->addCompilerPass(new DataCollectorCompilerPass()); + } + + public function testWithLogger() + { + $collector = new Definition(); + $this->setDefinition('cache.data_collector', $collector); + + $this->setParameter('cache.logging', ['logger' => 'foo_logger', 'level' => 'bar']); + $this->compile(); + + $this->assertContainerBuilderHasServiceDefinitionWithArgument( + 'cache.recorder_factory', + 0, + new Reference('foo_logger') + ); + $this->assertContainerBuilderHasServiceDefinitionWithArgument( + 'cache.recorder_factory', + 1, + 'bar' + ); + } + + public function testFactory() + { + $collector = new Definition(); + $this->setDefinition('cache.data_collector', $collector); + + $collectedService = new Definition(); + $collectedService->addTag('cache.provider'); + $this->setDefinition('collected_pool', $collectedService); + + $this->compile(); + + $this->assertContainerBuilderHasServiceDefinitionWithMethodCall( + 'cache.data_collector', + 'addInstance', + [ + 'collected_pool', + new Reference('collected_pool'), + ] + ); + + $this->assertContainerBuilderHasService('collected_pool.inner'); + $this->assertContainerBuilderHasServiceDefinitionWithTag('collected_pool', 'cache.provider'); + } +}