diff --git a/src/Symfony/Component/DependencyInjection/Compiler/AnalyzeServiceReferencesPass.php b/src/Symfony/Component/DependencyInjection/Compiler/AnalyzeServiceReferencesPass.php index 2399f6ca1218..fed33ff11f01 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/AnalyzeServiceReferencesPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/AnalyzeServiceReferencesPass.php @@ -73,6 +73,7 @@ public function process(ContainerBuilder $container) if (!$this->onlyConstructorArguments) { $this->processArguments($definition->getMethodCalls()); $this->processArguments($definition->getProperties()); + $this->processArguments($definition->getLookupMethods()); } } diff --git a/src/Symfony/Component/DependencyInjection/Compiler/GenerateLookupMethodClassesPass.php b/src/Symfony/Component/DependencyInjection/Compiler/GenerateLookupMethodClassesPass.php new file mode 100644 index 000000000000..627b3b94f4a1 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Compiler/GenerateLookupMethodClassesPass.php @@ -0,0 +1,173 @@ + + */ +class GenerateLookupMethodClassesPass implements CompilerPassInterface +{ + private $generatedClasses = array(); + private $container; + private $currentId; + private $cacheDir; + + public function __construct($cacheDir) + { + $this->cacheDir = $cacheDir; + } + + public function process(ContainerBuilder $container) + { + $this->container = $container; + $this->generatedClasses = array(); + $this->cleanUpCacheDir($this->cacheDir); + + foreach ($container->getDefinitions() as $id => $definition) { + if ($definition->isSynthetic() || $definition->isAbstract()) { + continue; + } + if (!$methods = $definition->getLookupMethods()) { + continue; + } + + $this->currentId = $id; + $this->generateClass($definition, $this->cacheDir); + } + } + + private function cleanUpCacheDir($dir) + { + if (!is_dir($dir)) { + if (false === @mkdir($dir, 0777, true)) { + throw new \RuntimeException(sprintf('The cache directory "%s" could not be created.', $dir)); + } + + return; + } + + if (!is_writable($dir)) { + throw new \RuntimeException(sprintf('The cache directory "%s" is not writable.', $dir)); + } + + foreach (new \DirectoryIterator($dir) as $file) { + if ('.' === $file->getFileName() || !is_file($file->getPathName())) { + continue; + } + + if (false === @unlink($file->getPathName())) { + throw new \RuntimeException(sprintf('Could not delete auto-generated file "%s".', $file->getPathName())); + } + } + } + + private function generateClass(Definition $definition, $cacheDir) + { + $code = <<<'EOF' +getFile()) { + $require = sprintf("\nrequire_once %s;\n", var_export($file, true)); + } else { + $require = ''; + } + + // get class name + $class = new \ReflectionClass($definition->getClass()); + $i = 1; + do { + $className = $class->getShortName(); + + if ($i > 1) { + $className .= '_'.$i; + } + + $i += 1; + } while (isset($this->generatedClasses[$className])); + $this->generatedClasses[$className] = true; + + $lookupMethod = <<<'EOF' + + %s function %s() + { + return %s; + } +EOF; + $lookupMethods = ''; + foreach ($definition->getLookupMethods() as $name => $value) { + if (!$class->hasMethod($name)) { + throw new \RuntimeException(sprintf('The class "%s" has no method named "%s".', $class->getName(), $name)); + } + $method = $class->getMethod($name); + if ($method->isFinal()) { + throw new \RuntimeException(sprintf('The method "%s::%s" is marked as final and cannot be declared as lookup-method.', $class->getName(), $name)); + } + if ($method->isPrivate()) { + throw new \RuntimeException(sprintf('The method "%s::%s" is marked as private and cannot be declared as lookup-method.', $class->getName(), $name)); + } + if ($method->getParameters()) { + throw new \RuntimeException(sprintf('The method "%s::%s" must have a no-arguments signature if you want to use it as lookup-method.', $class->getName(), $name)); + } + + $lookupMethods .= sprintf($lookupMethod, + $method->isPublic() ? 'public' : 'protected', + $name, + $this->dumpValue($value) + ); + } + + $code = sprintf($code, $require, $this->currentId, $className, $class->getName(), $lookupMethods); + file_put_contents($cacheDir.'/'.$className.'.php', $code); + require_once $cacheDir.'/'.$className.'.php'; + $definition->setClass('Symfony\Component\DependencyInjection\LookupMethodClasses\\'.$className); + $definition->setFile($cacheDir.'/'.$className.'.php'); + $definition->setProperty('__symfonyDependencyInjectionContainer', new Reference('service_container')); + $definition->setLookupMethods(array()); + } + + private function dumpValue($value) + { + if ($value instanceof Parameter) { + return var_export($this->container->getParameter((string) $value), true); + } else if ($value instanceof Reference) { + $id = (string) $value; + if ($this->container->hasAlias($id)) { + $this->container->setAlias($id, (string) $this->container->getAlias()); + } else if ($this->container->hasDefinition($id)) { + $this->container->getDefinition($id)->setPublic(true); + } + + return '$this->__symfonyDependencyInjectionContainer->get('.var_export($id, true).', '.var_export($value->getInvalidBehavior(), true).')'; + } else if (is_array($value) || is_scalar($value) || null === $value) { + return var_export($value, true); + } + + throw new \RuntimeException(sprintf('Invalid value for lookup method of service "%s": %s', $this->currentId, json_encode($value))); + } +} \ No newline at end of file diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ResolveDefinitionTemplatesPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ResolveDefinitionTemplatesPass.php index 9473b788c023..78953d9c4901 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/ResolveDefinitionTemplatesPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/ResolveDefinitionTemplatesPass.php @@ -68,6 +68,7 @@ private function resolveDefinition($id, DefinitionDecorator $definition) $def->setArguments($parentDef->getArguments()); $def->setMethodCalls($parentDef->getMethodCalls()); $def->setProperties($parentDef->getProperties()); + $def->setLookupMethods($parentDef->getLookupMethods()); $def->setFactoryClass($parentDef->getFactoryClass()); $def->setFactoryMethod($parentDef->getFactoryMethod()); $def->setFactoryService($parentDef->getFactoryService()); @@ -119,6 +120,11 @@ private function resolveDefinition($id, DefinitionDecorator $definition) $def->setProperty($k, $v); } + // merge lookup methods + foreach ($definition->getLookupMethods() as $k => $v) { + $def->setLookupMethod($k, $v); + } + // append method calls if (count($calls = $definition->getMethodCalls()) > 0) { $def->setMethodCalls(array_merge($def->getMethodCalls(), $calls)); diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ResolveInvalidReferencesPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ResolveInvalidReferencesPass.php index 1ed10058c989..fe813eba0e41 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/ResolveInvalidReferencesPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/ResolveInvalidReferencesPass.php @@ -41,6 +41,9 @@ public function process(ContainerBuilder $container) $definition->setArguments( $this->processArguments($definition->getArguments()) ); + $definition->setLookupMethods( + $this->processArguments($definition->getLookupMethods()) + ); $calls = array(); foreach ($definition->getMethodCalls() as $call) { diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ResolveReferencesToAliasesPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ResolveReferencesToAliasesPass.php index f00bb75595ef..2e7c4587f522 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/ResolveReferencesToAliasesPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/ResolveReferencesToAliasesPass.php @@ -41,6 +41,7 @@ public function process(ContainerBuilder $container) $definition->setArguments($this->processArguments($definition->getArguments())); $definition->setMethodCalls($this->processArguments($definition->getMethodCalls())); $definition->setProperties($this->processArguments($definition->getProperties())); + $definition->setLookupMethods($this->processArguments($definition->getLookupMethods())); } foreach ($container->getAliases() as $id => $alias) { diff --git a/src/Symfony/Component/DependencyInjection/Definition.php b/src/Symfony/Component/DependencyInjection/Definition.php index 5962480b529d..2ed5112986bf 100644 --- a/src/Symfony/Component/DependencyInjection/Definition.php +++ b/src/Symfony/Component/DependencyInjection/Definition.php @@ -15,6 +15,7 @@ * Definition represents a service definition. * * @author Fabien Potencier + * @author Johannes M. Schmitt */ class Definition { @@ -31,6 +32,7 @@ class Definition private $public; private $synthetic; private $abstract; + private $lookupMethods; protected $arguments; @@ -51,6 +53,7 @@ public function __construct($class = null, array $arguments = array()) $this->synthetic = false; $this->abstract = false; $this->properties = array(); + $this->lookupMethods = array(); } /** @@ -546,4 +549,23 @@ public function getConfigurator() { return $this->configurator; } + + public function setLookupMethod($name, $value) + { + $this->lookupMethods[$name] = $value; + + return $this; + } + + public function getLookupMethods() + { + return $this->lookupMethods; + } + + public function setLookupMethods(array $methods) + { + $this->lookupMethods = $methods; + + return $this; + } } diff --git a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php index 69d4195eff33..302b4a926ee0 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php @@ -459,6 +459,10 @@ private function addService($id, $definition) $this->referenceVariables = array(); $this->variableCount = 0; + if ($definition->getLookupMethods()) { + throw new \RuntimeException(sprintf('You have set lookup methods for service "%s", but the GenerateLookupMethodClassesPass was not enabled.', $id)); + } + $return = ''; if ($definition->isSynthetic()) { $return = sprintf('@throws \RuntimeException always since this service is expected to be injected dynamically'); diff --git a/src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php index 00a2acf2b9e0..90248ef76e0a 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php @@ -150,6 +150,10 @@ private function addService($definition, $id, \DOMElement $parent) $this->convertParameters($parameters, 'property', $service, 'name'); } + if ($parameters = $definition->getLookupMethods()) { + $this->convertParameters($parameters, 'lookup_method', $service, 'name'); + } + $this->addMethodCalls($definition->getMethodCalls(), $service); if ($callable = $definition->getConfigurator()) { diff --git a/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php index 0bdf1dd4e9ee..888e4a56a934 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php @@ -85,6 +85,10 @@ private function addService($id, $definition) $code .= sprintf(" properties: %s\n", Yaml::dump($this->dumpValue($definition->getProperties()), 0)); } + if ($definition->getLookupMethods()) { + $code .= sprintf(" lookup_methods: %s\n", Yaml::dump($this->dumpValue($definition->getLookupMethods()), 0)); + } + if ($definition->getMethodCalls()) { $code .= sprintf(" calls:\n %s\n", str_replace("\n", "\n ", Yaml::dump($this->dumpValue($definition->getMethodCalls()), 1))); } diff --git a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php index 45c639045214..ccf2d3539220 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php @@ -12,7 +12,6 @@ namespace Symfony\Component\DependencyInjection\Loader; use Symfony\Component\DependencyInjection\DefinitionDecorator; - use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Alias; use Symfony\Component\DependencyInjection\Definition; @@ -164,6 +163,7 @@ private function parseDefinition($id, $service, $file) $definition->setArguments($service->getArgumentsAsPhp('argument')); $definition->setProperties($service->getArgumentsAsPhp('property')); + $definition->setLookupMethods($service->getArgumentsAsPhp('lookup-method')); if (isset($service->configurator)) { if (isset($service->configurator['function'])) { diff --git a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php index 7885421a5dd9..798e594b974f 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php @@ -186,6 +186,10 @@ private function parseDefinition($id, $service, $file) $definition->setProperties($this->resolveServices($service['properties'])); } + if (isset($service['lookup_methods'])) { + $definition->setLookupMethods($this->resolveServices($service['lookup_methods'])); + } + if (isset($service['configurator'])) { if (is_string($service['configurator'])) { $definition->setConfigurator($service['configurator']); diff --git a/src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd b/src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd index 7d46e8caa6e8..b7862e51ada5 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd +++ b/src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd @@ -80,6 +80,7 @@ + diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index 797a09df050a..2e92edfe7882 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -27,6 +27,7 @@ use Symfony\Component\HttpKernel\Config\FileLocator; use Symfony\Component\HttpKernel\DependencyInjection\MergeExtensionConfigurationPass; use Symfony\Component\HttpKernel\DependencyInjection\AddClassesToCachePass; +use Symfony\Component\DependencyInjection\Compiler\GenerateLookupMethodClassesPass; use Symfony\Component\HttpKernel\DependencyInjection\Extension as DIExtension; use Symfony\Component\Config\Loader\LoaderResolver; use Symfony\Component\Config\Loader\DelegatingLoader; @@ -577,8 +578,15 @@ protected function buildContainer() } $container->addObjectResource($this); + $passConfig = $container->getCompilerPassConfig(); + // ensure these extensions are implicitly loaded - $container->getCompilerPassConfig()->setMergePass(new MergeExtensionConfigurationPass($extensions)); + $passConfig->setMergePass(new MergeExtensionConfigurationPass($extensions)); + + // enable method injection + $passes = $passConfig->getRemovingPasses(); + array_unshift($passes, new GenerateLookupMethodClassesPass($this->getCacheDir().'/lookup_method_classes')); + $passConfig->setRemovingPasses($passes); if (null !== $cont = $this->registerContainerConfiguration($this->getContainerLoader($container))) { $container->merge($cont); diff --git a/tests/Symfony/Tests/Component/DependencyInjection/Compiler/GenerateLookupMethodClassesPassTest.php b/tests/Symfony/Tests/Component/DependencyInjection/Compiler/GenerateLookupMethodClassesPassTest.php new file mode 100644 index 000000000000..f027aa17e291 --- /dev/null +++ b/tests/Symfony/Tests/Component/DependencyInjection/Compiler/GenerateLookupMethodClassesPassTest.php @@ -0,0 +1,68 @@ +dir = sys_get_temp_dir().'/lookup_method_classes'; + + if (!is_dir($this->dir) && false === @mkdir($this->dir, 0777, true)) { + $this->markTestIncomplete('Cache dir could not be created.'); + } + + $this->container = new ContainerBuilder(); + } + + protected function tearDown() + { + foreach (new \DirectoryIterator($this->dir) as $file) { + if ('.' === $file->getFileName()) { + continue; + } + + @unlink($file->getPathName()); + } + + @rmdir($this->dir); + } + + public function testProcess() + { + $defFoobar = $this->container + ->register('foobar', 'Symfony\Tests\Component\DependencyInjection\Compiler\FooBarService') + ->setPublic(false) + ; + $def = $this->container + ->register('test', 'Symfony\Tests\Component\DependencyInjection\Compiler\LookupMethodTestClass') + ->setLookupMethod('getFooBar', new Reference('foobar')) + ; + + $this->process(); + + $service = $this->container->get('test'); + $this->assertInstanceOf('Symfony\Component\DependencyInjection\LookupMethodClasses\LookupMethodTestClass', $service); + $this->assertInstanceOf('Symfony\Tests\Component\DependencyInjection\Compiler\FooBarService', $service->getFooBar()); + $this->assertEquals($this->dir.'/LookupMethodTestClass.php', $def->getFile()); + $this->assertEquals('Symfony\Component\DependencyInjection\LookupMethodClasses\LookupMethodTestClass', $def->getClass()); + $this->assertTrue($defFoobar->isPublic()); + } + + private function process() + { + $pass = new GenerateLookupMethodClassesPass($this->dir); + $pass->process($this->container); + } +} + +class FooBarService {} + +abstract class LookupMethodTestClass +{ + abstract public function getFooBar(); +} diff --git a/tests/Symfony/Tests/Component/DependencyInjection/DefinitionTest.php b/tests/Symfony/Tests/Component/DependencyInjection/DefinitionTest.php index 9ff87e09b731..26fdc2da3362 100644 --- a/tests/Symfony/Tests/Component/DependencyInjection/DefinitionTest.php +++ b/tests/Symfony/Tests/Component/DependencyInjection/DefinitionTest.php @@ -238,4 +238,22 @@ public function testSetProperty() $this->assertSame($def, $def->setProperty('foo', 'bar')); $this->assertEquals(array('foo' => 'bar'), $def->getProperties()); } + + public function testSetLookupMethod() + { + $def = new Definition('stdClass'); + + $this->assertEquals(array(), $def->getLookupMethods()); + $this->assertSame($def, $def->setLookupMethod('foo', 'bar')); + $this->assertEquals(array('foo' => 'bar'), $def->getLookupMethods()); + } + + public function testSetLookupMethods() + { + $def = new Definition('stdClass'); + + $this->assertEquals(array(), $def->getLookupMethods()); + $this->assertSame($def, $def->setLookupMethods(array('foo' => 'bar'))); + $this->assertEquals(array('foo' => 'bar'), $def->getLookupMethods()); + } }