├── .gush.yml ├── Changelog.md ├── LICENSE ├── README.md ├── composer.json ├── src ├── Bridge │ └── SymfonyValidatorBridge.php ├── Cache │ └── FixedTaggingCachePool.php ├── CacheBundle.php ├── Command │ └── CacheFlushCommand.php ├── DataCollector │ ├── CacheDataCollector.php │ ├── CacheProxyInterface.php │ ├── DecoratingFactory.php │ ├── ProxyFactory.php │ └── TraceableAdapterEvent.php ├── DependencyInjection │ ├── CacheExtension.php │ ├── Compiler │ │ ├── CacheTaggingPass.php │ │ ├── DataCollectorCompilerPass.php │ │ ├── DoctrineCompilerPass.php │ │ ├── LoggerPass.php │ │ └── SessionSupportCompilerPass.php │ └── Configuration.php ├── Factory │ ├── DoctrineBridgeFactory.php │ ├── RouterFactory.php │ ├── SessionHandlerFactory.php │ └── ValidationFactory.php ├── KeyNormalizer.php ├── Resources │ ├── config │ │ └── data-collector.yml │ ├── proxy │ │ └── template.php │ └── views │ │ ├── Collector │ │ └── cache.html.twig │ │ └── Icon │ │ └── logo.svg └── Routing │ └── CachingRouter.php └── tests ├── Functional ├── BundleInitializationTest.php ├── DummyNormalizer.php ├── config.yml └── sf2_and_3.yml └── Unit ├── ContainerTest.php ├── DependencyInjection └── CacheExtensionTest.php ├── Fixtures └── router.yml ├── KeyNormalizerTest.php └── TestCase.php /.gush.yml: -------------------------------------------------------------------------------- 1 | # Gush configuration file, any comments will be lost. 2 | repo_adapter: github 3 | issue_tracker: github 4 | repo_org: php-cache 5 | repo_name: cache-bundle 6 | issue_project_org: php-cache 7 | issue_project_name: cache-bundle 8 | meta-header: "This file is part of php-cache\\cache-bundle package.\n\n(c) 2015-2015 Aaron Scherer \n\nThis source file is subject to the MIT license that is bundled\nwith this source code in the file LICENSE." 9 | -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | The change log describes what is "Added", "Removed", "Changed" or "Fixed" between each release. 4 | 5 | ## 1.0.2 6 | 7 | ### Fixed 8 | 9 | - Fixed inheritence issues with for SF3.3 and above. 10 | 11 | ## 1.0.1 12 | 13 | ### Fixed 14 | 15 | - Make sure we clone data better in the DataCollector. 16 | 17 | ## 1.0.0 18 | 19 | ### Added 20 | 21 | - Adds an option to disable the data collector 22 | - Support for SimpleCache 23 | 24 | ### Changed 25 | 26 | - Using stable depedencies 27 | - Added dynamic proxy classes to handle data collection when debugging 28 | 29 | ## 0.5.0 30 | 31 | ### Changed 32 | 33 | - Using cache/session-handler: ^0.2. **This will break all cached sessions** 34 | - Using cache/taggable-cache: ^0.5 to support the latest versions of the adapters. 35 | - New Collector and WebProfiler page 36 | 37 | ## 0.4.4 38 | 39 | ### Fixed 40 | 41 | - Make sure RecordingPool does not change the type of pool. 42 | 43 | ## 0.4.3 44 | 45 | ### Fixed 46 | 47 | * Require taggable 0.4.3 to avoid bugs in 0.4.2 48 | 49 | ## 0.4.2 50 | 51 | ### Added 52 | 53 | * A KeyNormalizer that cleans the cache keys from invalid chars. 54 | 55 | ### Fixed 56 | 57 | * Exception when clearing cache with a non taggable pool 58 | * Default value for the second argument to `RecordingCachePool::timeCall` should be array, not null. 59 | 60 | ## 0.4.1 61 | 62 | No changelog before this version 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Aaron Scherer, Tobias Nyholm 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PSR-6 Cache bundle 2 | [![Latest Stable Version](https://poser.pugx.org/cache/cache-bundle/v/stable)](https://packagist.org/packages/cache/cache-bundle) 3 | [![codecov.io](https://codecov.io/github/php-cache/cache-bundle/coverage.svg?branch=master)](https://codecov.io/github/php-cache/cache-bundle?branch=master) 4 | [![Build Status](https://travis-ci.org/php-cache/cache-bundle.svg?branch=master)](https://travis-ci.org/php-cache/cache-bundle) 5 | [![Total Downloads](https://poser.pugx.org/cache/cache-bundle/downloads)](https://packagist.org/packages/cache/cache-bundle) 6 | [![Monthly Downloads](https://poser.pugx.org/cache/cache-bundle/d/monthly.png)](https://packagist.org/packages/cache/cache-bundle) 7 | [![Quality Score](https://img.shields.io/scrutinizer/g/php-cache/cache-bundle.svg?style=flat-square)](https://scrutinizer-ci.com/g/php-cache/cache-bundle) 8 | [![SensioLabsInsight](https://insight.sensiolabs.com/projects/21963379-2b15-4cc4-bdf6-0f98aa292f8a/mini.png)](https://insight.sensiolabs.com/projects/21963379-2b15-4cc4-bdf6-0f98aa292f8a) 9 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) 10 | 11 | This is a Symfony bundle that lets you you integrate your PSR-6 compliant cache service with the framework. 12 | It lets you cache your sessions, routes and Doctrine results and metadata. It also provides an integration with the 13 | debug toolbar. This bundle does not contain any pool implementation nor does it help you register cache pool services. 14 | You maybe interested in [AdapterBundle](https://github.com/php-cache/adapter-bundle) which will help you configure and 15 | register PSR-6 cache pools as services. 16 | 17 | This bundle is a part of the PHP Cache organisation. To read about features like tagging and hierarchy support please 18 | read the shared documentation at [www.php-cache.com](http://www.php-cache.com). 19 | 20 | ### To Install 21 | 22 | Run the following in your project root, assuming you have composer set up for your project 23 | ```sh 24 | composer require cache/cache-bundle 25 | ``` 26 | 27 | Add the bundle to app/AppKernel.php 28 | 29 | ```php 30 | $bundles( 31 | // ... 32 | new Cache\CacheBundle\CacheBundle(), 33 | // ... 34 | ); 35 | ``` 36 | 37 | Read the documentation at [www.php-cache.com/symfony/cache-bundle](http://www.php-cache.com/en/latest/symfony/cache-bundle/). 38 | 39 | ### Contribute 40 | 41 | Contributions are very welcome! Send a pull request or report any issues you find on the [issue tracker](http://issues.php-cache.com). 42 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cache/cache-bundle", 3 | "type": "symfony-bundle", 4 | "description": "Symfony 2 bundle providing integration between PSR-6 compliant cache services and the framework. It supports cache for sessions, routing and Doctrine", 5 | "keywords": [ 6 | "cache", 7 | "psr6", 8 | "doctrine", 9 | "router", 10 | "session" 11 | ], 12 | "homepage": "http://www.php-cache.com/en/latest/", 13 | "license": "MIT", 14 | "authors": [ 15 | { 16 | "name": "Aaron Scherer", 17 | "email": "aequasi@gmail.com", 18 | "homepage": "https://github.com/aequasi" 19 | }, 20 | { 21 | "name": "Tobias Nyholm", 22 | "email": "tobias.nyholm@gmail.com", 23 | "homepage": "https://github.com/nyholm" 24 | } 25 | ], 26 | "require": { 27 | "php": "^5.6 || ^7.0", 28 | "cache/session-handler": "^1.0", 29 | "cache/taggable-cache": "^1.0", 30 | "nyholm/nsa": "^1.1", 31 | "symfony/config": "^2.7 || ^3.0 || ^4.0", 32 | "symfony/console": "^2.7 || ^3.0 || ^4.0", 33 | "symfony/dependency-injection": "^2.7 || ^3.0 || ^4.0", 34 | "symfony/framework-bundle": "^2.7 || ^3.0 || ^4.0", 35 | "symfony/http-foundation": "^2.7 || ^3.0 || ^4.0", 36 | "symfony/http-kernel": "^2.7 || ^3.0 || ^4.0", 37 | "symfony/var-dumper": "^2.7 || ^3.3 || ^4.0" 38 | }, 39 | "require-dev": { 40 | "cache/array-adapter": "^1.0", 41 | "cache/psr-6-doctrine-bridge": "^3.0", 42 | "doctrine/annotations": "^1.0", 43 | "matthiasnoback/symfony-dependency-injection-test": "^1.1 || ^2.3", 44 | "nyholm/symfony-bundle-test": "^1.2", 45 | "symfony/phpunit-bridge": "^3.3 || ^4.0", 46 | "symfony/routing": "^2.7 || ^3.0 || ^4.0", 47 | "symfony/serializer": "^2.7 || ^3.0 || ^4.0", 48 | "symfony/validator": "^2.7 || ^3.0 || ^4.0" 49 | }, 50 | "suggest": { 51 | "cache/adapter-bundle": "To register PSR-6 compliant cache implementations as services.", 52 | "cache/psr-6-doctrine-bridge": "To be able to use Doctrine query, result and metadata cache." 53 | }, 54 | "autoload": { 55 | "psr-4": { 56 | "Cache\\CacheBundle\\": "src/" 57 | } 58 | }, 59 | "autoload-dev": { 60 | "psr-4": { 61 | "Cache\\CacheBundle\\Tests\\": "tests/" 62 | } 63 | }, 64 | "scripts": { 65 | "test": "vendor/bin/simple-phpunit", 66 | "test-ci": "vendor/bin/simple-phpunit --coverage-text --coverage-clover=build/coverage.xml" 67 | }, 68 | "config": { 69 | "sort-packages": true 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Bridge/SymfonyValidatorBridge.php: -------------------------------------------------------------------------------- 1 | , Tobias Nyholm 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Cache\CacheBundle\Bridge; 13 | 14 | use Psr\Cache\CacheItemPoolInterface; 15 | use Symfony\Component\Validator\Mapping\Cache\CacheInterface; 16 | use Symfony\Component\Validator\Mapping\ClassMetadata; 17 | 18 | /** 19 | * @author Tobias Nyholm 20 | */ 21 | class SymfonyValidatorBridge implements CacheInterface 22 | { 23 | /** 24 | * @type CacheItemPoolInterface 25 | */ 26 | private $pool; 27 | 28 | /** 29 | * DoctrineCacheBridge constructor. 30 | * 31 | * @param CacheItemPoolInterface $cachePool 32 | */ 33 | public function __construct(CacheItemPoolInterface $cachePool) 34 | { 35 | $this->pool = $cachePool; 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | public function has($class) 42 | { 43 | return $this->pool->hasItem($this->normalizeKey($class)); 44 | } 45 | 46 | /** 47 | * {@inheritdoc} 48 | */ 49 | public function read($class) 50 | { 51 | $item = $this->pool->getItem($this->normalizeKey($class)); 52 | 53 | if (!$item->isHit()) { 54 | return false; 55 | } 56 | 57 | return $item->get(); 58 | } 59 | 60 | /** 61 | * {@inheritdoc} 62 | */ 63 | public function write(ClassMetadata $metadata) 64 | { 65 | $item = $this->pool->getItem($this->normalizeKey($metadata->getClassName())); 66 | $item->set($metadata); 67 | $this->pool->save($item); 68 | } 69 | 70 | /** 71 | * @param string $key 72 | * 73 | * @return string 74 | */ 75 | private function normalizeKey($key) 76 | { 77 | return preg_replace('|[\\\/]|', '.', $key); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Cache/FixedTaggingCachePool.php: -------------------------------------------------------------------------------- 1 | , Tobias Nyholm 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Cache\CacheBundle\Cache; 13 | 14 | use Cache\TagInterop\TaggableCacheItemInterface; 15 | use Cache\TagInterop\TaggableCacheItemPoolInterface; 16 | use Psr\Cache\CacheItemInterface; 17 | 18 | /** 19 | * This class is a decorator for a TaggableCacheItemPoolInterface. It tags everything with predefined tags. 20 | * 21 | * @author Tobias Nyholm 22 | * 23 | * @internal 24 | */ 25 | class FixedTaggingCachePool implements TaggableCacheItemPoolInterface 26 | { 27 | /** 28 | * @type TaggableCacheItemPoolInterface 29 | */ 30 | private $cache; 31 | 32 | /** 33 | * @type array 34 | */ 35 | private $tags; 36 | 37 | /** 38 | * @param TaggableCacheItemPoolInterface $cache 39 | * @param array $tags 40 | */ 41 | public function __construct(TaggableCacheItemPoolInterface $cache, array $tags) 42 | { 43 | $this->cache = $cache; 44 | $this->tags = $tags; 45 | } 46 | 47 | /** 48 | * {@inheritdoc} 49 | */ 50 | public function getItem($key) 51 | { 52 | return $this->cache->getItem($key); 53 | } 54 | 55 | /** 56 | * {@inheritdoc} 57 | */ 58 | public function getItems(array $keys = []) 59 | { 60 | return $this->cache->getItems($keys); 61 | } 62 | 63 | /** 64 | * {@inheritdoc} 65 | */ 66 | public function hasItem($key) 67 | { 68 | return $this->cache->hasItem($key); 69 | } 70 | 71 | /** 72 | * {@inheritdoc} 73 | */ 74 | public function clear() 75 | { 76 | return $this->cache->clear(); 77 | } 78 | 79 | /** 80 | * {@inheritdoc} 81 | */ 82 | public function deleteItem($key) 83 | { 84 | return $this->cache->deleteItem($key); 85 | } 86 | 87 | /** 88 | * {@inheritdoc} 89 | */ 90 | public function deleteItems(array $keys) 91 | { 92 | return $this->cache->deleteItems($keys); 93 | } 94 | 95 | /** 96 | * {@inheritdoc} 97 | */ 98 | public function save(CacheItemInterface $item) 99 | { 100 | if (!$item instanceof TaggableCacheItemInterface) { 101 | throw new \InvalidArgumentException('Cache items are not transferable between pools. Item MUST implement TaggableCacheItemInterface.'); 102 | } 103 | 104 | $item->setTags($this->tags); 105 | 106 | return $this->cache->save($item); 107 | } 108 | 109 | /** 110 | * {@inheritdoc} 111 | */ 112 | public function saveDeferred(CacheItemInterface $item) 113 | { 114 | if (!$item instanceof TaggableCacheItemInterface) { 115 | throw new \InvalidArgumentException('Cache items are not transferable between pools. Item MUST implement TaggableCacheItemInterface.'); 116 | } 117 | 118 | $item->setTags($this->tags); 119 | 120 | return $this->cache->saveDeferred($item); 121 | } 122 | 123 | /** 124 | * {@inheritdoc} 125 | */ 126 | public function commit() 127 | { 128 | return $this->cache->commit(); 129 | } 130 | 131 | /** 132 | * {@inheritdoc} 133 | */ 134 | public function invalidateTag($tag) 135 | { 136 | return $this->invalidateTag($tag); 137 | } 138 | 139 | /** 140 | * {@inheritdoc} 141 | */ 142 | public function invalidateTags(array $tags) 143 | { 144 | return $this->cache - $this->invalidateTags($tags); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/CacheBundle.php: -------------------------------------------------------------------------------- 1 | , Tobias Nyholm 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Cache\CacheBundle; 13 | 14 | use Cache\CacheBundle\DependencyInjection\Compiler; 15 | use Symfony\Component\DependencyInjection\ContainerBuilder; 16 | use Symfony\Component\HttpKernel\Bundle\Bundle; 17 | 18 | /** 19 | * @author Aaron Scherer 20 | */ 21 | class CacheBundle extends Bundle 22 | { 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | public function build(ContainerBuilder $container) 27 | { 28 | parent::build($container); 29 | 30 | $container->addCompilerPass(new Compiler\CacheTaggingPass()); 31 | $container->addCompilerPass(new Compiler\SessionSupportCompilerPass()); 32 | $container->addCompilerPass(new Compiler\DoctrineCompilerPass()); 33 | $container->addCompilerPass(new Compiler\LoggerPass()); 34 | $container->addCompilerPass(new Compiler\DataCollectorCompilerPass()); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Command/CacheFlushCommand.php: -------------------------------------------------------------------------------- 1 | , Tobias Nyholm 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Cache\CacheBundle\Command; 13 | 14 | use Cache\Taggable\TaggablePoolInterface; 15 | use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; 16 | use Symfony\Component\Console\Input\ArrayInput; 17 | use Symfony\Component\Console\Input\InputArgument; 18 | use Symfony\Component\Console\Input\InputInterface; 19 | use Symfony\Component\Console\Output\OutputInterface; 20 | use Symfony\Component\Console\Question\ConfirmationQuestion; 21 | 22 | /** 23 | * Class CacheFlushCommand. 24 | * 25 | * @author Aaron Scherer 26 | */ 27 | class CacheFlushCommand extends ContainerAwareCommand 28 | { 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | protected function configure() 33 | { 34 | $this->setName('cache:flush'); 35 | $this->setDescription('Flushes the given cache'); 36 | $this->addArgument('type', InputArgument::OPTIONAL, sprintf('Which type of cache do you want to clear? Valid types are: %s', implode(', ', $this->getValidTypes()))); 37 | $this->addArgument('service', InputArgument::OPTIONAL, 'If using type "provider" you must give a service id for the cache you want to clear.'); 38 | $this->setHelp(<<<'EOD' 39 | 40 | Types and their description 41 | all Clear all types of caches 42 | annotation Clear annotation cache 43 | doctrine Clear doctrine cache for query, result and metadata 44 | privider cache.acme Clear all the cache for the provider with service id "cache.acme" 45 | router Clear router cache 46 | serializer Clear serializer cache 47 | session Clear session cache. All your logged in users will be logged out. 48 | symfony Clear Symfony cache. This is the same as cache:clear 49 | validation Clear validation cache 50 | 51 | EOD 52 | ); 53 | } 54 | 55 | /** 56 | * {@inheritdoc} 57 | */ 58 | protected function execute(InputInterface $input, OutputInterface $output) 59 | { 60 | if (false === $type = $this->verifyArguments($input, $output)) { 61 | return 0; 62 | } 63 | 64 | $builtInTypes = ['annotation', 'doctrine', 'serializer', 'session', 'router', 'validation']; 65 | if (in_array($type, $builtInTypes)) { 66 | return $this->clearCacheForBuiltInType($type) ? 0 : 1; 67 | } 68 | 69 | if ($type === 'symfony') { 70 | return $this->clearSymfonyCache($output) ? 0 : 1; 71 | } 72 | 73 | if ($type === 'provider') { 74 | return $this->clearCacheForProvider($input->getArgument('service')) ? 0 : 1; 75 | } 76 | 77 | if ($type === 'all') { 78 | $result = true; 79 | foreach ($builtInTypes as $builtInType) { 80 | $result = $result && $this->clearCacheForBuiltInType($builtInType); 81 | } 82 | $result = $result && $this->clearSymfonyCache($output); 83 | 84 | return $result ? 0 : 1; 85 | } 86 | } 87 | 88 | /** 89 | * Clear the cache for a type. 90 | * 91 | * @param string $type 92 | * 93 | * @return bool 94 | */ 95 | private function clearCacheForBuiltInType($type) 96 | { 97 | if (!$this->getContainer()->hasParameter(sprintf('cache.%s', $type))) { 98 | return true; 99 | } 100 | 101 | $config = $this->getContainer()->getParameter(sprintf('cache.%s', $type)); 102 | 103 | if ($type === 'doctrine') { 104 | $result = true; 105 | $result = $result && $this->clearTypedCacheFromService($type, $config['metadata']['service_id']); 106 | $result = $result && $this->clearTypedCacheFromService($type, $config['result']['service_id']); 107 | $result = $result && $this->clearTypedCacheFromService($type, $config['query']['service_id']); 108 | 109 | return $result; 110 | } else { 111 | return $this->clearTypedCacheFromService($type, $config['service_id']); 112 | } 113 | } 114 | 115 | /** 116 | * @param string $type 117 | * @param string $serviceId 118 | * 119 | * @return bool 120 | */ 121 | private function clearTypedCacheFromService($type, $serviceId) 122 | { 123 | /** @type \Psr\Cache\CacheItemPoolInterface $service */ 124 | $service = $this->getContainer()->get($serviceId); 125 | if ($service instanceof TaggablePoolInterface) { 126 | return $service->clearTags([$type]); 127 | } else { 128 | return $service->clear(); 129 | } 130 | } 131 | 132 | /** 133 | * @param InputInterface $input 134 | * @param OutputInterface $output 135 | * 136 | * @return string|bool type or false if invalid arguements 137 | */ 138 | protected function verifyArguments(InputInterface $input, OutputInterface $output) 139 | { 140 | $type = $input->getArgument('type'); 141 | $validTypes = $this->getValidTypes(); 142 | if ($type === null) { 143 | // ask a question and default $type='all' 144 | $helper = $this->getHelper('question'); 145 | $question = new ConfirmationQuestion('Do you want to clear all cache? [N] ', false); 146 | 147 | if (!$helper->ask($input, $output, $question)) { 148 | return false; 149 | } 150 | 151 | $type = 'all'; 152 | } 153 | 154 | if (!in_array($type, $validTypes)) { 155 | $output->writeln( 156 | sprintf( 157 | 'Type "%s" does not exist. Valid type are: %s.', 158 | $type, 159 | implode(', ', $validTypes) 160 | ) 161 | ); 162 | 163 | return false; 164 | } 165 | 166 | if ($type === 'provider' && !$input->hasArgument('service')) { 167 | $output->writeln( 168 | 'When using type "provider" you must specify a service id for that provider.' 169 | ); 170 | $output->writeln('Usage: php app/console cache:flush provider cache.provider.acme'); 171 | 172 | return false; 173 | } 174 | 175 | return $type; 176 | } 177 | 178 | /** 179 | * @param string $serviceId 180 | * 181 | * @return bool 182 | */ 183 | protected function clearCacheForProvider($serviceId) 184 | { 185 | /** @type \Psr\Cache\CacheItemPoolInterface $service */ 186 | $service = $this->getContainer()->get($serviceId); 187 | 188 | return $service->clear(); 189 | } 190 | 191 | /** 192 | * @param OutputInterface $output 193 | * 194 | * @return bool 195 | */ 196 | protected function clearSymfonyCache(OutputInterface $output) 197 | { 198 | $command = $this->getApplication()->find('cache:clear'); 199 | $arguments = [ 200 | 'command' => 'cache:clear', 201 | ]; 202 | 203 | return $command->run(new ArrayInput($arguments), $output) === 0; 204 | } 205 | 206 | /** 207 | * List of valid cache identifiers. 208 | * 209 | * @return string[] 210 | */ 211 | private function getValidTypes() 212 | { 213 | return ['all', 'annotation', 'session', 'serializer', 'router', 'doctrine', 'symfony', 'validation', 'provider']; 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/DataCollector/CacheDataCollector.php: -------------------------------------------------------------------------------- 1 | , Tobias Nyholm 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Cache\CacheBundle\DataCollector; 13 | 14 | use Symfony\Component\HttpFoundation\Request; 15 | use Symfony\Component\HttpFoundation\Response; 16 | use Symfony\Component\HttpKernel\DataCollector\DataCollector; 17 | use Symfony\Component\VarDumper\Caster\CutStub; 18 | use Symfony\Component\VarDumper\Cloner\Stub; 19 | use Symfony\Component\VarDumper\Cloner\VarCloner; 20 | 21 | /** 22 | * @author Aaron Scherer 23 | * @author Tobias Nyholm 24 | * 25 | * @internal 26 | */ 27 | class CacheDataCollector extends DataCollector 28 | { 29 | /** 30 | * @type CacheProxyInterface[] 31 | */ 32 | private $instances = []; 33 | 34 | /** 35 | * @type VarCloner 36 | */ 37 | private $cloner = null; 38 | 39 | /** 40 | * @param string $name 41 | * @param CacheProxyInterface $instance 42 | */ 43 | public function addInstance($name, CacheProxyInterface $instance) 44 | { 45 | $this->instances[$name] = $instance; 46 | } 47 | 48 | /** 49 | * {@inheritdoc} 50 | */ 51 | public function collect(Request $request, Response $response, \Exception $exception = null) 52 | { 53 | $empty = ['calls' => [], 'config' => [], 'options' => [], 'statistics' => []]; 54 | $this->data = ['instances' => $empty, 'total' => $empty]; 55 | foreach ($this->instances as $name => $instance) { 56 | $calls = $instance->__getCalls(); 57 | foreach ($calls as $call) { 58 | if (isset($call->result)) { 59 | $call->result = $this->cloneData($call->result); 60 | } 61 | if (isset($call->argument)) { 62 | $call->argument = $this->cloneData($call->argument); 63 | } 64 | } 65 | $this->data['instances']['calls'][$name] = $calls; 66 | } 67 | 68 | $this->data['instances']['statistics'] = $this->calculateStatistics(); 69 | $this->data['total']['statistics'] = $this->calculateTotalStatistics(); 70 | } 71 | 72 | /** 73 | * {@inheritdoc} 74 | */ 75 | public function reset() 76 | { 77 | $empty = ['calls' => [], 'config' => [], 'options' => [], 'statistics' => []]; 78 | $this->data = ['instances' => $empty, 'total' => $empty]; 79 | } 80 | 81 | /** 82 | * To be compatible with many versions of Symfony. 83 | * 84 | * @param $var 85 | */ 86 | private function cloneData($var) 87 | { 88 | if (method_exists($this, 'cloneVar')) { 89 | // Symfony 3.2 or higher 90 | return $this->cloneVar($var); 91 | } 92 | 93 | if (null === $this->cloner) { 94 | $this->cloner = new VarCloner(); 95 | $this->cloner->setMaxItems(-1); 96 | $this->cloner->addCasters($this->getCloneCasters()); 97 | } 98 | 99 | return $this->cloner->cloneVar($var); 100 | } 101 | 102 | /** 103 | * {@inheritdoc} 104 | */ 105 | public function getName() 106 | { 107 | return 'php-cache'; 108 | } 109 | 110 | /** 111 | * Method returns amount of logged Cache reads: "get" calls. 112 | * 113 | * @return array 114 | */ 115 | public function getStatistics() 116 | { 117 | return $this->data['instances']['statistics']; 118 | } 119 | 120 | /** 121 | * Method returns the statistic totals. 122 | * 123 | * @return array 124 | */ 125 | public function getTotals() 126 | { 127 | return $this->data['total']['statistics']; 128 | } 129 | 130 | /** 131 | * Method returns all logged Cache call objects. 132 | * 133 | * @return mixed 134 | */ 135 | public function getCalls() 136 | { 137 | return $this->data['instances']['calls']; 138 | } 139 | 140 | /** 141 | * @return array 142 | */ 143 | private function calculateStatistics() 144 | { 145 | $statistics = []; 146 | foreach ($this->data['instances']['calls'] as $name => $calls) { 147 | $statistics[$name] = [ 148 | 'calls' => 0, 149 | 'time' => 0, 150 | 'reads' => 0, 151 | 'writes' => 0, 152 | 'deletes' => 0, 153 | 'hits' => 0, 154 | 'misses' => 0, 155 | ]; 156 | /** @type TraceableAdapterEvent $call */ 157 | foreach ($calls as $call) { 158 | $statistics[$name]['calls'] += 1; 159 | $statistics[$name]['time'] += $call->end - $call->start; 160 | if ('getItem' === $call->name) { 161 | $statistics[$name]['reads'] += 1; 162 | if ($call->hits) { 163 | $statistics[$name]['hits'] += 1; 164 | } else { 165 | $statistics[$name]['misses'] += 1; 166 | } 167 | } elseif ('getItems' === $call->name) { 168 | $count = $call->hits + $call->misses; 169 | $statistics[$name]['reads'] += $count; 170 | $statistics[$name]['hits'] += $call->hits; 171 | $statistics[$name]['misses'] += $count - $call->misses; 172 | } elseif ('hasItem' === $call->name) { 173 | $statistics[$name]['reads'] += 1; 174 | if (false === $call->result) { 175 | $statistics[$name]['misses'] += 1; 176 | } else { 177 | $statistics[$name]['hits'] += 1; 178 | } 179 | } elseif ('save' === $call->name) { 180 | $statistics[$name]['writes'] += 1; 181 | } elseif ('deleteItem' === $call->name) { 182 | $statistics[$name]['deletes'] += 1; 183 | } 184 | } 185 | if ($statistics[$name]['reads']) { 186 | $statistics[$name]['hit_read_ratio'] = round(100 * $statistics[$name]['hits'] / $statistics[$name]['reads'], 2); 187 | } else { 188 | $statistics[$name]['hit_read_ratio'] = null; 189 | } 190 | } 191 | 192 | return $statistics; 193 | } 194 | 195 | /** 196 | * @return array 197 | */ 198 | private function calculateTotalStatistics() 199 | { 200 | $statistics = $this->getStatistics(); 201 | $totals = [ 202 | 'calls' => 0, 203 | 'time' => 0, 204 | 'reads' => 0, 205 | 'writes' => 0, 206 | 'deletes' => 0, 207 | 'hits' => 0, 208 | 'misses' => 0, 209 | ]; 210 | foreach ($statistics as $name => $values) { 211 | foreach ($totals as $key => $value) { 212 | $totals[$key] += $statistics[$name][$key]; 213 | } 214 | } 215 | if ($totals['reads']) { 216 | $totals['hit_read_ratio'] = round(100 * $totals['hits'] / $totals['reads'], 2); 217 | } else { 218 | $totals['hit_read_ratio'] = null; 219 | } 220 | 221 | return $totals; 222 | } 223 | 224 | /** 225 | * @return callable[] The casters to add to the cloner 226 | */ 227 | private function getCloneCasters() 228 | { 229 | return [ 230 | '*' => function ($v, array $a, Stub $s, $isNested) { 231 | if (!$v instanceof Stub) { 232 | foreach ($a as $k => $v) { 233 | if (is_object($v) && !$v instanceof \DateTimeInterface && !$v instanceof Stub) { 234 | $a[$k] = new CutStub($v); 235 | } 236 | } 237 | } 238 | 239 | return $a; 240 | }, 241 | ]; 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /src/DataCollector/CacheProxyInterface.php: -------------------------------------------------------------------------------- 1 | , Tobias Nyholm 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Cache\CacheBundle\DataCollector; 13 | 14 | use Psr\Cache\CacheItemPoolInterface; 15 | 16 | /** 17 | * An interface for a cache proxy. A cache proxy is created when we profile a cache pool. 18 | * 19 | * @author Tobias Nyholm 20 | */ 21 | interface CacheProxyInterface extends CacheItemPoolInterface 22 | { 23 | public function __getCalls(); 24 | 25 | public function __setName($name); 26 | } 27 | -------------------------------------------------------------------------------- /src/DataCollector/DecoratingFactory.php: -------------------------------------------------------------------------------- 1 | , Tobias Nyholm 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Cache\CacheBundle\DataCollector; 13 | 14 | use Nyholm\NSA; 15 | use Psr\Cache\CacheItemPoolInterface; 16 | 17 | /** 18 | * A factory that decorates another factory to be able to use the proxy cache. 19 | * 20 | * @author Tobias Nyholm 21 | */ 22 | class DecoratingFactory 23 | { 24 | /** 25 | * @type ProxyFactory 26 | */ 27 | private $proxyFactory; 28 | 29 | /** 30 | * @param ProxyFactory $proxyFactory 31 | */ 32 | public function __construct(ProxyFactory $proxyFactory) 33 | { 34 | $this->proxyFactory = $proxyFactory; 35 | } 36 | 37 | /** 38 | * @param CacheItemPoolInterface $originalObject original class 39 | * 40 | * @return CacheProxyInterface 41 | */ 42 | public function create($originalObject) 43 | { 44 | $proxyClass = $this->proxyFactory->createProxy(get_class($originalObject)); 45 | $reflection = new \ReflectionClass($proxyClass); 46 | $pool = $reflection->newInstanceWithoutConstructor(); 47 | 48 | // Copy properties from original pool to new 49 | foreach (NSA::getProperties($originalObject) as $property) { 50 | NSA::setProperty($pool, $property, NSA::getProperty($originalObject, $property)); 51 | } 52 | 53 | return $pool; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/DataCollector/ProxyFactory.php: -------------------------------------------------------------------------------- 1 | , Tobias Nyholm 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Cache\CacheBundle\DataCollector; 13 | 14 | /** 15 | * Generate proxies over your cache pool. This should only be used in development. 16 | * 17 | * @author Tobias Nyholm 18 | */ 19 | class ProxyFactory 20 | { 21 | /** 22 | * @type string 23 | */ 24 | private $proxyDirectory; 25 | 26 | /** 27 | * @param string $proxyDirectory 28 | */ 29 | public function __construct($proxyDirectory) 30 | { 31 | $this->proxyDirectory = $proxyDirectory; 32 | } 33 | 34 | /** 35 | * Create a proxy that handles data collecting better. 36 | * 37 | * @param string $class 38 | * @param string &$proxyFile where we store the proxy class 39 | * 40 | * @return string the name of a much much better class 41 | */ 42 | public function createProxy($class, &$proxyFile = null) 43 | { 44 | $proxyClass = $this->getProxyClass($class); 45 | $class = '\\'.rtrim($class, '\\'); 46 | $proxyFile = $this->proxyDirectory.'/'.$proxyClass.'.php'; 47 | 48 | if (class_exists($proxyClass)) { 49 | return $proxyClass; 50 | } 51 | 52 | if (file_exists($proxyFile)) { 53 | require $proxyFile; 54 | 55 | return $proxyClass; 56 | } 57 | 58 | $content = file_get_contents(dirname(__DIR__).'/Resources/proxy/template.php'); 59 | $content = str_replace('__TPL_CLASS__', $proxyClass, $content); 60 | $content = str_replace('__TPL_EXTENDS__', $class, $content); 61 | 62 | $this->checkProxyDirectory(); 63 | file_put_contents($proxyFile, $content); 64 | require $proxyFile; 65 | 66 | return $proxyClass; 67 | } 68 | 69 | private function checkProxyDirectory() 70 | { 71 | if (!is_dir($this->proxyDirectory)) { 72 | @mkdir($this->proxyDirectory, 0777, true); 73 | } 74 | } 75 | 76 | private function getProxyClass($namespace) 77 | { 78 | return 'php_cache_proxy_'.str_replace('\\', '_', $namespace); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/DataCollector/TraceableAdapterEvent.php: -------------------------------------------------------------------------------- 1 | , Tobias Nyholm 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Cache\CacheBundle\DataCollector; 13 | 14 | /** 15 | * @internal 16 | */ 17 | class TraceableAdapterEvent 18 | { 19 | public $name; 20 | public $argument; 21 | public $start; 22 | public $end; 23 | public $result; 24 | public $hits = 0; 25 | public $misses = 0; 26 | } 27 | -------------------------------------------------------------------------------- /src/DependencyInjection/CacheExtension.php: -------------------------------------------------------------------------------- 1 | , Tobias Nyholm 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Cache\CacheBundle\DependencyInjection; 13 | 14 | use Cache\Bridge\Doctrine\DoctrineCacheBridge; 15 | use Cache\CacheBundle\Bridge\SymfonyValidatorBridge; 16 | use Cache\CacheBundle\Command\CacheFlushCommand; 17 | use Cache\CacheBundle\Factory\DoctrineBridgeFactory; 18 | use Cache\CacheBundle\Factory\RouterFactory; 19 | use Cache\CacheBundle\Factory\SessionHandlerFactory; 20 | use Cache\CacheBundle\Factory\ValidationFactory; 21 | use Cache\CacheBundle\Routing\CachingRouter; 22 | use Cache\SessionHandler\Psr6SessionHandler; 23 | use Symfony\Component\Config\FileLocator; 24 | use Symfony\Component\DependencyInjection\ContainerBuilder; 25 | use Symfony\Component\DependencyInjection\Loader; 26 | use Symfony\Component\DependencyInjection\Reference; 27 | use Symfony\Component\HttpKernel\DependencyInjection\Extension; 28 | 29 | /** 30 | * @author Aaron Scherer 31 | * @author Tobias Nyholm 32 | */ 33 | class CacheExtension extends Extension 34 | { 35 | /** 36 | * Loads the configs for Cache and puts data into the container. 37 | * 38 | * @param array $configs Array of configs 39 | * @param ContainerBuilder $container Container Object 40 | */ 41 | public function load(array $configs, ContainerBuilder $container) 42 | { 43 | $config = $this->processConfiguration(new Configuration(), $configs); 44 | 45 | $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); 46 | 47 | // Make sure config values are in the parameters 48 | foreach (['router', 'session', 'doctrine', 'logging', 'annotation', 'serializer', 'validation'] as $section) { 49 | if ($config[$section]['enabled']) { 50 | $container->setParameter('cache.'.$section, $config[$section]); 51 | } 52 | } 53 | 54 | if ($config['doctrine']['enabled']) { 55 | $this->verifyDoctrineBridgeExists('doctrine'); 56 | } 57 | 58 | $this->registerServices($container, $config); 59 | 60 | // Add toolbar and data collector if we are debugging 61 | if (!isset($config['data_collector']['enabled'])) { 62 | $config['data_collector']['enabled'] = $container->getParameter('kernel.debug'); 63 | } 64 | 65 | if ($config['data_collector']['enabled']) { 66 | $loader->load('data-collector.yml'); 67 | } 68 | 69 | // Get a list of the psr-6 services we are using. 70 | $serviceIds = []; 71 | $this->findServiceIds($config, $serviceIds); 72 | $container->setParameter('cache.provider_service_ids', $serviceIds); 73 | 74 | $container->register(CacheFlushCommand::class, CacheFlushCommand::class) 75 | ->addTag('console.command', ['command' => 'cache:flush']); 76 | } 77 | 78 | /** 79 | * Find service ids that we configured. These services should be tagged so we can use them in the debug toolbar. 80 | * 81 | * @param array $config 82 | * @param array $serviceIds 83 | */ 84 | protected function findServiceIds(array $config, array &$serviceIds) 85 | { 86 | foreach ($config as $name => $value) { 87 | if (is_array($value)) { 88 | $this->findServiceIds($value, $serviceIds); 89 | } elseif ($name === 'service_id') { 90 | $serviceIds[] = $value; 91 | } 92 | } 93 | } 94 | 95 | /** 96 | * Make sure the DoctrineBridge is installed. 97 | * 98 | * @param string $name 99 | * 100 | * @throws \Exception 101 | */ 102 | private function verifyDoctrineBridgeExists($name) 103 | { 104 | if (!class_exists('Cache\Bridge\Doctrine\DoctrineCacheBridge')) { 105 | throw new \Exception( 106 | sprintf( 107 | 'You need the DoctrineCacheBridge to be able to use "%s". Please run "composer require cache/psr-6-doctrine-bridge" to install the missing dependency.', 108 | $name 109 | ) 110 | ); 111 | } 112 | } 113 | 114 | /** 115 | * @return string 116 | */ 117 | public function getAlias() 118 | { 119 | return 'cache'; 120 | } 121 | 122 | /** 123 | * Register services. All service ids will start witn "cache.service.". 124 | * 125 | * @param ContainerBuilder $container 126 | * @param $config 127 | * 128 | * @throws \Exception 129 | */ 130 | private function registerServices(ContainerBuilder $container, $config) 131 | { 132 | if ($config['annotation']['enabled']) { 133 | $this->verifyDoctrineBridgeExists('annotation'); 134 | $container->register('cache.service.annotation', DoctrineCacheBridge::class) 135 | ->setFactory([DoctrineBridgeFactory::class, 'get']) 136 | ->addArgument(new Reference($config['annotation']['service_id'])) 137 | ->addArgument($config['annotation']) 138 | ->addArgument(['annotation']); 139 | } 140 | 141 | if ($config['serializer']['enabled']) { 142 | $this->verifyDoctrineBridgeExists('serializer'); 143 | $container->register('cache.service.serializer', DoctrineCacheBridge::class) 144 | ->setFactory([DoctrineBridgeFactory::class, 'get']) 145 | ->addArgument(new Reference($config['serializer']['service_id'])) 146 | ->addArgument($config['serializer']) 147 | ->addArgument(['serializer']); 148 | } 149 | 150 | if ($config['validation']['enabled']) { 151 | $container->register('cache.service.validation', SymfonyValidatorBridge::class) 152 | ->setFactory([ValidationFactory::class, 'get']) 153 | ->addArgument(new Reference($config['validation']['service_id'])) 154 | ->addArgument($config['validation']); 155 | } 156 | 157 | if ($config['session']['enabled']) { 158 | $container->register('cache.service.session', Psr6SessionHandler::class) 159 | ->setFactory([SessionHandlerFactory::class, 'get']) 160 | ->addArgument(new Reference($config['session']['service_id'])) 161 | ->addArgument($config['session']); 162 | } 163 | 164 | if ($config['router']['enabled']) { 165 | $container->register('cache.service.router', CachingRouter::class) 166 | ->setFactory([RouterFactory::class, 'get']) 167 | ->setDecoratedService('router', null, 10) 168 | ->addArgument(new Reference($config['router']['service_id'])) 169 | ->addArgument(new Reference('cache.service.router.inner')) 170 | ->addArgument($config['router']); 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/DependencyInjection/Compiler/CacheTaggingPass.php: -------------------------------------------------------------------------------- 1 | , Tobias Nyholm 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Cache\CacheBundle\DependencyInjection\Compiler; 13 | 14 | use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; 15 | use Symfony\Component\DependencyInjection\ContainerBuilder; 16 | 17 | /** 18 | * Make sure to tag all cache services we can find. 19 | * 20 | * @author Tobias Nyholm 21 | */ 22 | class CacheTaggingPass implements CompilerPassInterface 23 | { 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | public function process(ContainerBuilder $container) 28 | { 29 | // get service ids form parameters 30 | $serviceIds = $container->getParameter('cache.provider_service_ids'); 31 | 32 | foreach ($serviceIds as $id) { 33 | $def = $container->findDefinition($id); 34 | if (!$def->hasTag('cache.provider')) { 35 | $def->addTag('cache.provider'); 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/DependencyInjection/Compiler/DataCollectorCompilerPass.php: -------------------------------------------------------------------------------- 1 | , Tobias Nyholm 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Cache\CacheBundle\DependencyInjection\Compiler; 13 | 14 | use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; 15 | use Symfony\Component\DependencyInjection\ContainerBuilder; 16 | use Symfony\Component\DependencyInjection\Definition; 17 | use Symfony\Component\DependencyInjection\Reference; 18 | 19 | /** 20 | * Inject a data collector to all the cache services to be able to get detailed statistics. 21 | * 22 | * @author Aaron Scherer 23 | * @author Tobias Nyholm 24 | */ 25 | class DataCollectorCompilerPass implements CompilerPassInterface 26 | { 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | public function process(ContainerBuilder $container) 31 | { 32 | if (!$container->hasDefinition('cache.data_collector')) { 33 | return; 34 | } 35 | 36 | $proxyFactory = $container->get('cache.proxy_factory'); 37 | $collectorDefinition = $container->getDefinition('cache.data_collector'); 38 | $serviceIds = $container->findTaggedServiceIds('cache.provider'); 39 | 40 | foreach (array_keys($serviceIds) as $id) { 41 | 42 | // Get the pool definition and rename it. 43 | $poolDefinition = $container->getDefinition($id); 44 | if (null === $poolDefinition->getFactory()) { 45 | // Just replace the class 46 | $proxyClass = $proxyFactory->createProxy($poolDefinition->getClass(), $file); 47 | $poolDefinition->setClass($proxyClass); 48 | $poolDefinition->setFile($file); 49 | $poolDefinition->addMethodCall('__setName', [$id]); 50 | } else { 51 | // Create a new ID for the original service 52 | $innerId = $id.'.inner'; 53 | $container->setDefinition($innerId, $poolDefinition); 54 | 55 | // Create a new definition. 56 | $decoratedPool = new Definition($poolDefinition->getClass()); 57 | $decoratedPool->setFactory([new Reference('cache.decorating_factory'), 'create']); 58 | $decoratedPool->setArguments([new Reference($innerId)]); 59 | $container->setDefinition($id, $decoratedPool); 60 | $decoratedPool->addMethodCall('__setName', [$id]); 61 | } 62 | 63 | // Tell the collector to add the new instance 64 | $collectorDefinition->addMethodCall('addInstance', [$id, new Reference($id)]); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/DependencyInjection/Compiler/DoctrineCompilerPass.php: -------------------------------------------------------------------------------- 1 | , Tobias Nyholm 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Cache\CacheBundle\DependencyInjection\Compiler; 13 | 14 | use Cache\Bridge\Doctrine\DoctrineCacheBridge; 15 | use Cache\CacheBundle\Factory\DoctrineBridgeFactory; 16 | use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; 17 | use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; 18 | use Symfony\Component\DependencyInjection\ContainerBuilder; 19 | use Symfony\Component\DependencyInjection\Reference; 20 | 21 | /** 22 | * Add the doctrine bridge around the PSR-6 cache services. 23 | * 24 | * @author Aaron Scherer 25 | * @author Tobias Nyholm 26 | */ 27 | class DoctrineCompilerPass implements CompilerPassInterface 28 | { 29 | /** 30 | * @type ContainerBuilder 31 | */ 32 | private $container; 33 | 34 | /** 35 | * @throws \Exception 36 | * 37 | * @return void 38 | */ 39 | public function process(ContainerBuilder $container) 40 | { 41 | $this->container = $container; 42 | 43 | // If disabled, continue 44 | if (!$this->container->hasParameter('cache.doctrine')) { 45 | return; 46 | } 47 | 48 | if (!$this->hasDoctrine()) { 49 | throw new \Exception( 50 | 'Not able to find any doctrine caches to implement. Ensure you have Doctrine ORM or ODM' 51 | ); 52 | } 53 | 54 | $this->enableDoctrineSupport($this->container->getParameter('cache.doctrine')); 55 | } 56 | 57 | /** 58 | * Loads the Doctrine configuration. 59 | * 60 | * @param array $config A configuration array 61 | * 62 | * @throws InvalidConfigurationException 63 | */ 64 | protected function enableDoctrineSupport(array $config) 65 | { 66 | $types = ['entity_managers', 'document_managers']; 67 | // For each ['metadata' => [], 'result' => [], 'query' => []] 68 | foreach ($config as $cacheType => $typeConfig) { 69 | foreach ($types as $type) { 70 | if (!isset($typeConfig[$type])) { 71 | continue; 72 | } 73 | 74 | // Copy the tagging setting to the $typeConfig 75 | $typeConfig['use_tagging'] = $config['use_tagging']; 76 | 77 | // Doctrine can't talk to a PSR-6 cache, so we need a bridge 78 | $bridgeServiceId = sprintf('cache.service.doctrine.%s.%s.bridge', $cacheType, $type); 79 | $this->container->register($bridgeServiceId, DoctrineCacheBridge::class) 80 | ->setPublic(false) 81 | ->setFactory([DoctrineBridgeFactory::class, 'get']) 82 | ->addArgument(new Reference($typeConfig['service_id'])) 83 | ->addArgument($typeConfig) 84 | ->addArgument(['doctrine', $cacheType]); 85 | 86 | foreach ($typeConfig[$type] as $manager) { 87 | $doctrineDefinitionId = 88 | sprintf( 89 | 'doctrine.%s.%s_%s_cache', 90 | ($type === 'entity_managers' ? 'orm' : 'odm'), 91 | $manager, 92 | $cacheType 93 | ); 94 | 95 | // Replace the doctrine entity manager cache with our bridge 96 | $this->container->setAlias($doctrineDefinitionId, $bridgeServiceId); 97 | } 98 | } 99 | } 100 | } 101 | 102 | /** 103 | * Checks to see if there are ORM's or ODM's. 104 | * 105 | * @return bool 106 | */ 107 | private function hasDoctrine() 108 | { 109 | return 110 | $this->container->hasAlias('doctrine.orm.entity_manager') || 111 | $this->container->hasAlias('doctrine_mongodb.document_manager'); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/DependencyInjection/Compiler/LoggerPass.php: -------------------------------------------------------------------------------- 1 | , Tobias Nyholm 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Cache\CacheBundle\DependencyInjection\Compiler; 13 | 14 | use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; 15 | use Symfony\Component\DependencyInjection\ContainerBuilder; 16 | use Symfony\Component\DependencyInjection\Reference; 17 | 18 | /** 19 | * Add logging to pool implementing LoggerAwareInterface. 20 | * 21 | * @author Tobias Nyholm 22 | */ 23 | class LoggerPass implements CompilerPassInterface 24 | { 25 | /** 26 | * @param ContainerBuilder $container 27 | * 28 | * @throws \Exception 29 | */ 30 | public function process(ContainerBuilder $container) 31 | { 32 | if (!$container->hasParameter('cache.logging')) { 33 | return; 34 | } 35 | 36 | $config = $container->getParameter('cache.logging'); 37 | if (!$config['enabled']) { 38 | return; 39 | } 40 | 41 | $serviceIds = $container->findTaggedServiceIds('cache.provider'); 42 | 43 | foreach (array_keys($serviceIds) as $id) { 44 | $poolDefinition = $container->getDefinition($id); 45 | if (!method_exists($poolDefinition->getClass(), 'setLogger')) { 46 | continue; 47 | } 48 | $poolDefinition->addMethodCall('setLogger', [new Reference($config['logger'])]); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/DependencyInjection/Compiler/SessionSupportCompilerPass.php: -------------------------------------------------------------------------------- 1 | , Tobias Nyholm 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Cache\CacheBundle\DependencyInjection\Compiler; 13 | 14 | use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; 15 | use Symfony\Component\DependencyInjection\ContainerBuilder; 16 | 17 | /** 18 | * Enable the session support by rewriting the "session.handler" alias. 19 | * 20 | * @author Aaron Scherer 21 | */ 22 | class SessionSupportCompilerPass implements CompilerPassInterface 23 | { 24 | /** 25 | * @param ContainerBuilder $container 26 | * 27 | * @throws \Exception 28 | */ 29 | public function process(ContainerBuilder $container) 30 | { 31 | // Check if session support is enabled 32 | if (!$container->hasParameter('cache.session')) { 33 | return; 34 | } 35 | 36 | // If there is no active session support, throw 37 | if (!$container->hasAlias('session.storage')) { 38 | throw new \Exception('Session cache support cannot be enabled if there is no session.storage service'); 39 | } 40 | 41 | $container->setAlias('session.handler', 'cache.service.session'); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | , Tobias Nyholm 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Cache\CacheBundle\DependencyInjection; 13 | 14 | use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; 15 | use Symfony\Component\Config\Definition\Builder\NodeDefinition; 16 | use Symfony\Component\Config\Definition\Builder\TreeBuilder; 17 | use Symfony\Component\Config\Definition\ConfigurationInterface; 18 | 19 | /** 20 | * Class Configuration. 21 | * 22 | * @author Aaron Scherer 23 | */ 24 | class Configuration implements ConfigurationInterface 25 | { 26 | /** 27 | * Generates the configuration tree builder. 28 | * 29 | * @return TreeBuilder The tree builder 30 | */ 31 | public function getConfigTreeBuilder() 32 | { 33 | $treeBuilder = new TreeBuilder(); 34 | $rootNode = $treeBuilder->root('cache'); 35 | 36 | $rootNode->children() 37 | ->append($this->addSessionSupportSection()) 38 | ->append($this->addDoctrineSection()) 39 | ->append($this->addRouterSection()) 40 | ->append($this->addAnnotationSection()) 41 | ->append($this->addSerializerSection()) 42 | ->append($this->addValidationSection()) 43 | ->append($this->addLoggingSection()) 44 | ->append($this->addDataCollectorSection()) 45 | ->end(); 46 | 47 | return $treeBuilder; 48 | } 49 | 50 | /** 51 | * Normalizes the enabled field to be truthy. 52 | * 53 | * @param NodeDefinition $node 54 | * 55 | * @return Configuration 56 | */ 57 | private function normalizeEnabled(NodeDefinition $node) 58 | { 59 | $node->beforeNormalization() 60 | ->always() 61 | ->then( 62 | function ($v) { 63 | if (is_string($v['enabled'])) { 64 | $v['enabled'] = $v['enabled'] === 'true'; 65 | } 66 | if (is_int($v['enabled'])) { 67 | $v['enabled'] = $v['enabled'] === 1; 68 | } 69 | 70 | return $v; 71 | } 72 | ) 73 | ->end(); 74 | 75 | return $this; 76 | } 77 | 78 | /** 79 | * Configure the "cache.session" section. 80 | * 81 | * @return ArrayNodeDefinition 82 | */ 83 | private function addSessionSupportSection() 84 | { 85 | $tree = new TreeBuilder(); 86 | $node = $tree->root('session'); 87 | 88 | $node 89 | ->canBeEnabled() 90 | ->addDefaultsIfNotSet() 91 | ->children() 92 | ->scalarNode('service_id')->isRequired()->end() 93 | ->booleanNode('use_tagging')->defaultTrue()->end() 94 | ->scalarNode('prefix')->defaultValue('session_')->end() 95 | ->scalarNode('ttl')->end() 96 | ->end(); 97 | 98 | $this->normalizeEnabled($node); 99 | 100 | return $node; 101 | } 102 | 103 | /** 104 | * Configure the "cache.serializer" section. 105 | * 106 | * @return ArrayNodeDefinition 107 | */ 108 | private function addSerializerSection() 109 | { 110 | $tree = new TreeBuilder(); 111 | $node = $tree->root('serializer'); 112 | 113 | $node 114 | ->canBeEnabled() 115 | ->addDefaultsIfNotSet() 116 | ->children() 117 | ->scalarNode('service_id')->isRequired()->end() 118 | ->booleanNode('use_tagging')->defaultTrue()->end() 119 | ->scalarNode('prefix')->defaultValue('')->end() 120 | ->end(); 121 | 122 | $this->normalizeEnabled($node); 123 | 124 | return $node; 125 | } 126 | 127 | /** 128 | * Configure the "cache.serializer" section. 129 | * 130 | * @return ArrayNodeDefinition 131 | */ 132 | private function addValidationSection() 133 | { 134 | $tree = new TreeBuilder(); 135 | $node = $tree->root('validation'); 136 | 137 | $node 138 | ->canBeEnabled() 139 | ->addDefaultsIfNotSet() 140 | ->children() 141 | ->scalarNode('service_id')->isRequired()->end() 142 | ->booleanNode('use_tagging')->defaultTrue()->end() 143 | ->scalarNode('prefix')->defaultValue('')->end() 144 | ->end(); 145 | 146 | $this->normalizeEnabled($node); 147 | 148 | return $node; 149 | } 150 | 151 | /** 152 | * Configure the "cache.annotation" section. 153 | * 154 | * @return ArrayNodeDefinition 155 | */ 156 | private function addAnnotationSection() 157 | { 158 | $tree = new TreeBuilder(); 159 | $node = $tree->root('annotation'); 160 | 161 | $node 162 | ->canBeEnabled() 163 | ->addDefaultsIfNotSet() 164 | ->children() 165 | ->scalarNode('service_id')->isRequired()->end() 166 | ->booleanNode('use_tagging')->defaultTrue()->end() 167 | ->scalarNode('prefix')->defaultValue('')->end() 168 | ->end(); 169 | 170 | $this->normalizeEnabled($node); 171 | 172 | return $node; 173 | } 174 | 175 | /** 176 | * @return ArrayNodeDefinition 177 | */ 178 | private function addLoggingSection() 179 | { 180 | $tree = new TreeBuilder(); 181 | $node = $tree->root('logging'); 182 | 183 | $node 184 | ->canBeEnabled() 185 | ->addDefaultsIfNotSet() 186 | ->children() 187 | ->scalarNode('logger')->defaultValue('logger')->end() 188 | ->scalarNode('level')->defaultValue('info')->end() 189 | ->end(); 190 | 191 | $this->normalizeEnabled($node); 192 | 193 | return $node; 194 | } 195 | 196 | /** 197 | * Configure the "cache.doctrine" section. 198 | * 199 | * @return ArrayNodeDefinition 200 | */ 201 | private function addDoctrineSection() 202 | { 203 | $tree = new TreeBuilder(); 204 | $node = $tree->root('doctrine'); 205 | 206 | $node 207 | ->canBeEnabled() 208 | ->addDefaultsIfNotSet() 209 | ->children() 210 | ->booleanNode('use_tagging') 211 | ->defaultTrue() 212 | ->end() 213 | ->end(); 214 | 215 | $this->normalizeEnabled($node); 216 | 217 | $types = ['metadata', 'result', 'query']; 218 | foreach ($types as $type) { 219 | $node->children() 220 | ->arrayNode($type) 221 | ->canBeUnset() 222 | ->children() 223 | ->scalarNode('service_id')->isRequired()->end() 224 | ->arrayNode('entity_managers') 225 | ->defaultValue([]) 226 | ->beforeNormalization() 227 | ->ifString() 228 | ->then( 229 | function ($v) { 230 | return (array) $v; 231 | } 232 | ) 233 | ->end() 234 | ->prototype('scalar')->end() 235 | ->end() 236 | ->arrayNode('document_managers') 237 | ->defaultValue([]) 238 | ->beforeNormalization() 239 | ->ifString() 240 | ->then( 241 | function ($v) { 242 | return (array) $v; 243 | } 244 | ) 245 | ->end() 246 | ->prototype('scalar')->end() 247 | ->end() 248 | ->end() 249 | ->end(); 250 | } 251 | 252 | return $node; 253 | } 254 | 255 | /** 256 | * Configure the "cache.router" section. 257 | * 258 | * @return ArrayNodeDefinition 259 | */ 260 | private function addRouterSection() 261 | { 262 | $tree = new TreeBuilder(); 263 | $node = $tree->root('router'); 264 | 265 | $node 266 | ->canBeEnabled() 267 | ->addDefaultsIfNotSet() 268 | ->children() 269 | ->integerNode('ttl') 270 | ->defaultValue(604800) 271 | ->end() 272 | ->scalarNode('service_id') 273 | ->isRequired() 274 | ->end() 275 | ->booleanNode('use_tagging')->defaultTrue()->end() 276 | ->scalarNode('prefix')->defaultValue('')->end() 277 | ->end(); 278 | 279 | $this->normalizeEnabled($node); 280 | 281 | return $node; 282 | } 283 | 284 | /** 285 | * @return ArrayNodeDefinition 286 | */ 287 | private function addDataCollectorSection() 288 | { 289 | $tree = new TreeBuilder(); 290 | $node = $tree->root('data_collector'); 291 | 292 | $node 293 | ->canBeEnabled() 294 | ->addDefaultsIfNotSet() 295 | ->children() 296 | ->booleanNode('enabled')->end() 297 | ->end(); 298 | 299 | $this->normalizeEnabled($node); 300 | 301 | return $node; 302 | } 303 | } 304 | -------------------------------------------------------------------------------- /src/Factory/DoctrineBridgeFactory.php: -------------------------------------------------------------------------------- 1 | , Tobias Nyholm 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Cache\CacheBundle\Factory; 13 | 14 | use Cache\Bridge\Doctrine\DoctrineCacheBridge; 15 | use Cache\CacheBundle\Cache\FixedTaggingCachePool; 16 | use Cache\Prefixed\PrefixedCachePool; 17 | use Cache\Taggable\TaggablePSR6PoolAdapter; 18 | use Psr\Cache\CacheItemPoolInterface; 19 | 20 | /** 21 | * @author Tobias Nyholm 22 | */ 23 | class DoctrineBridgeFactory 24 | { 25 | /** 26 | * @param CacheItemPoolInterface $pool 27 | * @param array $config 28 | * @param array $tags 29 | * 30 | * @return DoctrineCacheBridge 31 | */ 32 | public static function get(CacheItemPoolInterface $pool, array $config, array $tags) 33 | { 34 | if ($config['use_tagging']) { 35 | $pool = new FixedTaggingCachePool(TaggablePSR6PoolAdapter::makeTaggable($pool), $tags); 36 | } 37 | 38 | if (!empty($config['prefix'])) { 39 | $pool = new PrefixedCachePool($pool, $config['prefix']); 40 | } 41 | 42 | return new DoctrineCacheBridge($pool); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Factory/RouterFactory.php: -------------------------------------------------------------------------------- 1 | , Tobias Nyholm 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Cache\CacheBundle\Factory; 13 | 14 | use Cache\CacheBundle\Routing\CachingRouter; 15 | use Cache\Prefixed\PrefixedCachePool; 16 | use Cache\Taggable\TaggablePSR6PoolAdapter; 17 | use Psr\Cache\CacheItemPoolInterface; 18 | use Symfony\Component\Routing\RouterInterface; 19 | 20 | /** 21 | * @author Tobias Nyholm 22 | */ 23 | class RouterFactory 24 | { 25 | /** 26 | * @param CacheItemPoolInterface $pool 27 | * @param RouterInterface $router 28 | * @param array $config 29 | * 30 | * @return CachingRouter 31 | */ 32 | public static function get(CacheItemPoolInterface $pool, RouterInterface $router, array $config) 33 | { 34 | if ($config['use_tagging']) { 35 | $pool = TaggablePSR6PoolAdapter::makeTaggable($pool); 36 | } 37 | 38 | if (!empty($config['prefix'])) { 39 | $pool = new PrefixedCachePool($pool, $config['prefix']); 40 | } 41 | 42 | return new CachingRouter($pool, $router, $config); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Factory/SessionHandlerFactory.php: -------------------------------------------------------------------------------- 1 | , Tobias Nyholm 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Cache\CacheBundle\Factory; 13 | 14 | use Cache\CacheBundle\Cache\FixedTaggingCachePool; 15 | use Cache\SessionHandler\Psr6SessionHandler; 16 | use Cache\Taggable\TaggablePSR6PoolAdapter; 17 | use Psr\Cache\CacheItemPoolInterface; 18 | 19 | /** 20 | * @author Tobias Nyholm 21 | */ 22 | class SessionHandlerFactory 23 | { 24 | /** 25 | * @param CacheItemPoolInterface $pool 26 | * @param array $config 27 | * 28 | * @return Psr6SessionHandler 29 | */ 30 | public static function get(CacheItemPoolInterface $pool, array $config) 31 | { 32 | if ($config['use_tagging']) { 33 | $pool = new FixedTaggingCachePool(TaggablePSR6PoolAdapter::makeTaggable($pool), ['session']); 34 | } 35 | 36 | return new Psr6SessionHandler($pool, $config); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Factory/ValidationFactory.php: -------------------------------------------------------------------------------- 1 | , Tobias Nyholm 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Cache\CacheBundle\Factory; 13 | 14 | use Cache\CacheBundle\Bridge\SymfonyValidatorBridge; 15 | use Cache\CacheBundle\Cache\FixedTaggingCachePool; 16 | use Cache\Prefixed\PrefixedCachePool; 17 | use Cache\Taggable\TaggablePSR6PoolAdapter; 18 | use Psr\Cache\CacheItemPoolInterface; 19 | 20 | /** 21 | * @author Tobias Nyholm 22 | */ 23 | class ValidationFactory 24 | { 25 | /** 26 | * @param CacheItemPoolInterface $pool 27 | * @param array $config 28 | * 29 | * @return SymfonyValidatorBridge 30 | */ 31 | public static function get(CacheItemPoolInterface $pool, array $config) 32 | { 33 | if ($config['use_tagging']) { 34 | $pool = new FixedTaggingCachePool(TaggablePSR6PoolAdapter::makeTaggable($pool), ['validation']); 35 | } 36 | 37 | if (!empty($config['prefix'])) { 38 | $pool = new PrefixedCachePool($pool, $config['prefix']); 39 | } 40 | 41 | return new SymfonyValidatorBridge($pool); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/KeyNormalizer.php: -------------------------------------------------------------------------------- 1 | , Tobias Nyholm 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Cache\CacheBundle; 13 | 14 | /** 15 | * A class to normalize cache keys. 16 | * 17 | * @author Tobias Nyholm 18 | */ 19 | class KeyNormalizer 20 | { 21 | /** 22 | * Remove all characters that is not supported by PSR6. 23 | * 24 | * @param $key 25 | */ 26 | public static function onlyValid($key) 27 | { 28 | return preg_replace('|[^A-Za-z0-9_\.]|', '', $key); 29 | } 30 | 31 | /** 32 | * Remove all characters that are marked as reserved in PSR6. 33 | * 34 | * @param string $key 35 | */ 36 | public static function noInvalid($key) 37 | { 38 | return preg_replace('|[\{\}\(\)/\\\@\:]|', '', $key); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Resources/config/data-collector.yml: -------------------------------------------------------------------------------- 1 | services: 2 | cache.data_collector: 3 | class: Cache\CacheBundle\DataCollector\CacheDataCollector 4 | tags: 5 | - { name: data_collector, template: 'CacheBundle:Collector:cache.html.twig', id: 'php-cache' } 6 | 7 | cache.proxy_factory: 8 | class: Cache\CacheBundle\DataCollector\ProxyFactory 9 | arguments: ['%kernel.cache_dir%/phpcache/proxy'] 10 | 11 | cache.decorating_factory: 12 | class: Cache\CacheBundle\DataCollector\DecoratingFactory 13 | arguments: ["@cache.proxy_factory"] 14 | public: false 15 | -------------------------------------------------------------------------------- /src/Resources/proxy/template.php: -------------------------------------------------------------------------------- 1 | , Tobias Nyholm 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | use Cache\CacheBundle\DataCollector\CacheProxyInterface; 13 | use Cache\CacheBundle\DataCollector\TraceableAdapterEvent; 14 | use Psr\Cache\CacheItemInterface; 15 | 16 | class __TPL_CLASS__ extends __TPL_EXTENDS__ implements CacheProxyInterface 17 | { 18 | private $__name; 19 | private $__calls = []; 20 | 21 | public function getItem($key) 22 | { 23 | $event = $this->start(__FUNCTION__, $key); 24 | 25 | try { 26 | $item = parent::getItem($key); 27 | } finally { 28 | $event->end = microtime(true); 29 | } 30 | if ($item->isHit()) { 31 | $event->hits++; 32 | } else { 33 | $event->misses++; 34 | } 35 | $event->result = $item->get(); 36 | 37 | return $item; 38 | } 39 | 40 | public function hasItem($key) 41 | { 42 | $event = $this->start(__FUNCTION__, $key); 43 | 44 | try { 45 | $event->result = parent::hasItem($key); 46 | } finally { 47 | $event->end = microtime(true); 48 | } 49 | 50 | if (!$event->result) { 51 | $event->misses++; 52 | } 53 | 54 | return $event->result; 55 | } 56 | 57 | public function deleteItem($key) 58 | { 59 | $event = $this->start(__FUNCTION__, $key); 60 | 61 | try { 62 | return $event->result = parent::deleteItem($key); 63 | } finally { 64 | $event->end = microtime(true); 65 | } 66 | } 67 | 68 | public function save(CacheItemInterface $item) 69 | { 70 | $event = $this->start(__FUNCTION__, $item); 71 | 72 | try { 73 | return $event->result = parent::save($item); 74 | } finally { 75 | $event->end = microtime(true); 76 | } 77 | } 78 | 79 | public function saveDeferred(CacheItemInterface $item) 80 | { 81 | $event = $this->start(__FUNCTION__, $item); 82 | 83 | try { 84 | return $event->result = parent::saveDeferred($item); 85 | } finally { 86 | $event->end = microtime(true); 87 | } 88 | } 89 | 90 | public function getItems(array $keys = []) 91 | { 92 | $event = $this->start(__FUNCTION__, $keys); 93 | 94 | try { 95 | $result = parent::getItems($keys); 96 | } finally { 97 | $event->end = microtime(true); 98 | } 99 | $f = function () use ($result, $event) { 100 | $event->result = []; 101 | foreach ($result as $key => $item) { 102 | if ($item->isHit()) { 103 | $event->hits++; 104 | } else { 105 | $event->misses++; 106 | } 107 | $event->result[$key] = $item->get(); 108 | yield $key => $item; 109 | } 110 | }; 111 | 112 | return $f(); 113 | } 114 | 115 | public function clear() 116 | { 117 | $event = $this->start(__FUNCTION__); 118 | 119 | try { 120 | return $event->result = parent::clear(); 121 | } finally { 122 | $event->end = microtime(true); 123 | } 124 | } 125 | 126 | public function deleteItems(array $keys) 127 | { 128 | $event = $this->start(__FUNCTION__, $keys); 129 | 130 | try { 131 | return $event->result = parent::deleteItems($keys); 132 | } finally { 133 | $event->end = microtime(true); 134 | } 135 | } 136 | 137 | public function commit() 138 | { 139 | $event = $this->start(__FUNCTION__); 140 | 141 | try { 142 | return $event->result = parent::commit(); 143 | } finally { 144 | $event->end = microtime(true); 145 | } 146 | } 147 | 148 | public function invalidateTag($tag) 149 | { 150 | $event = $this->start(__FUNCTION__, $tag); 151 | 152 | try { 153 | return $event->result = parent::invalidateTag($tag); 154 | } finally { 155 | $event->end = microtime(true); 156 | } 157 | } 158 | 159 | public function invalidateTags(array $tags) 160 | { 161 | $event = $this->start(__FUNCTION__, $tags); 162 | 163 | try { 164 | return $event->result = parent::invalidateTags($tags); 165 | } finally { 166 | $event->end = microtime(true); 167 | } 168 | } 169 | 170 | public function __getCalls() 171 | { 172 | return $this->__calls; 173 | } 174 | 175 | public function __setName($name) 176 | { 177 | $this->__name = $name; 178 | } 179 | 180 | private function start($name, $argument = null) 181 | { 182 | $this->__calls[] = $event = new TraceableAdapterEvent(); 183 | $event->name = $name; 184 | $event->argument = $argument; 185 | $event->start = microtime(true); 186 | 187 | return $event; 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/Resources/views/Collector/cache.html.twig: -------------------------------------------------------------------------------- 1 | {# 2 | # This file is part of php-cache\cache-bundle package. 3 | # 4 | # (c) 2015-2015 Aaron Scherer 5 | # 6 | # This source file is subject to the MIT license that is bundled 7 | # with this source code in the file LICENSE. 8 | #} 9 | 10 | {% extends '@WebProfiler/Profiler/layout.html.twig' %} 11 | 12 | {% block toolbar %} 13 | {% if collector.totals.calls > 0 %} 14 | {% set icon %} 15 | {{ include('@Cache/Icon/logo.svg') }} 16 | {{ collector.totals.calls }} 17 | 18 | in 19 | {{ '%0.2f'|format(collector.totals.time * 1000) }} 20 | ms 21 | 22 | {% endset %} 23 | {% set text %} 24 |
25 | Cache Calls 26 | {{ collector.totals.calls }} 27 |
28 |
29 | Total time 30 | {{ '%0.2f'|format(collector.totals.time * 1000) }} ms 31 |
32 |
33 | Cache hits 34 | {{ collector.totals.hits }}/{{ collector.totals.reads }}{% if collector.totals.hit_read_ratio is not null %} ({{ collector.totals.hit_read_ratio }}%){% endif %} 35 |
36 |
37 | Cache writes 38 | {{ collector.totals.writes }} 39 |
40 | {% endset %} 41 | {% include 'WebProfilerBundle:Profiler:toolbar_item.html.twig' with { 'link': profiler_url } %} 42 | {% endif %} 43 | {% endblock %} 44 | 45 | {% block menu %} 46 | 47 | 48 | {{ include('@Cache/Icon/logo.svg') }} 49 | 50 | PHPCache 51 | 52 | {{ collector.totals.calls }} 53 | {{ '%0.2f'|format(collector.totals.time * 1000) }} ms 54 | 55 | 56 | {% endblock %} 57 | 58 | {% block panel %} 59 |

PHPCache

60 |
61 |
62 | {{ collector.totals.calls }} 63 | Total calls 64 |
65 |
66 | {{ '%0.2f'|format(collector.totals.time * 1000) }} ms 67 | Total time 68 |
69 |
70 |
71 | {{ collector.totals.reads }} 72 | Total reads 73 |
74 |
75 | {{ collector.totals.writes }} 76 | Total writes 77 |
78 |
79 | {{ collector.totals.deletes }} 80 | Total deletes 81 |
82 |
83 |
84 | {{ collector.totals.hits }} 85 | Total hits 86 |
87 |
88 | {{ collector.totals.misses }} 89 | Total misses 90 |
91 |
92 | 93 | {% if collector.totals.hit_read_ratio is null %} 94 | n/a 95 | {% else %} 96 | {{ collector.totals.hit_read_ratio }} % 97 | {% endif %} 98 | 99 | Hits/reads 100 |
101 |
102 | 103 |

Pools

104 |
105 | {% for name, calls in collector.calls %} 106 |
107 |

{{ name }} {{ collector.statistics[name].calls }}

108 | 109 |
110 |

Statistics

111 |
112 | {% for key, value in collector.statistics[name] %} 113 |
114 | 115 | {% if key == 'time' %} 116 | {{ '%0.2f'|format(1000 * value) }} ms 117 | {% elseif key == 'hit_read_ratio' %} 118 | {% if value is null %} 119 | n/a 120 | {% else %} 121 | {{ value }} % 122 | {% endif %} 123 | {% else %} 124 | {{ value }} 125 | {% endif %} 126 | 127 | {{ key == 'hit_read_ratio' ? 'Hits/reads' : key|capitalize }} 128 |
129 | {% if key == 'time' or key == 'deletes' %} 130 |
131 | {% endif %} 132 | {% endfor %} 133 |
134 | 135 |

Calls

136 | {% if calls|length == 0 %} 137 |
138 |

No calls

139 |
140 | {% else %} 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | {% for call in calls %} 151 | {% set separatorStyle = not loop.first ? ' style="border-top-width: medium;"' : '' %} 152 | 153 | 154 | {{ call.name }} 155 | {{ dump(call.argument) }} 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | {% endfor %} 166 | 167 |
#KeyValue
{{ loop.index }}
Result{{ dump(call.result) }}
Time{{ '%0.2f'|format((call.end - call.start) * 1000) }} ms
168 | {% endif %} 169 |
170 |
171 | {% endfor %} 172 |
173 | 174 | {% endblock %} 175 | -------------------------------------------------------------------------------- /src/Resources/views/Icon/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 8 | 9 | -------------------------------------------------------------------------------- /src/Routing/CachingRouter.php: -------------------------------------------------------------------------------- 1 | , Tobias Nyholm 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Cache\CacheBundle\Routing; 13 | 14 | use Cache\CacheBundle\KeyNormalizer; 15 | use Cache\TagInterop\TaggableCacheItemInterface; 16 | use Psr\Cache\CacheItemPoolInterface; 17 | use Symfony\Component\Routing\RequestContext; 18 | use Symfony\Component\Routing\RouterInterface; 19 | 20 | /** 21 | * @author Tobias Nyholm 22 | */ 23 | class CachingRouter implements RouterInterface 24 | { 25 | /** 26 | * @type CacheItemPoolInterface 27 | */ 28 | private $cache; 29 | 30 | /** 31 | * @type int 32 | */ 33 | private $ttl; 34 | 35 | /** 36 | * @type RouterInterface 37 | */ 38 | private $router; 39 | 40 | /** 41 | * @param CacheItemPoolInterface $cache 42 | * @param RouterInterface $router 43 | * @param array $config 44 | */ 45 | public function __construct(CacheItemPoolInterface $cache, RouterInterface $router, array $config) 46 | { 47 | $this->cache = $cache; 48 | $this->ttl = $config['ttl']; 49 | $this->router = $router; 50 | } 51 | 52 | /** 53 | * {@inheritdoc} 54 | */ 55 | public function match($pathinfo) 56 | { 57 | $cacheItem = $this->getCacheItemMatch($pathinfo); 58 | if ($cacheItem->isHit()) { 59 | return $cacheItem->get(); 60 | } 61 | 62 | // Get the result form the router 63 | $result = $this->router->match($pathinfo); 64 | 65 | // Save the result 66 | $cacheItem->set($result) 67 | ->expiresAfter($this->ttl); 68 | $this->cache->save($cacheItem); 69 | 70 | return $result; 71 | } 72 | 73 | /** 74 | * {@inheritdoc} 75 | */ 76 | public function generate($name, $parameters = [], $referenceType = self::ABSOLUTE_PATH) 77 | { 78 | $cacheItem = $this->getCacheItemGenerate($name, $parameters, $referenceType); 79 | if ($cacheItem->isHit()) { 80 | return $cacheItem->get(); 81 | } 82 | 83 | // Get the result form the router 84 | $result = $this->router->generate($name, $parameters, $referenceType); 85 | 86 | // Save the result 87 | $cacheItem->set($result) 88 | ->expiresAfter($this->ttl); 89 | $this->cache->save($cacheItem); 90 | 91 | return $result; 92 | } 93 | 94 | /** 95 | * {@inheritdoc} 96 | */ 97 | public function getRouteCollection() 98 | { 99 | return $this->router->getRouteCollection(); 100 | } 101 | 102 | /** 103 | * {@inheritdoc} 104 | */ 105 | public function setContext(RequestContext $context) 106 | { 107 | $this->router->setContext($context); 108 | } 109 | 110 | /** 111 | * {@inheritdoc} 112 | */ 113 | public function getContext() 114 | { 115 | return $this->router->getContext(); 116 | } 117 | 118 | /** 119 | * Get a cache item for a call to match(). 120 | * 121 | * @param string $pathinfo 122 | * 123 | * @return \Psr\Cache\CacheItemInterface 124 | */ 125 | private function getCacheItemMatch($pathinfo) 126 | { 127 | /** @type RequestContext $c */ 128 | $c = $this->getContext(); 129 | $key = sprintf('%s__%s__%s__%s', $c->getHost(), $pathinfo, $c->getMethod(), $c->getQueryString()); 130 | 131 | return $this->getCacheItemFromKey($key, 'match'); 132 | } 133 | 134 | /** 135 | * Get a cache item for a call to generate(). 136 | * 137 | * @param $name 138 | * @param array $parameters 139 | * @param $referenceType 140 | * 141 | * @return \Psr\Cache\CacheItemInterface 142 | */ 143 | private function getCacheItemGenerate($name, array $parameters, $referenceType) 144 | { 145 | asort($parameters); 146 | $key = sprintf('%s.%s.%s', $name, $referenceType ? 'true' : 'false', http_build_query($parameters)); 147 | 148 | return $this->getCacheItemFromKey($key, 'generate'); 149 | } 150 | 151 | /** 152 | * Passes through all unknown calls onto the router object. 153 | */ 154 | public function __call($method, $args) 155 | { 156 | return call_user_func_array([$this->router, $method], $args); 157 | } 158 | 159 | /** 160 | * @param string $key 161 | * @param string $tag 162 | * 163 | * @return \Psr\Cache\CacheItemInterface 164 | */ 165 | private function getCacheItemFromKey($key, $tag) 166 | { 167 | $item = $this->cache->getItem(KeyNormalizer::noInvalid($key)); 168 | 169 | if ($item instanceof TaggableCacheItemInterface) { 170 | $item->setTags(['router', $tag]); 171 | } 172 | 173 | return $item; 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /tests/Functional/BundleInitializationTest.php: -------------------------------------------------------------------------------- 1 | , Tobias Nyholm 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Cache\CacheBundle\Tests\Functional; 13 | 14 | use Cache\Bridge\Doctrine\DoctrineCacheBridge; 15 | use Cache\CacheBundle\Bridge\SymfonyValidatorBridge; 16 | use Cache\CacheBundle\CacheBundle; 17 | use Cache\CacheBundle\Routing\CachingRouter; 18 | use Cache\SessionHandler\Psr6SessionHandler; 19 | use Nyholm\BundleTest\BaseBundleTestCase; 20 | use Symfony\Component\HttpKernel\Kernel; 21 | 22 | /** 23 | * @author Tobias Nyholm 24 | */ 25 | class BundleInitializationTest extends BaseBundleTestCase 26 | { 27 | protected function getBundleClass() 28 | { 29 | return CacheBundle::class; 30 | } 31 | 32 | protected function setUp() 33 | { 34 | parent::setUp(); 35 | $kernel = $this->createKernel(); 36 | $kernel->addConfigFile(__DIR__.'/config.yml'); 37 | 38 | if (Kernel::MAJOR_VERSION < 4) { 39 | $kernel->addConfigFile(__DIR__.'/sf2_and_3.yml'); 40 | } 41 | } 42 | 43 | public function testInitBundle() 44 | { 45 | $this->bootKernel(); 46 | $container = $this->getContainer(); 47 | 48 | $this->assertTrue($container->hasParameter('cache.provider_service_ids')); 49 | 50 | if (Kernel::MAJOR_VERSION < 4) { 51 | $this->assertInstanceOf(DoctrineCacheBridge::class, $container->get('cache.service.annotation')); 52 | $this->assertInstanceOf(DoctrineCacheBridge::class, $container->get('cache.service.serializer')); 53 | $this->assertInstanceOf(SymfonyValidatorBridge::class, $container->get('cache.service.validation')); 54 | $this->assertInstanceOf(Psr6SessionHandler::class, $container->get('cache.service.session')); 55 | $this->assertInstanceOf(CachingRouter::class, $container->get('cache.service.router')); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/Functional/DummyNormalizer.php: -------------------------------------------------------------------------------- 1 | , Tobias Nyholm 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Cache\CacheBundle\Tests\Functional; 13 | 14 | use Symfony\Component\Serializer\Normalizer\NormalizerInterface; 15 | 16 | class DummyNormalizer implements NormalizerInterface 17 | { 18 | public function normalize($object, $format = null, array $context = []) 19 | { 20 | return []; 21 | } 22 | 23 | public function supportsNormalization($data, $format = null) 24 | { 25 | return false; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/Functional/config.yml: -------------------------------------------------------------------------------- 1 | services: 2 | array_cache: 3 | class: Cache\Adapter\PHPArray\ArrayCachePool 4 | 5 | dummy_normalizer: 6 | class: Cache\CacheBundle\Tests\Functional\DummyNormalizer 7 | tags: 8 | - { name: serializer.normalizer } 9 | 10 | cache: 11 | session: 12 | enabled: true 13 | service_id: 'array_cache' 14 | use_tagging: true 15 | ttl: 7200 16 | 17 | router: 18 | enabled: true 19 | service_id: 'array_cache' 20 | ttl: 86400 21 | 22 | annotation: 23 | enabled: true 24 | service_id: 'array_cache' 25 | use_tagging: true 26 | 27 | serializer: 28 | enabled: true 29 | service_id: 'array_cache' 30 | use_tagging: true 31 | 32 | validation: 33 | enabled: true 34 | service_id: 'array_cache' 35 | use_tagging: true 36 | 37 | -------------------------------------------------------------------------------- /tests/Functional/sf2_and_3.yml: -------------------------------------------------------------------------------- 1 | framework: 2 | annotations: 3 | cache: 'cache.service.annotation' 4 | serializer: 5 | cache: 'cache.service.serializer' 6 | validation: 7 | cache: 'cache.service.validation' 8 | -------------------------------------------------------------------------------- /tests/Unit/ContainerTest.php: -------------------------------------------------------------------------------- 1 | , Tobias Nyholm 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Cache\CacheBundle\Tests\Unit; 13 | 14 | /** 15 | * Class ContainerTest. 16 | * 17 | * @author Aaron Scherer 18 | */ 19 | class ContainerTest extends TestCase 20 | { 21 | public function testContainer() 22 | { 23 | //$container = $this->createContainer(); 24 | 25 | // @TODO Create Tests..... 26 | $this->assertTrue(true); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/Unit/DependencyInjection/CacheExtensionTest.php: -------------------------------------------------------------------------------- 1 | , Tobias Nyholm 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Cache\CacheBundle\Tests\Unit\DependencyInjection; 13 | 14 | use Cache\CacheBundle\Tests\Unit\TestCase; 15 | 16 | /** 17 | * Class CacheExtensionTest. 18 | * 19 | * @author Aaron Scherer 20 | */ 21 | class CacheExtensionTest extends TestCase 22 | { 23 | public function testRouterBuilder() 24 | { 25 | $container = $this->createContainerFromFile('router'); 26 | 27 | $config = $container->getParameter('cache.router'); 28 | 29 | $this->assertTrue(isset($config['enabled'])); 30 | 31 | $this->assertTrue($config['enabled']); 32 | $this->assertEquals($config['service_id'], 'default'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/Unit/Fixtures/router.yml: -------------------------------------------------------------------------------- 1 | cache: 2 | router: 3 | enabled: true 4 | service_id: default -------------------------------------------------------------------------------- /tests/Unit/KeyNormalizerTest.php: -------------------------------------------------------------------------------- 1 | , Tobias Nyholm 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Cache\CacheBundle\Tests\Unit; 13 | 14 | use Cache\CacheBundle\KeyNormalizer; 15 | 16 | class KeyNormalizerTest extends TestCase 17 | { 18 | public function testOnlyValid() 19 | { 20 | $input = '%foo!bar-'; 21 | $expected = 'foobar'; 22 | $this->assertEquals($expected, KeyNormalizer::onlyValid($input)); 23 | } 24 | 25 | public function testNoInvalid() 26 | { 27 | $input = '{foo@bar}'; 28 | $expected = 'foobar'; 29 | $this->assertEquals($expected, KeyNormalizer::noInvalid($input)); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/Unit/TestCase.php: -------------------------------------------------------------------------------- 1 | , Tobias Nyholm 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Cache\CacheBundle\Tests\Unit; 13 | 14 | use Cache\CacheBundle\DependencyInjection\CacheExtension; 15 | use PHPUnit\Framework\TestCase as BaseTestCase; 16 | use Symfony\Component\Config\FileLocator; 17 | use Symfony\Component\DependencyInjection\ContainerBuilder; 18 | use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; 19 | use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; 20 | 21 | /** 22 | * Class TestCase. 23 | * 24 | * @author Aaron Scherer 25 | */ 26 | class TestCase extends BaseTestCase 27 | { 28 | /** 29 | * @param ContainerBuilder $container 30 | * @param string $file 31 | */ 32 | protected function loadFromFile(ContainerBuilder $container, $file) 33 | { 34 | $loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/Fixtures')); 35 | $loader->load($file.'.yml'); 36 | } 37 | 38 | /** 39 | * @param array $data 40 | * 41 | * @return ContainerBuilder 42 | */ 43 | protected function createContainer(array $data = []) 44 | { 45 | return new ContainerBuilder(new ParameterBag(array_merge( 46 | [ 47 | 'kernel.bundles' => ['FrameworkBundle' => 'Symfony\\Bundle\\FrameworkBundle\\FrameworkBundle'], 48 | 'kernel.cache_dir' => __DIR__, 49 | 'kernel.debug' => false, 50 | 'kernel.environment' => 'test', 51 | 'kernel.name' => 'kernel', 52 | 'kernel.root_dir' => __DIR__, 53 | ], 54 | $data 55 | ))); 56 | } 57 | 58 | /** 59 | * @param string $file 60 | * @param array $data 61 | * 62 | * @return ContainerBuilder 63 | */ 64 | protected function createContainerFromFile($file, $data = []) 65 | { 66 | $container = $this->createContainer($data); 67 | $container->registerExtension(new CacheExtension()); 68 | $this->loadFromFile($container, $file); 69 | 70 | $container->getCompilerPassConfig() 71 | ->setOptimizationPasses([]); 72 | $container->getCompilerPassConfig() 73 | ->setRemovingPasses([]); 74 | $container->compile(); 75 | 76 | return $container; 77 | } 78 | } 79 | --------------------------------------------------------------------------------