diff --git a/composer.json b/composer.json index e867685..45a599d 100644 --- a/composer.json +++ b/composer.json @@ -27,14 +27,15 @@ "php": "^5.6 || ^7.0", "symfony/framework-bundle": "^2.7 || ^3.0", "cache/taggable-cache": "^0.5", - "cache/session-handler": "^0.2" + "cache/session-handler": "^0.2", + "nyholm/nsa": "^1.1" }, "require-dev": { "phpunit/phpunit": "^5.7.17", "symfony/symfony": "^2.7 || ^3.0", "cache/psr-6-doctrine-bridge": "^3.0", "cache/array-adapter": "^0.5", - "nyholm/symfony-bundle-test": "^1.0.1", + "nyholm/symfony-bundle-test": "^1.2", "matthiasnoback/symfony-dependency-injection-test": "^1.0" }, "suggest": { diff --git a/src/Cache/Recording/Factory.php b/src/Cache/Recording/Factory.php deleted file mode 100644 index a7a7cb1..0000000 --- a/src/Cache/Recording/Factory.php +++ /dev/null @@ -1,74 +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\Recording; - -use Cache\Hierarchy\HierarchicalPoolInterface; -use Cache\TagInterop\TaggableCacheItemPoolInterface; -use Psr\Cache\CacheItemPoolInterface; -use Psr\Log\LoggerInterface; - -/** - * Create a recording CachePool. - * - * @author Tobias Nyholm - * - * @internal - */ -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 TaggableCacheItemPoolInterface && $pool instanceof HierarchicalPoolInterface) { - $recorder = new HierarchyAndTaggablePool($pool); - } elseif ($pool instanceof TaggableCacheItemPoolInterface) { - $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/TaggablePool.php b/src/Cache/Recording/TaggablePool.php deleted file mode 100644 index 0057537..0000000 --- a/src/Cache/Recording/TaggablePool.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\Cache\Recording; - -use Cache\TagInterop\TaggableCacheItemPoolInterface; - -/** - * @author Tobias Nyholm - * - * @internal - */ -class TaggablePool extends CachePool implements TaggableCacheItemPoolInterface -{ - /** - * {@inheritdoc} - */ - public function invalidateTag($tag) - { - $event = $this->start(__FUNCTION__, $tag); - try { - return $event->result = $this->pool->invalidateTag($tag); - } finally { - $event->end = microtime(true); - } - } - - /** - * {@inheritdoc} - */ - public function invalidateTags(array $tags) - { - $event = $this->start(__FUNCTION__, $tags); - try { - return $event->result = $this->pool->invalidateTags($tags); - } finally { - $event->end = microtime(true); - } - } -} diff --git a/src/DataCollector/CacheDataCollector.php b/src/DataCollector/CacheDataCollector.php index d371383..54b1148 100644 --- a/src/DataCollector/CacheDataCollector.php +++ b/src/DataCollector/CacheDataCollector.php @@ -11,7 +11,6 @@ namespace Cache\CacheBundle\DataCollector; -use Cache\CacheBundle\Cache\Recording\CachePool; use Cache\CacheBundle\Cache\Recording\TraceableAdapterEvent; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -26,15 +25,15 @@ class CacheDataCollector extends DataCollector { /** - * @type CachePool[] + * @type CacheProxy[] */ private $instances = []; /** - * @param string $name - * @param CachePool $instance + * @param string $name + * @param CacheProxy $instance */ - public function addInstance($name, CachePool $instance) + public function addInstance($name, CacheProxy $instance) { $this->instances[$name] = $instance; } @@ -47,7 +46,7 @@ public function collect(Request $request, Response $response, \Exception $except $empty = ['calls' => [], 'config' => [], 'options' => [], 'statistics' => []]; $this->data = ['instances' => $empty, 'total' => $empty]; foreach ($this->instances as $name => $instance) { - $this->data['instances']['calls'][$name] = $instance->getCalls(); + $this->data['instances']['calls'][$name] = $instance->__getCalls(); } $this->data['instances']['statistics'] = $this->calculateStatistics(); diff --git a/src/Cache/Recording/HierarchyAndTaggablePool.php b/src/DataCollector/CacheProxy.php similarity index 60% rename from src/Cache/Recording/HierarchyAndTaggablePool.php rename to src/DataCollector/CacheProxy.php index 718251c..759f5e8 100644 --- a/src/Cache/Recording/HierarchyAndTaggablePool.php +++ b/src/DataCollector/CacheProxy.php @@ -9,15 +9,16 @@ * with this source code in the file LICENSE. */ -namespace Cache\CacheBundle\Cache\Recording; - -use Cache\Hierarchy\HierarchicalPoolInterface; +namespace Cache\CacheBundle\DataCollector; /** - * @author Tobias Nyholm + * An interface for a cache proxy. A cache proxy is created when we profile a cache pool. * - * @internal + * @author Tobias Nyholm */ -class HierarchyAndTaggablePool extends TaggablePool implements HierarchicalPoolInterface +interface CacheProxy { + public function __getCalls(); + + public function __setName($name); } diff --git a/src/DataCollector/DecoratingFactory.php b/src/DataCollector/DecoratingFactory.php new file mode 100644 index 0000000..dfccc96 --- /dev/null +++ b/src/DataCollector/DecoratingFactory.php @@ -0,0 +1,55 @@ +, 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\DataCollector; + +use Nyholm\NSA; +use Psr\Cache\CacheItemPoolInterface; + +/** + * A factory that decorates another factory to be able to use the proxy cache. + * + * @author Tobias Nyholm + */ +class DecoratingFactory +{ + /** + * @type ProxyFactory + */ + private $proxyFactory; + + /** + * @param ProxyFactory $proxyFactory + */ + public function __construct(ProxyFactory $proxyFactory) + { + $this->proxyFactory = $proxyFactory; + } + + /** + * @param CacheItemPoolInterface $originalObject original class + * + * @return CacheProxy|CacheItemPoolInterface + */ + public function create($originalObject) + { + $proxyClass = $this->proxyFactory->createProxy(get_class($originalObject)); + $rc = new \ReflectionClass($proxyClass); + $pool = $rc->newInstanceWithoutConstructor(); + + // Copy properties from original pool to new + foreach (NSA::getProperties($originalObject) as $property) { + NSA::setProperty($pool, $property, NSA::getProperty($originalObject, $property)); + } + + return $pool; + } +} diff --git a/src/DataCollector/ProxyFactory.php b/src/DataCollector/ProxyFactory.php new file mode 100644 index 0000000..986cf0e --- /dev/null +++ b/src/DataCollector/ProxyFactory.php @@ -0,0 +1,74 @@ +, 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\DataCollector; + +/** + * Generate proxies over your cache pool. This should only be used in development. + * + * @author Tobias Nyholm + */ +class ProxyFactory +{ + /** + * @type string + */ + private $proxyDirectory; + + /** + * @param string $proxyDirectory + */ + public function __construct($proxyDirectory) + { + $this->proxyDirectory = $proxyDirectory; + } + + /** + * Create a proxy that handles data collecting better. + * + * @param string $class + * @param string &$proxyFile where we store the proxy class + * + * @return string the name of a much much better class + */ + public function createProxy($class, &$proxyFile = null) + { + $proxyClass = $this->getProxyClass($class); + $class = '\\'.rtrim($class, '\\'); + $proxyFile = $this->proxyDirectory.'/'.$proxyClass.'.php'; + + if (class_exists($proxyClass)) { + return $proxyClass; + } + + $content = file_get_contents(dirname(__DIR__).'/Resources/proxy/template.php'); + $content = str_replace('__TPL_CLASS__', $proxyClass, $content); + $content = str_replace('__TPL_EXTENDS__', $class, $content); + + $this->checkProxyDirectory(); + file_put_contents($proxyFile, $content); + require $proxyFile; + + return $proxyClass; + } + + private function checkProxyDirectory() + { + if (!is_dir($this->proxyDirectory)) { + @mkdir($this->proxyDirectory, 0777, true); + } + } + + private function getProxyClass($namespace) + { + return 'php_cache_proxy_'.str_replace('\\', '_', $namespace); + } +} diff --git a/src/Cache/Recording/HierarchyPool.php b/src/DataCollector/TraceableAdapterEvent.php similarity index 58% rename from src/Cache/Recording/HierarchyPool.php rename to src/DataCollector/TraceableAdapterEvent.php index a33a17c..ab3c799 100644 --- a/src/Cache/Recording/HierarchyPool.php +++ b/src/DataCollector/TraceableAdapterEvent.php @@ -9,15 +9,18 @@ * with this source code in the file LICENSE. */ -namespace Cache\CacheBundle\Cache\Recording; - -use Cache\Hierarchy\HierarchicalPoolInterface; +namespace Cache\CacheBundle\DataCollector; /** - * @author Tobias Nyholm - * * @internal */ -class HierarchyPool extends CachePool implements HierarchicalPoolInterface +class TraceableAdapterEvent { + public $name; + public $argument; + public $start; + public $end; + public $result; + public $hits = 0; + public $misses = 0; } diff --git a/src/DependencyInjection/Compiler/DataCollectorCompilerPass.php b/src/DependencyInjection/Compiler/DataCollectorCompilerPass.php index d86fe15..b82b952 100644 --- a/src/DependencyInjection/Compiler/DataCollectorCompilerPass.php +++ b/src/DependencyInjection/Compiler/DataCollectorCompilerPass.php @@ -11,10 +11,9 @@ namespace Cache\CacheBundle\DependencyInjection\Compiler; -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\Definition; use Symfony\Component\DependencyInjection\Reference; /** @@ -34,16 +33,7 @@ public function process(ContainerBuilder $container) 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']); - } - + $proxyFactory = $container->get('cache.proxy_factory'); $collectorDefinition = $container->getDefinition('cache.data_collector'); $serviceIds = $container->findTaggedServiceIds('cache.provider'); @@ -51,15 +41,24 @@ public function process(ContainerBuilder $container) // Get the pool definition and rename it. $poolDefinition = $container->getDefinition($id); - $poolDefinition->setPublic(false); - $container->setDefinition($id.'.inner', $poolDefinition); + if (null === $poolDefinition->getFactory()) { + // Just replace the class + $proxyClass = $proxyFactory->createProxy($poolDefinition->getClass(), $file); + $poolDefinition->setClass($proxyClass); + $poolDefinition->setFile($file); + $poolDefinition->addMethodCall('__setName', [$id]); + } else { + // Create a new ID for the original service + $innerId = $id.'.inner'; + $container->setDefinition($innerId, $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()); + // Create a new definition. + $decoratedPool = new Definition($poolDefinition->getClass()); + $decoratedPool->setFactory([new Reference('cache.decorating_factory'), 'create']); + $decoratedPool->setArguments([new Reference($innerId)]); + $container->setDefinition($id, $decoratedPool); + $decoratedPool->addMethodCall('__setName', [$id]); + } // Tell the collector to add the new instance $collectorDefinition->addMethodCall('addInstance', [$id, new Reference($id)]); diff --git a/src/Resources/config/data-collector.yml b/src/Resources/config/data-collector.yml index 48d9fc3..3c02faa 100644 --- a/src/Resources/config/data-collector.yml +++ b/src/Resources/config/data-collector.yml @@ -3,3 +3,12 @@ services: class: Cache\CacheBundle\DataCollector\CacheDataCollector tags: - { name: data_collector, template: 'CacheBundle:Collector:cache.html.twig', id: 'php-cache' } + + cache.proxy_factory: + class: Cache\CacheBundle\DataCollector\ProxyFactory + arguments: ['%kernel.cache_dir%/phpcache/proxy'] + + cache.decorating_factory: + class: Cache\CacheBundle\DataCollector\DecoratingFactory + arguments: ["@cache.proxy_factory"] + public: false diff --git a/src/Cache/Recording/CachePool.php b/src/Resources/proxy/template.php similarity index 51% rename from src/Cache/Recording/CachePool.php rename to src/Resources/proxy/template.php index 9ba58b6..91b69fd 100644 --- a/src/Cache/Recording/CachePool.php +++ b/src/Resources/proxy/template.php @@ -9,64 +9,20 @@ * with this source code in the file LICENSE. */ -namespace Cache\CacheBundle\Cache\Recording; - +use Cache\CacheBundle\DataCollector\CacheProxy; +use Cache\CacheBundle\DataCollector\TraceableAdapterEvent; 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 - * @author Nicolas Grekas - * - * @internal - */ -class CachePool implements CacheItemPoolInterface +class __TPL_CLASS__ extends __TPL_EXTENDS__ implements CacheProxy { - /** - * @type CacheItemPoolInterface - */ - protected $pool; - - /** - * @type LoggerInterface - */ - private $logger; - - /** - * @type string - */ - private $name; - - /** - * @type string - */ - private $level = 'info'; + private $__name; + private $__calls = []; - /** - * @type array calls - */ - private $calls = []; - - /** - * @param CacheItemPoolInterface $pool - */ - public function __construct(CacheItemPoolInterface $pool) - { - $this->pool = $pool; - } - - /** - * {@inheritdoc} - */ public function getItem($key) { $event = $this->start(__FUNCTION__, $key); try { - $item = $this->pool->getItem($key); + $item = parent::getItem($key); } finally { $event->end = microtime(true); } @@ -80,14 +36,11 @@ public function getItem($key) return $item; } - /** - * {@inheritdoc} - */ public function hasItem($key) { $event = $this->start(__FUNCTION__, $key); try { - $event->result = $this->pool->hasItem($key); + $event->result = parent::hasItem($key); } finally { $event->end = microtime(true); } @@ -99,53 +52,41 @@ public function hasItem($key) return $event->result; } - /** - * {@inheritdoc} - */ public function deleteItem($key) { $event = $this->start(__FUNCTION__, $key); try { - return $event->result = $this->pool->deleteItem($key); + return $event->result = parent::deleteItem($key); } finally { $event->end = microtime(true); } } - /** - * {@inheritdoc} - */ public function save(CacheItemInterface $item) { $event = $this->start(__FUNCTION__, $item); try { - return $event->result = $this->pool->save($item); + return $event->result = parent::save($item); } finally { $event->end = microtime(true); } } - /** - * {@inheritdoc} - */ public function saveDeferred(CacheItemInterface $item) { $event = $this->start(__FUNCTION__, $item); try { - return $event->result = $this->pool->saveDeferred($item); + return $event->result = parent::saveDeferred($item); } finally { $event->end = microtime(true); } } - /** - * {@inheritdoc} - */ public function getItems(array $keys = []) { $event = $this->start(__FUNCTION__, $keys); try { - $result = $this->pool->getItems($keys); + $result = parent::getItems($keys); } finally { $event->end = microtime(true); } @@ -165,99 +106,73 @@ public function getItems(array $keys = []) return $f(); } - /** - * {@inheritdoc} - */ public function clear() { $event = $this->start(__FUNCTION__); try { - return $event->result = $this->pool->clear(); + return $event->result = parent::clear(); } finally { $event->end = microtime(true); } } - /** - * {@inheritdoc} - */ public function deleteItems(array $keys) { $event = $this->start(__FUNCTION__, $keys); try { - return $event->result = $this->pool->deleteItems($keys); + return $event->result = parent::deleteItems($keys); } finally { $event->end = microtime(true); } } - /** - * {@inheritdoc} - */ public function commit() { $event = $this->start(__FUNCTION__); try { - return $event->result = $this->pool->commit(); + return $event->result = parent::commit(); } finally { $event->end = microtime(true); } } - public function getCalls() + public function invalidateTag($tag) { - return $this->calls; + $event = $this->start(__FUNCTION__, $tag); + try { + return $event->result = parent::invalidateTag($tag); + } finally { + $event->end = microtime(true); + } } - protected function start($name, $argument = null) + public function invalidateTags(array $tags) { - $this->calls[] = $event = new TraceableAdapterEvent(); - $event->name = $name; - $event->argument = $argument; - $event->start = microtime(true); - - return $event; + $event = $this->start(__FUNCTION__, $tags); + try { + return $event->result = parent::invalidateTags($tags); + } finally { + $event->end = microtime(true); + } } - /** - * @param LoggerInterface $logger - */ - public function setLogger(LoggerInterface $logger = null) + public function __getCalls() { - $this->logger = $logger; + return $this->__calls; } - /** - * @param string $name - */ - public function setName($name) + public function __setName($name) { - $this->name = $name; - - return $this; + $this->__name = $name; } - /** - * @param string $level - */ - public function setLevel($level) + private function start($name, $argument = null) { - $this->level = $level; + $this->__calls[] = $event = new TraceableAdapterEvent(); + $event->name = $name; + $event->argument = $argument; + $event->start = microtime(true); - return $this; + return $event; } } - -/** - * @internal - */ -class TraceableAdapterEvent -{ - public $name; - public $argument; - public $start; - public $end; - public $result; - public $hits = 0; - public $misses = 0; -} diff --git a/tests/Unit/DependencyInjection/DataCollectorCompilerPassTest.php b/tests/Unit/DependencyInjection/DataCollectorCompilerPassTest.php deleted file mode 100644 index 835090b..0000000 --- a/tests/Unit/DependencyInjection/DataCollectorCompilerPassTest.php +++ /dev/null @@ -1,70 +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\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'); - } -}