Permalink
Browse files

feature #20973 [DI] Add getter injection (nicolas-grekas)

This PR was merged into the 3.3-dev branch.

Discussion
----------

[DI] Add getter injection

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #20657
| License       | MIT
| Doc PR        | symfony/symfony-docs#7300

Getter overriding by the container will allow a new kind of dependency injection which enables easier laziness and more immutable classes, by not requiring any corresponding setter. See linked issue for more.

This is WIP:
- [x] wire the concept
- [x] dump anonymous classes with PhpDumper
- [x] generate at runtime in ContainerBuilder::createService
- [x] tests
- [x] make it work on PHP 5

Commits
-------

cb49858 [DI] Add getter injection
  • Loading branch information...
2 parents b465634 + cb49858 commit 2183f98f5482096ead88e9d251c6283fca067174 @fabpot fabpot committed Jan 30, 2017
Showing with 811 additions and 40 deletions.
  1. +1 −0 src/Symfony/Component/DependencyInjection/CHANGELOG.md
  2. +1 −0 src/Symfony/Component/DependencyInjection/Compiler/AbstractRecursivePass.php
  3. +6 −0 src/Symfony/Component/DependencyInjection/Compiler/ResolveDefinitionTemplatesPass.php
  4. +1 −0 src/Symfony/Component/DependencyInjection/Compiler/ResolveInvalidReferencesPass.php
  5. +1 −0 src/Symfony/Component/DependencyInjection/Compiler/ResolveReferencesToAliasesPass.php
  6. +122 −2 src/Symfony/Component/DependencyInjection/ContainerBuilder.php
  7. +32 −0 src/Symfony/Component/DependencyInjection/Definition.php
  8. +7 −0 src/Symfony/Component/DependencyInjection/Dumper/GraphvizDumper.php
  9. +146 −37 src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php
  10. +4 −0 src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php
  11. +4 −0 src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php
  12. +23 −0 src/Symfony/Component/DependencyInjection/LazyProxy/GetterProxyInterface.php
  13. +2 −1 src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php
  14. +5 −0 src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php
  15. +13 −0 src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd
  16. +13 −0 src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveInvalidReferencesPassTest.php
  17. +53 −0 src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php
  18. +61 −0 src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php
  19. +57 −0 src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container29.php
  20. +42 −0 src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container30.php
  21. +152 −0 src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services29.php
  22. +10 −0 src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services31.xml
  23. +6 −0 src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services31.yml
  24. +9 −0 src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php
  25. +9 −0 src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php
  26. +14 −0 src/Symfony/Component/VarDumper/Caster/ClassStub.php
  27. +16 −0 src/Symfony/Component/VarDumper/Caster/SymfonyCaster.php
  28. +1 −0 src/Symfony/Component/VarDumper/Cloner/AbstractCloner.php
@@ -4,6 +4,7 @@ CHANGELOG
3.3.0
-----
+ * [EXPERIMENTAL] added support for getter-injection
* added support for omitting the factory class name in a service definition if the definition class is set
* deprecated case insensitivity of service identifiers
* added "iterator" argument type for lazy iteration over a set of values and services
@@ -61,6 +61,7 @@ protected function processValue($value, $isRoot = false)
} elseif ($value instanceof Definition) {
$value->setArguments($this->processValue($value->getArguments()));
$value->setProperties($this->processValue($value->getProperties()));
+ $value->setOverriddenGetters($this->processValue($value->getOverriddenGetters()));
$value->setMethodCalls($this->processValue($value->getMethodCalls()));
if ($v = $value->getFactory()) {
@@ -88,6 +88,7 @@ private function doResolveDefinition(ChildDefinition $definition)
$def->setClass($parentDef->getClass());
$def->setArguments($parentDef->getArguments());
$def->setMethodCalls($parentDef->getMethodCalls());
+ $def->setOverriddenGetters($parentDef->getOverriddenGetters());
$def->setProperties($parentDef->getProperties());
$def->setAutowiringTypes($parentDef->getAutowiringTypes());
if ($parentDef->isDeprecated()) {
@@ -160,6 +161,11 @@ private function doResolveDefinition(ChildDefinition $definition)
$def->setMethodCalls(array_merge($def->getMethodCalls(), $calls));
}
+ // merge overridden getters
+ foreach ($definition->getOverriddenGetters() as $k => $v) {
+ $def->setOverriddenGetter($k, $v);
+ }
+
// merge autowiring types
foreach ($definition->getAutowiringTypes() as $autowiringType) {
$def->addAutowiringType($autowiringType);
@@ -59,6 +59,7 @@ private function processValue($value, $rootLevel = 0, $level = 0)
}
$value->setArguments($this->processValue($value->getArguments(), 0));
$value->setProperties($this->processValue($value->getProperties(), 1));
+ $value->setOverriddenGetters($this->processValue($value->getOverriddenGetters(), 1));
$value->setMethodCalls($this->processValue($value->getMethodCalls(), 2));
} elseif (is_array($value)) {
$i = 0;
@@ -42,6 +42,7 @@ public function process(ContainerBuilder $container)
$definition->setArguments($this->processArguments($definition->getArguments()));
$definition->setMethodCalls($this->processArguments($definition->getMethodCalls()));
+ $definition->setOverriddenGetters($this->processArguments($definition->getOverriddenGetters()));
$definition->setProperties($this->processArguments($definition->getProperties()));
$definition->setFactory($this->processFactory($definition->getFactory()));
}
@@ -29,6 +29,7 @@
use Symfony\Component\Config\Resource\ResourceInterface;
use Symfony\Component\DependencyInjection\LazyProxy\Instantiator\InstantiatorInterface;
use Symfony\Component\DependencyInjection\LazyProxy\Instantiator\RealServiceInstantiator;
+use Symfony\Component\DependencyInjection\LazyProxy\GetterProxyInterface;
use Symfony\Component\ExpressionLanguage\Expression;
use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface;
@@ -890,6 +891,9 @@ private function createService(Definition $definition, $id, $tryProxy = true)
$arguments = $this->resolveServices($parameterBag->unescapeValue($parameterBag->resolveValue($definition->getArguments())));
if (null !== $factory = $definition->getFactory()) {
+ if ($definition->getOverriddenGetters()) {
+ throw new RuntimeException(sprintf('Cannot create service "%s": factories and overridden getters are incompatible with each other.', $id));
+ }
if (is_array($factory)) {
$factory = array($this->resolveServices($parameterBag->resolveValue($factory[0])), $factory[1]);
} elseif (!is_string($factory)) {
@@ -908,11 +912,31 @@ private function createService(Definition $definition, $id, $tryProxy = true)
} else {
$r = new \ReflectionClass($parameterBag->resolveValue($definition->getClass()));
- $service = null === $r->getConstructor() ? $r->newInstance() : $r->newInstanceArgs($arguments);
-
if (!$definition->isDeprecated() && 0 < strpos($r->getDocComment(), "\n * @deprecated ")) {
@trigger_error(sprintf('The "%s" service relies on the deprecated "%s" class. It should either be deprecated or its implementation upgraded.', $id, $r->name), E_USER_DEPRECATED);
}
+ if ($definition->getOverriddenGetters()) {
+ static $salt;
+ if (null === $salt) {
+ $salt = str_replace('.', '', uniqid('', true));
+ }
+ $service = sprintf('%s implements \\%s { private $container%4$s; private $values%4$s; %s }', $r->name, GetterProxyInterface::class, $this->generateOverriddenGetters($id, $definition, $r, $salt), $salt);
+ if (!class_exists($proxyClass = 'SymfonyProxy_'.md5($service), false)) {
+ eval(sprintf('class %s extends %s', $proxyClass, $service));
+ }
+ $r = new \ReflectionClass($proxyClass);
+ $constructor = $r->getConstructor();
+ if ($constructor && !defined('HHVM_VERSION') && $constructor->getDeclaringClass()->isInternal()) {
+ $constructor = null;
+ }
+ $service = $constructor ? $r->newInstanceWithoutConstructor() : $r->newInstanceArgs($arguments);
+ call_user_func(\Closure::bind(function ($c, $v, $s) { $this->{'container'.$s} = $c; $this->{'values'.$s} = $v; }, $service, $service), $this, $definition->getOverriddenGetters(), $salt);
+ if ($constructor) {
+ $constructor->invokeArgs($service, $arguments);
+ }
+ } else {
+ $service = null === $r->getConstructor() ? $r->newInstance() : $r->newInstanceArgs($arguments);
+ }
}
if ($tryProxy || !$definition->isLazy()) {
@@ -1155,6 +1179,102 @@ public function getEnvCounters()
return $this->envCounters;
}
+ private function generateOverriddenGetters($id, Definition $definition, \ReflectionClass $class, $salt)
+ {
+ if ($class->isFinal()) {
+ throw new RuntimeException(sprintf('Unable to configure getter injection for service "%s": class "%s" cannot be marked as final.', $id, $class->name));
+ }
+ $getters = '';
+ foreach ($definition->getOverriddenGetters() as $name => $returnValue) {
+ $r = self::getGetterReflector($class, $name, $id, $type);
+ $visibility = $r->isProtected() ? 'protected' : 'public';
+ $name = var_export($name, true);
+ $getters .= <<<EOF
+
+{$visibility} function {$r->name}(){$type} {
+ \$c = \$this->container{$salt};
+ \$b = \$c->getParameterBag();
+ \$v = \$this->values{$salt}[{$name}];
+
+ foreach (\$c->getServiceConditionals(\$v) as \$s) {
+ if (!\$c->has(\$s)) {
+ return parent::{$r->name}();
+ }
+ }
+
+ return \$c->resolveServices(\$b->unescapeValue(\$b->resolveValue(\$v)));
+}
+EOF;
+ }
+
+ return $getters;
+ }
+
+ /**
+ * @internal
+ */
+ public static function getGetterReflector(\ReflectionClass $class, $name, $id, &$type)
+ {
+ if (!$class->hasMethod($name)) {
+ throw new RuntimeException(sprintf('Unable to configure getter injection for service "%s": method "%s::%s" does not exist.', $id, $class->name, $name));
+ }
+ $r = $class->getMethod($name);
+ if ($r->isPrivate()) {
+ throw new RuntimeException(sprintf('Unable to configure getter injection for service "%s": method "%s::%s" must be public or protected.', $id, $class->name, $r->name));
+ }
+ if ($r->isStatic()) {
+ throw new RuntimeException(sprintf('Unable to configure getter injection for service "%s": method "%s::%s" cannot be static.', $id, $class->name, $r->name));
+ }
+ if ($r->isFinal()) {
+ throw new RuntimeException(sprintf('Unable to configure getter injection for service "%s": method "%s::%s" cannot be marked as final.', $id, $class->name, $r->name));
+ }
+ if ($r->returnsReference()) {
+ throw new RuntimeException(sprintf('Unable to configure getter injection for service "%s": method "%s::%s" cannot return by reference.', $id, $class->name, $r->name));
+ }
+ if (0 < $r->getNumberOfParameters()) {
+ throw new RuntimeException(sprintf('Unable to configure getter injection for service "%s": method "%s::%s" cannot have any arguments.', $id, $class->name, $r->name));
+ }
+ if ($type = method_exists($r, 'getReturnType') ? $r->getReturnType() : null) {
+ $type = ': '.($type->allowsNull() ? '?' : '').self::generateTypeHint($type, $r);
+ }
+
+ return $r;
+ }
+
+ /**
+ * @internal
+ */
+ public static function generateTypeHint($type, \ReflectionFunctionAbstract $r)
+ {
+ if (is_string($type)) {
+ $name = $type;
+
+ if ('callable' === $name || 'array' === $name) {
+ return $name;
+ }
+ } else {
+ $name = $type instanceof \ReflectionNamedType ? $type->getName() : $type->__toString();
+
+ if ($type->isBuiltin()) {
+ return $name;
+ }
+ }
+ $lcName = strtolower($name);
+
+ if ('self' !== $lcName && 'parent' !== $lcName) {
+ return '\\'.$name;
+ }
+ if (!$r instanceof \ReflectionMethod) {
+ return;
+ }
+ if ('self' === $lcName) {
+ return '\\'.$r->getDeclaringClass()->name;
+ }
+ if ($parent = $r->getDeclaringClass()->getParentClass()) {
+ return '\\'.$parent->name;
+ }
+ }
+
/**
* @internal
*/
@@ -29,6 +29,7 @@ class Definition
private $deprecationTemplate = 'The "%service_id%" service is deprecated. You should stop using it, as it will soon be removed.';
private $properties = array();
private $calls = array();
+ private $getters = array();
private $configurator;
private $tags = array();
private $public = true;
@@ -324,6 +325,37 @@ public function getMethodCalls()
}
/**
+ * @experimental in version 3.3
+ */
+ public function setOverriddenGetter($name, $returnValue)
+ {
+ if (!$name) {
+ throw new InvalidArgumentException(sprintf('Getter name cannot be empty.'));
+ }
+ $this->getters[strtolower($name)] = $returnValue;
+
+ return $this;
+ }
+
+ /**
+ * @experimental in version 3.3
+ */
+ public function setOverriddenGetters(array $getters)
+ {
+ $this->getters = array_change_key_case($getters, CASE_LOWER);
+
+ return $this;
+ }
+
+ /**
+ * @experimental in version 3.3
+ */
+ public function getOverriddenGetters()
+ {
+ return $this->getters;
+ }
+
+ /**
* Sets tags for this definition.
*
* @param array $tags
@@ -80,6 +80,13 @@ public function dump(array $options = array())
$this->findEdges($id, $call[1], false, $call[0].'()')
);
}
+
+ foreach ($definition->getOverriddenGetters() as $name => $value) {
+ $this->edges[$id] = array_merge(
+ $this->edges[$id],
+ $this->findEdges($id, $value, false, $name.'()')
+ );
+ }
}
return $this->container->resolveEnvPlaceholders($this->startDot().$this->addNodes().$this->addEdges().$this->endDot(), '__ENV_%s__');
Oops, something went wrong.

0 comments on commit 2183f98

Please sign in to comment.