diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CachePoolPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CachePoolPass.php new file mode 100644 index 0000000000000..395169caaca47 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CachePoolPass.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\DefinitionDecorator; + +/** + * @author Nicolas Grekas + */ +class CachePoolPass implements CompilerPassInterface +{ + /** + * {@inheritdoc} + */ + public function process(ContainerBuilder $container) + { + foreach ($container->findTaggedServiceIds('cache.pool') as $id => $tags) { + $pool = $container->getDefinition($id); + + if (!$pool instanceof DefinitionDecorator) { + throw new \InvalidArgumentException(sprintf('Services tagged with "cache.pool" must have a parent service but "%s" has none.', $id)); + } + + $adapter = $pool; + + do { + $adapterId = $adapter->getParent(); + $adapter = $container->getDefinition($adapterId); + } while ($adapter instanceof DefinitionDecorator && !$adapter->hasTag('cache.adapter')); + + if (!$adapter->hasTag('cache.adapter')) { + throw new \InvalidArgumentException(sprintf('Services tagged with "cache.pool" must have a parent service tagged with "cache.adapter" but "%s" has none.', $id)); + } + + $tags = $adapter->getTag('cache.adapter'); + + if (!isset($tags[0]['namespace_arg_index'])) { + throw new \InvalidArgumentException(sprintf('Invalid "cache.adapter" tag for service "%s": attribute "namespace_arg_index" is missing.', $adapterId)); + } + + if (!$adapter->isAbstract()) { + throw new \InvalidArgumentException(sprintf('Services tagged as "cache.adapter" must be abstract: "%s" is not.', $adapterId)); + } + + if (0 <= $namespaceArgIndex = $tags[0]['namespace_arg_index']) { + $pool->replaceArgument($namespaceArgIndex, $this->getNamespace($id)); + } + } + } + + private function getNamespace($id) + { + return substr(str_replace('/', '-', base64_encode(md5('symfony.'.$id, true))), 0, 10); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index ab55275b20b2c..83981d8c76603 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -114,6 +114,7 @@ public function getConfigTreeBuilder() $this->addSerializerSection($rootNode); $this->addPropertyAccessSection($rootNode); $this->addPropertyInfoSection($rootNode); + $this->addCacheSection($rootNode); return $treeBuilder; } @@ -547,4 +548,33 @@ private function addPropertyInfoSection(ArrayNodeDefinition $rootNode) ->end() ; } + + private function addCacheSection(ArrayNodeDefinition $rootNode) + { + $rootNode + ->children() + ->arrayNode('cache') + ->info('Cache configuration') + ->fixXmlConfig('pool') + ->children() + ->arrayNode('pools') + ->useAttributeAsKey('name') + ->prototype('array') + ->children() + ->enumNode('type') + ->info('The cache pool type (one of "apcu", "doctrine", "psr6" or "filesystem")') + ->isRequired() + ->values(array('apcu', 'doctrine', 'psr6', 'filesystem')) + ->end() + ->integerNode('default_lifetime')->defaultValue(0)->end() + ->scalarNode('cache_provider_service')->defaultNull()->end() + ->scalarNode('directory')->defaultNull()->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ; + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 324915ecb211d..956610f410c85 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -138,6 +138,10 @@ public function load(array $configs, ContainerBuilder $container) $this->registerPropertyInfoConfiguration($config['property_info'], $container, $loader); } + if (isset($config['cache'])) { + $this->registerCacheConfiguration($config['cache'], $container, $loader); + } + $loader->load('debug_prod.xml'); $definition = $container->findDefinition('debug.debug_handlers_listener'); @@ -1017,6 +1021,27 @@ private function registerPropertyInfoConfiguration(array $config, ContainerBuild } } + private function registerCacheConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader) + { + if (!empty($config['pools'])) { + $loader->load('cache_adapters.xml'); + } + + foreach ($config['pools'] as $name => $poolConfig) { + $poolDefinition = new DefinitionDecorator('cache.adapter.'.$poolConfig['type']); + $poolDefinition->replaceArgument(1, $poolConfig['default_lifetime']); + + if ('doctrine' === $poolConfig['type'] || 'psr6' === $poolConfig['type']) { + $poolDefinition->replaceArgument(0, new Reference($poolConfig['cache_provider_service'])); + } elseif ('filesystem' === $poolConfig['type'] && isset($poolConfig['directory'][0])) { + $poolDefinition->replaceArgument(0, $poolConfig['directory']); + } + + $poolDefinition->addTag('cache.pool'); + $container->setDefinition('cache.pool.'.$name, $poolDefinition); + } + } + /** * Gets a hash of the kernel root directory. * diff --git a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php index 94062da0039f5..9c0c91eda6b1f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php +++ b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php @@ -14,6 +14,7 @@ use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddConstraintValidatorsPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddValidatorInitializersPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddConsoleCommandPass; +use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\CachePoolPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\FormPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\PropertyInfoPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TemplatingPass; @@ -87,6 +88,7 @@ public function build(ContainerBuilder $container) $container->addCompilerPass(new FragmentRendererPass(), PassConfig::TYPE_AFTER_REMOVING); $container->addCompilerPass(new SerializerPass()); $container->addCompilerPass(new PropertyInfoPass()); + $container->addCompilerPass(new CachePoolPass()); if ($container->getParameter('kernel.debug')) { $container->addCompilerPass(new UnusedTagsPass(), PassConfig::TYPE_AFTER_REMOVING); diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache_adapters.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache_adapters.xml new file mode 100644 index 0000000000000..9c49c8672de8d --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache_adapters.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + %kernel.cache_dir% + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd index cead2295ed1ac..cee9299e44e58 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd @@ -25,6 +25,7 @@ + @@ -202,4 +203,18 @@ + + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/CachePoolPassTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/CachePoolPassTest.php new file mode 100644 index 0000000000000..f07c04c7e0767 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/CachePoolPassTest.php @@ -0,0 +1,108 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Compiler; + +use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\CachePoolPass; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\DefinitionDecorator; + +class CachePoolPassTest extends \PHPUnit_Framework_TestCase +{ + private $cachePoolPass; + + protected function setUp() + { + $this->cachePoolPass = new CachePoolPass(); + } + + public function testNamespaceArgumentIsReplaced() + { + $container = new ContainerBuilder(); + $adapter = new Definition(); + $adapter->setAbstract(true); + $adapter->addTag('cache.adapter', array('namespace_arg_index' => 0)); + $container->setDefinition('app.cache_adapter', $adapter); + $cachePool = new DefinitionDecorator('app.cache_adapter'); + $cachePool->addArgument(null); + $cachePool->addTag('cache.pool'); + $container->setDefinition('app.cache_pool', $cachePool); + + $this->cachePoolPass->process($container); + + $this->assertSame('yRnzIIVLvL', $cachePool->getArgument(0)); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Services tagged with "cache.pool" must have a parent service but "app.cache_pool" has none. + */ + public function testThrowsExceptionWhenCachePoolHasNoParentDefinition() + { + $container = new ContainerBuilder(); + $cachePool = new Definition(); + $cachePool->addTag('cache.pool'); + $container->setDefinition('app.cache_pool', $cachePool); + + $this->cachePoolPass->process($container); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Services tagged with "cache.pool" must have a parent service tagged with "cache.adapter" but "app.cache_pool" has none. + */ + public function testThrowsExceptionWhenCachePoolIsNotBasedOnAdapter() + { + $container = new ContainerBuilder(); + $container->register('app.cache_adapter'); + $cachePool = new DefinitionDecorator('app.cache_adapter'); + $cachePool->addTag('cache.pool'); + $container->setDefinition('app.cache_pool', $cachePool); + + $this->cachePoolPass->process($container); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Invalid "cache.adapter" tag for service "app.cache_adapter": attribute "namespace_arg_index" is missing. + */ + public function testThrowsExceptionWhenCacheAdapterDefinesNoNamespaceArgument() + { + $container = new ContainerBuilder(); + $adapter = new Definition(); + $adapter->setAbstract(true); + $adapter->addTag('cache.adapter'); + $container->setDefinition('app.cache_adapter', $adapter); + $cachePool = new DefinitionDecorator('app.cache_adapter'); + $cachePool->addTag('cache.pool'); + $container->setDefinition('app.cache_pool', $cachePool); + + $this->cachePoolPass->process($container); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Services tagged as "cache.adapter" must be abstract: "app.cache_adapter" is not. + */ + public function testThrowsExceptionWhenCacheAdapterIsNotAbstract() + { + $container = new ContainerBuilder(); + $adapter = new Definition(); + $adapter->addTag('cache.adapter', array('namespace_arg_index' => 0)); + $container->setDefinition('app.cache_adapter', $adapter); + $cachePool = new DefinitionDecorator('app.cache_adapter'); + $cachePool->addTag('cache.pool'); + $container->setDefinition('app.cache_pool', $cachePool); + + $this->cachePoolPass->process($container); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/cache.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/cache.php new file mode 100644 index 0000000000000..63e5441293f60 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/cache.php @@ -0,0 +1,27 @@ +loadFromExtension('framework', array( + 'cache' => array( + 'pools' => array( + 'foo' => array( + 'type' => 'apcu', + 'default_lifetime' => 30, + ), + 'bar' => array( + 'type' => 'doctrine', + 'default_lifetime' => 5, + 'cache_provider_service' => 'app.doctrine_cache_provider', + ), + 'baz' => array( + 'type' => 'filesystem', + 'default_lifetime' => 7, + 'directory' => 'app/cache/psr', + ), + 'foobar' => array( + 'type' => 'psr6', + 'default_lifetime' => 10, + 'cache_provider_service' => 'app.cache_pool', + ), + ), + ), +)); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/cache.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/cache.xml new file mode 100644 index 0000000000000..f3d26f7380290 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/cache.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/cache.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/cache.yml new file mode 100644 index 0000000000000..0d45b13527161 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/cache.yml @@ -0,0 +1,18 @@ +framework: + cache: + pools: + foo: + type: apcu + default_lifetime: 30 + bar: + type: doctrine + default_lifetime: 5 + cache_provider_service: app.doctrine_cache_provider + baz: + type: filesystem + default_lifetime: 7 + directory: app/cache/psr + foobar: + type: psr6 + default_lifetime: 10 + cache_provider_service: app.cache_pool diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php index 55d9a16e77e5c..93478df449b99 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php @@ -13,6 +13,9 @@ use Symfony\Bundle\FrameworkBundle\Tests\TestCase; use Symfony\Bundle\FrameworkBundle\DependencyInjection\FrameworkExtension; +use Symfony\Component\Cache\Adapter\ApcuAdapter; +use Symfony\Component\Cache\Adapter\DoctrineAdapter; +use Symfony\Component\Cache\Adapter\FilesystemAdapter; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\DefinitionDecorator; use Symfony\Component\DependencyInjection\Loader\ClosureLoader; @@ -568,6 +571,16 @@ public function testPropertyInfoEnabled() $this->assertTrue($container->has('property_info')); } + public function testCachePoolServices() + { + $container = $this->createContainerFromFile('cache'); + + $this->assertCachePoolServiceDefinitionIsCreated($container, 'foo', 'apcu', array('index_1' => 30), 0); + $this->assertCachePoolServiceDefinitionIsCreated($container, 'bar', 'doctrine', array('index_0' => new Reference('app.doctrine_cache_provider'), 'index_1' => 5)); + $this->assertCachePoolServiceDefinitionIsCreated($container, 'baz', 'filesystem', array('index_0' => 'app/cache/psr', 'index_1' => 7)); + $this->assertCachePoolServiceDefinitionIsCreated($container, 'foobar', 'psr6', array('index_0' => new Reference('app.cache_pool'), 'index_1' => 10)); + } + protected function createContainer(array $data = array()) { return new ContainerBuilder(new ParameterBag(array_merge(array( @@ -636,4 +649,39 @@ private function assertVersionStrategy(ContainerBuilder $container, Reference $r $this->assertEquals($format, $versionStrategy->getArgument(1)); } } + + private function assertCachePoolServiceDefinitionIsCreated(ContainerBuilder $container, $name, $type, array $arguments, $namespaceArgumentIndex = null) + { + $id = 'cache.pool.'.$name; + + $this->assertTrue($container->has($id), sprintf('Service definition "%s" for cache pool of type "%s" is registered', $id, $type)); + + $poolDefinition = $container->getDefinition($id); + + $this->assertInstanceOf(DefinitionDecorator::class, $poolDefinition, sprintf('Cache pool "%s" is based on an abstract cache adapter.', $name)); + $this->assertEquals($arguments, $poolDefinition->getArguments()); + + $adapterDefinition = $container->getDefinition($poolDefinition->getParent()); + + switch ($type) { + case 'apcu': + $this->assertSame(ApcuAdapter::class, $adapterDefinition->getClass()); + break; + case 'doctrine': + $this->assertSame(DoctrineAdapter::class, $adapterDefinition->getClass()); + break; + case 'filesystem': + $this->assertSame(FilesystemAdapter::class, $adapterDefinition->getClass()); + break; + } + + $this->assertTrue($adapterDefinition->hasTag('cache.adapter'), sprintf('Service definition "%s" is tagged with the "cache.adapter" tag.', $id)); + + $tag = $adapterDefinition->getTag('cache.adapter'); + + if (null !== $namespaceArgumentIndex) { + $this->assertTrue(isset($tag[0]['namespace-arg-index']), 'The namespace argument index is given by the "namespace-arg-index" attribute of the "cache.adapter" tag.'); + $this->assertSame($namespaceArgumentIndex, $tag[0]['namespace-arg-index'], 'The namespace argument index is given by the "namespace-arg-index" attribute of the "cache.adapter" tag.'); + } + } }