diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CacheAdapterPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CacheAdapterPass.php new file mode 100644 index 0000000000..d8453caa89 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CacheAdapterPass.php @@ -0,0 +1,88 @@ + + * + * 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; +use Symfony\Component\DependencyInjection\Reference; + +/** + * @author Christian Flothmann + */ +class CacheAdapterPass implements CompilerPassInterface +{ + /** + * {@inheritdoc} + */ + public function process(ContainerBuilder $container) + { + $adapters = array(); + + foreach ($container->findTaggedServiceIds('cache.adapter') as $id => $tags) { + foreach ($tags as $attributes) { + $adapters[$attributes['id']] = array( + 'definition_id' => $id, + 'namespace_argument_index' => isset($attributes['namespace-arg-index']) ? $attributes['namespace-arg-index'] : null, + ); + } + } + + foreach ($container->getDefinitions() as $id => $definition) { + $definition->setArguments($this->resolveArguments($adapters, $id, $definition->getArguments())); + + $calls = $definition->getMethodCalls(); + + foreach ($calls as $index => $call) { + $calls[$index] = array($call[0], $this->resolveArguments($adapters, $id, $call[1])); + } + + $definition->setMethodCalls($calls); + + $definition->setProperties($this->resolveArguments($adapters, $id, $definition->getProperties())); + } + } + + private function resolveArguments(array $adapters, $id, array $arguments) + { + foreach ($arguments as $index => $argument) { + if ($argument instanceof Reference) { + $arguments[$index] = $this->createCacheAdapter($adapters, $id, $argument); + } + } + + return $arguments; + } + + private function createCacheAdapter(array $adapters, $serviceId, Reference $argument) + { + $adapterId = (string) $argument; + + if (0 !== strpos($adapterId, 'cache.adapter.')) { + return $argument; + } + + $name = substr($adapterId, 14); + + if (!isset($adapters[$name])) { + throw new \InvalidArgumentException(sprintf('The cache adapter "%s" is not configured.', $name)); + } + + $adapter = new DefinitionDecorator($adapters[$name]['definition_id']); + + if (null !== $adapters[$name]['namespace_argument_index']) { + $adapter->replaceArgument($adapters[$name]['namespace_argument_index'], sha1($serviceId)); + } + + return $adapter; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index 69c310a920..6dab7150c9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -114,6 +114,7 @@ class Configuration implements ConfigurationInterface $this->addSerializerSection($rootNode); $this->addPropertyAccessSection($rootNode); $this->addPropertyInfoSection($rootNode); + $this->addCacheSection($rootNode); return $treeBuilder; } @@ -547,4 +548,53 @@ class Configuration implements ConfigurationInterface ->end() ; } + + private function addCacheSection(ArrayNodeDefinition $rootNode) + { + $rootNode + ->children() + ->arrayNode('cache') + ->info('Cache configuration') + ->fixXmlConfig('adapter') + ->children() + ->arrayNode('adapters') + ->useAttributeAsKey('name') + ->prototype('array') + ->beforeNormalization() + ->always(function ($v) { + if (!isset($v['options'])) { + $v['options'] = array(); + } + + foreach ($v as $key => $value) { + if (!in_array($key, array('type', 'name', 'options'))) { + $v['options'][$key] = $value; + unset($v[$key]); + } + } + + return $v; + }) + ->end() + ->children() + ->enumNode('type') + ->info('The cache adapter type (one of "apcu", "doctrine", "filesystem")') + ->isRequired() + ->values(array('apcu', 'doctrine', 'filesystem')) + ->end() + ->arrayNode('options') + ->children() + ->integerNode('default_lifetime')->end() + ->scalarNode('cache_provider_service')->end() + ->scalarNode('directory')->end() + ->end() + ->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 22b8dde6b3..7419622990 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -12,6 +12,9 @@ namespace Symfony\Bundle\FrameworkBundle\DependencyInjection; use Doctrine\Common\Annotations\Reader; +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\ContainerInterface; use Symfony\Component\DependencyInjection\Definition; @@ -138,6 +141,10 @@ class FrameworkExtension extends Extension $this->registerPropertyInfoConfiguration($config['property_info'], $container, $loader); } + if (isset($config['cache'])) { + $this->registerCacheConfiguration($config['cache'], $container); + } + $loader->load('debug_prod.xml'); $definition = $container->findDefinition('debug.debug_handlers_listener'); @@ -1016,6 +1023,46 @@ class FrameworkExtension extends Extension } } + private function registerCacheConfiguration(array $config, ContainerBuilder $container) + { + foreach ($config['adapters'] as $name => $adapter) { + $class = null; + $arguments = array(); + $namespaceArgumentIndex = null; + + switch ($adapter['type']) { + case 'apcu': + $class = ApcuAdapter::class; + $arguments[] = null; + $arguments[] = isset($adapter['options']['default_lifetime']) ? $adapter['options']['default_lifetime'] : 0; + $namespaceArgumentIndex = 0; + break; + case 'doctrine': + $class = DoctrineAdapter::class; + $arguments[] = isset($adapter['options']['cache_provider_service']) ? new Reference($adapter['options']['cache_provider_service']) : null; + $arguments[] = isset($adapter['options']['default_lifetime']) ? $adapter['options']['default_lifetime'] : null; + break; + case 'filesystem': + $class = FilesystemAdapter::class; + $arguments[] = isset($adapter['options']['directory']) ? $adapter['options']['directory'] : null; + $arguments[] = isset($adapter['options']['default_lifetime']) ? $adapter['options']['default_lifetime'] : null; + break; + } + + $tagAttributes = array('id' => $name); + + if (null !== $namespaceArgumentIndex) { + $tagAttributes['namespace-arg-index'] = $namespaceArgumentIndex; + } + + $adapterDefinition = new Definition($class); + $adapterDefinition->setArguments($arguments); + $adapterDefinition->setAbstract(true); + $adapterDefinition->addTag('cache.adapter', $tagAttributes); + $container->setDefinition('cache.adapter.'.$name, $adapterDefinition); + } + } + /** * 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 98002fb6b6..8c0fddbdc7 100644 --- a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php +++ b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php @@ -14,6 +14,7 @@ namespace Symfony\Bundle\FrameworkBundle; 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\CacheAdapterPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ControllerArgumentValueResolverPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\FormPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\PropertyInfoPass; @@ -89,6 +90,7 @@ class FrameworkBundle extends Bundle $container->addCompilerPass(new SerializerPass()); $container->addCompilerPass(new PropertyInfoPass()); $container->addCompilerPass(new ControllerArgumentValueResolverPass()); + $container->addCompilerPass(new CacheAdapterPass()); if ($container->getParameter('kernel.debug')) { $container->addCompilerPass(new UnusedTagsPass(), PassConfig::TYPE_AFTER_REMOVING); 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 cead2295ed..79f295205f 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/CacheAdapterPassTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/CacheAdapterPassTest.php new file mode 100644 index 0000000000..923189221f --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/CacheAdapterPassTest.php @@ -0,0 +1,109 @@ + + * + * 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\CacheAdapterPass; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Reference; + +class CacheAdapterPassTest extends \PHPUnit_Framework_TestCase +{ + private $cacheAdapterPass; + + protected function setUp() + { + $this->cacheAdapterPass = new CacheAdapterPass(); + } + + public function testAdapterIsInjectedIntoConstructorArguments() + { + $container = $this->initializeContainer(); + $this->cacheAdapterPass->process($container); + $adapter = $container->getDefinition('foo')->getArgument(0); + + $this->assertInstanceOf('Symfony\Component\DependencyInjection\DefinitionDecorator', $adapter); + $this->assertFalse($adapter->isAbstract()); + $this->assertSame('cache.adapter.apcu_adapter', $adapter->getParent()); + $this->assertSame('0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33', $adapter->getArgument(0)); + } + + public function testAdapterIsInjectedIntoMethodArguments() + { + $container = $this->initializeContainer(); + $this->cacheAdapterPass->process($container); + $methodCalls = $container->getDefinition('bar')->getMethodCalls(); + $arguments = $methodCalls[0][1]; + $adapter = $arguments[0]; + + $this->assertInstanceOf('Symfony\Component\DependencyInjection\DefinitionDecorator', $adapter); + $this->assertFalse($adapter->isAbstract()); + $this->assertSame('cache.adapter.doctrine_adapter', $adapter->getParent()); + } + + public function testAdapterIsInjectIntoProperties() + { + $container = $this->initializeContainer(); + $this->cacheAdapterPass->process($container); + $properties = $container->getDefinition('baz')->getProperties(); + $adapter = $properties['cache']; + + $this->assertInstanceOf('Symfony\Component\DependencyInjection\DefinitionDecorator', $adapter); + $this->assertFalse($adapter->isAbstract()); + $this->assertSame('cache.adapter.fs_adapter', $adapter->getParent()); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage The cache adapter "bar" is not configured + */ + public function testThrowsExceptionWhenReferencedAdapterIsNotConfigured() + { + $container = new ContainerBuilder(); + $container->setDefinition('foo', new Definition('Foo', array(new Reference('cache.adapter.bar')))); + $this->cacheAdapterPass->process($container); + } + + private function initializeContainer() + { + $container = new ContainerBuilder(); + + $apcuAdapter = new Definition('Symfony\Component\Cache\Adapter\ApcuAdapter'); + $apcuAdapter->setAbstract(true); + $apcuAdapter->addTag('cache.adapter', array('id' => 'adapter1', 'namespace-arg-index' => 0)); + $container->setDefinition('cache.adapter.apcu_adapter', $apcuAdapter); + + $doctrineAdapter = new Definition('Symfony\Component\Cache\Adapter\DoctrineAdapter'); + $doctrineAdapter->setAbstract(true); + $doctrineAdapter->addTag('cache.adapter', array('id' => 'adapter2')); + $container->setDefinition('cache.adapter.doctrine_adapter', $doctrineAdapter); + + $filesystemAdapter = new Definition('Symfony\Component\Cache\Adapter\FilesystemAdapter'); + $filesystemAdapter->setAbstract(true); + $filesystemAdapter->addTag('cache.adapter', array('id' => 'adapter3')); + $container->setDefinition('cache.adapter.fs_adapter', $filesystemAdapter); + + $foo = new Definition(); + $foo->setArguments(array(new Reference('cache.adapter.adapter1'))); + $container->setDefinition('foo', $foo); + + $bar = new Definition(); + $bar->addMethodCall('setCache', array(new Reference('cache.adapter.adapter2'))); + $container->setDefinition('bar', $bar); + + $baz = new Definition(); + $baz->setProperty('cache', new Reference('cache.adapter.adapter3')); + $container->setDefinition('baz', $baz); + + return $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 0000000000..a5c375ab3b --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/cache.php @@ -0,0 +1,28 @@ +loadFromExtension('framework', array( + 'cache' => array( + 'adapters' => array( + 'foo' => array( + 'type' => 'apcu', + 'options' => array( + 'default_lifetime' => 30, + ), + ), + 'bar' => array( + 'type' => 'doctrine', + 'options' => array( + 'default_lifetime' => 5, + 'cache_provider_service' => 'app.doctrine_cache_provider', + ), + ), + 'baz' => array( + 'type' => 'filesystem', + 'options' => array( + 'default_lifetime' => 7, + 'directory' => 'app/cache/psr', + ), + ), + ), + ), +)); 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 0000000000..086ea63a01 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/cache.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + 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 0000000000..9d0de6ad89 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/cache.yml @@ -0,0 +1,17 @@ +framework: + cache: + adapters: + foo: + type: apcu + options: + default_lifetime: 30 + bar: + type: doctrine + options: + default_lifetime: 5 + cache_provider_service: app.doctrine_cache_provider + baz: + type: filesystem + options: + default_lifetime: 7 + directory: app/cache/psr diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php index 55d9a16e77..6643e7f1f7 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php @@ -13,6 +13,9 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection; 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,15 @@ abstract class FrameworkExtensionTest extends TestCase $this->assertTrue($container->has('property_info')); } + public function testCacheAdaptersAbstractServices() + { + $container = $this->createContainerFromFile('cache'); + + $this->assertCacheAdapterIsRegistered($container, 'foo', 'apcu', array(null, 30), 0); + $this->assertCacheAdapterIsRegistered($container, 'bar', 'doctrine', array(new Reference('app.doctrine_cache_provider'), 5)); + $this->assertCacheAdapterIsRegistered($container, 'baz', 'filesystem', array('app/cache/psr', 7)); + } + protected function createContainer(array $data = array()) { return new ContainerBuilder(new ParameterBag(array_merge(array( @@ -636,4 +648,39 @@ abstract class FrameworkExtensionTest extends TestCase $this->assertEquals($format, $versionStrategy->getArgument(1)); } } + + private function assertCacheAdapterIsRegistered(ContainerBuilder $container, $name, $type, array $arguments, $namespaceArgumentIndex = null) + { + $id = 'cache.adapter.'.$name; + + $this->assertTrue($container->has($id), sprintf('Service definition "%s" for cache adapter of type "%s" is registered', $id, $type)); + + $adapterDefinition = $container->getDefinition($id); + + 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->isAbstract(), sprintf('Service definition "%s" for cache adapter "%s" is abstract', $id, $name)); + $this->assertEquals($arguments, $adapterDefinition->getArguments()); + $this->assertTrue($adapterDefinition->hasTag('cache.adapter'), sprintf('Service definition "%s" is tagged with the "cache.adapter" tag.', $id)); + + $tag = $adapterDefinition->getTag('cache.adapter'); + + $this->assertTrue(isset($tag[0]['id']), 'The adapter name is the "id" attribute of the "cache.adapter" tag.'); + $this->assertSame($name, $tag[0]['id'], 'The adapter name is the "id" attribute of the "cache.adapter" tag.'); + + 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.'); + } + } }