Skip to content

Commit 48ab62f

Browse files
committed
[DependencyInjection][WIP] Make method (setter) autowiring configurable
1 parent 070e53a commit 48ab62f

File tree

12 files changed

+357
-32
lines changed

12 files changed

+357
-32
lines changed

src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php

+104-29
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424
*/
2525
class AutowirePass implements CompilerPassInterface
2626
{
27+
/**
28+
* @var ContainerBuilder
29+
*/
2730
private $container;
2831
private $reflectionClasses = array();
2932
private $definedTypes = array();
@@ -41,8 +44,8 @@ public function process(ContainerBuilder $container)
4144
try {
4245
$this->container = $container;
4346
foreach ($container->getDefinitions() as $id => $definition) {
44-
if ($definition->isAutowired()) {
45-
$this->completeDefinition($id, $definition);
47+
if ($autowired = $definition->getAutowired()) {
48+
$this->completeDefinition($id, $definition, $autowired);
4649
}
4750
}
4851
} finally {
@@ -72,8 +75,10 @@ public static function createResourceForClass(\ReflectionClass $reflectionClass)
7275
$metadata['__construct'] = self::getResourceMetadataForMethod($constructor);
7376
}
7477

75-
foreach (self::getSetters($reflectionClass) as $reflectionMethod) {
76-
$metadata[$reflectionMethod->name] = self::getResourceMetadataForMethod($reflectionMethod);
78+
foreach ($reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $reflectionMethod) {
79+
if (!$reflectionMethod->isStatic()) {
80+
$metadata[$reflectionMethod->name] = self::getResourceMetadataForMethod($reflectionMethod);
81+
}
7782
}
7883

7984
return new AutowireServiceResource($reflectionClass->name, $reflectionClass->getFileName(), $metadata);
@@ -84,10 +89,11 @@ public static function createResourceForClass(\ReflectionClass $reflectionClass)
8489
*
8590
* @param string $id
8691
* @param Definition $definition
92+
* @param string[] $autowired
8793
*
8894
* @throws RuntimeException
8995
*/
90-
private function completeDefinition($id, Definition $definition)
96+
private function completeDefinition($id, Definition $definition, array $autowired)
9197
{
9298
if (!$reflectionClass = $this->getReflectionClass($id, $definition)) {
9399
return;
@@ -97,12 +103,75 @@ private function completeDefinition($id, Definition $definition)
97103
$this->container->addResource(static::createResourceForClass($reflectionClass));
98104
}
99105

100-
if (!$constructor = $reflectionClass->getConstructor()) {
101-
return;
106+
$methodsCalled = array();
107+
foreach ($definition->getMethodCalls() as $methodCall) {
108+
$methodsCalled[$methodCall[0]] = true;
102109
}
103110

104-
$arguments = $definition->getArguments();
105-
foreach ($constructor->getParameters() as $index => $parameter) {
111+
foreach ($this->getMethodsToAutowire($id, $reflectionClass, $autowired) as $reflectionMethod) {
112+
if (!isset($methodsCalled[$reflectionMethod->name])) {
113+
$this->autowireMethod($id, $definition, $reflectionMethod);
114+
}
115+
}
116+
}
117+
118+
/**
119+
* Gets the list of methods to autowire.
120+
*
121+
* @param string $id
122+
* @param \ReflectionClass $reflectionClass
123+
* @param string[] $autowired
124+
*
125+
* @return \ReflectionMethod[]
126+
*/
127+
private function getMethodsToAutowire($id, \ReflectionClass $reflectionClass, array $autowired)
128+
{
129+
$found = array();
130+
$regexList = array();
131+
foreach ($autowired as $pattern) {
132+
$regexList[] = '/^'.str_replace('\*', '.*', preg_quote($pattern, '/')).'$/i';
133+
}
134+
135+
foreach ($reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $reflectionMethod) {
136+
if ($reflectionMethod->isStatic()) {
137+
continue;
138+
}
139+
140+
foreach ($regexList as $k => $regex) {
141+
if (preg_match($regex, $reflectionMethod->name)) {
142+
$found[] = $autowired[$k];
143+
yield $reflectionMethod;
144+
145+
continue 2;
146+
}
147+
}
148+
}
149+
150+
if ($notFound = array_diff($autowired, $found)) {
151+
$compiler = $this->container->getCompiler();
152+
$compiler->addLogMessage($compiler->getLoggingFormatter()->formatUnusedAutowiringPatterns($this, $id, $notFound));
153+
}
154+
}
155+
156+
/**
157+
* Autowires the constructor or a setter.
158+
*
159+
* @param string $id
160+
* @param Definition $definition
161+
* @param \ReflectionMethod $reflectionMethod
162+
*
163+
* @throws RuntimeException
164+
*/
165+
private function autowireMethod($id, Definition $definition, \ReflectionMethod $reflectionMethod)
166+
{
167+
if ($isConstructor = $reflectionMethod->isConstructor()) {
168+
$arguments = $definition->getArguments();
169+
} else {
170+
$arguments = array();
171+
}
172+
173+
$addMethodCall = false;
174+
foreach ($reflectionMethod->getParameters() as $index => $parameter) {
106175
if (array_key_exists($index, $arguments) && '' !== $arguments[$index]) {
107176
continue;
108177
}
@@ -111,7 +180,11 @@ private function completeDefinition($id, Definition $definition)
111180
if (!$typeHint = $parameter->getClass()) {
112181
// no default value? Then fail
113182
if (!$parameter->isOptional()) {
114-
throw new RuntimeException(sprintf('Unable to autowire argument index %d ($%s) for the service "%s". If this is an object, give it a type-hint. Otherwise, specify this argument\'s value explicitly.', $index, $parameter->name, $id));
183+
if ($isConstructor) {
184+
throw new RuntimeException(sprintf('Unable to autowire argument index %d ($%s) for the service "%s". If this is an object, give it a type-hint. Otherwise, specify this argument\'s value explicitly.', $index, $parameter->name, $id));
185+
}
186+
187+
return;
115188
}
116189

117190
// specifically pass the default value
@@ -126,24 +199,35 @@ private function completeDefinition($id, Definition $definition)
126199

127200
if (isset($this->types[$typeHint->name])) {
128201
$value = new Reference($this->types[$typeHint->name]);
202+
$addMethodCall = true;
129203
} else {
130204
try {
131205
$value = $this->createAutowiredDefinition($typeHint, $id);
206+
$addMethodCall = true;
132207
} catch (RuntimeException $e) {
133208
if ($parameter->allowsNull()) {
134209
$value = null;
135210
} elseif ($parameter->isDefaultValueAvailable()) {
136211
$value = $parameter->getDefaultValue();
137212
} else {
138-
throw $e;
213+
// The exception code is set to 1 if the exception must be thrown even if it's a setter
214+
if (1 === $e->getCode() || $isConstructor) {
215+
throw $e;
216+
}
217+
218+
return;
139219
}
140220
}
141221
}
142222
} catch (\ReflectionException $e) {
143223
// Typehint against a non-existing class
144224

145225
if (!$parameter->isDefaultValueAvailable()) {
146-
throw new RuntimeException(sprintf('Cannot autowire argument %s for %s because the type-hinted class does not exist (%s).', $index + 1, $definition->getClass(), $e->getMessage()), 0, $e);
226+
if ($isConstructor) {
227+
throw new RuntimeException(sprintf('Cannot autowire argument %s for %s because the type-hinted class does not exist (%s).', $index + 1, $definition->getClass(), $e->getMessage()), 0, $e);
228+
}
229+
230+
return;
147231
}
148232

149233
$value = $parameter->getDefaultValue();
@@ -155,7 +239,12 @@ private function completeDefinition($id, Definition $definition)
155239
// it's possible index 1 was set, then index 0, then 2, etc
156240
// make sure that we re-order so they're injected as expected
157241
ksort($arguments);
158-
$definition->setArguments($arguments);
242+
243+
if ($isConstructor) {
244+
$definition->setArguments($arguments);
245+
} elseif ($addMethodCall) {
246+
$definition->addMethodCall($reflectionMethod->name, $arguments);
247+
}
159248
}
160249

161250
/**
@@ -253,7 +342,7 @@ private function createAutowiredDefinition(\ReflectionClass $typeHint, $id)
253342
$classOrInterface = $typeHint->isInterface() ? 'interface' : 'class';
254343
$matchingServices = implode(', ', $this->ambiguousServiceTypes[$typeHint->name]);
255344

256-
throw new RuntimeException(sprintf('Unable to autowire argument of type "%s" for the service "%s". Multiple services exist for this %s (%s).', $typeHint->name, $id, $classOrInterface, $matchingServices));
345+
throw new RuntimeException(sprintf('Unable to autowire argument of type "%s" for the service "%s". Multiple services exist for this %s (%s).', $typeHint->name, $id, $classOrInterface, $matchingServices), 1);
257346
}
258347

259348
if (!$typeHint->isInstantiable()) {
@@ -269,7 +358,7 @@ private function createAutowiredDefinition(\ReflectionClass $typeHint, $id)
269358
$this->populateAvailableType($argumentId, $argumentDefinition);
270359

271360
try {
272-
$this->completeDefinition($argumentId, $argumentDefinition);
361+
$this->completeDefinition($argumentId, $argumentDefinition, array('__construct'));
273362
} catch (RuntimeException $e) {
274363
$classOrInterface = $typeHint->isInterface() ? 'interface' : 'class';
275364
$message = sprintf('Unable to autowire argument of type "%s" for the service "%s". No services were found matching this %s and it cannot be auto-registered.', $typeHint->name, $id, $classOrInterface);
@@ -320,20 +409,6 @@ private function addServiceToAmbiguousType($id, $type)
320409
$this->ambiguousServiceTypes[$type][] = $id;
321410
}
322411

323-
/**
324-
* @param \ReflectionClass $reflectionClass
325-
*
326-
* @return \ReflectionMethod[]
327-
*/
328-
private static function getSetters(\ReflectionClass $reflectionClass)
329-
{
330-
foreach ($reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $reflectionMethod) {
331-
if (!$reflectionMethod->isStatic() && 1 === $reflectionMethod->getNumberOfParameters() && 0 === strpos($reflectionMethod->name, 'set')) {
332-
yield $reflectionMethod;
333-
}
334-
}
335-
}
336-
337412
private static function getResourceMetadataForMethod(\ReflectionMethod $method)
338413
{
339414
$methodArgumentsMetadata = array();

src/Symfony/Component/DependencyInjection/Compiler/LoggingFormatter.php

+5
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ public function formatResolveInheritance(CompilerPassInterface $pass, $childId,
3838
return $this->format($pass, sprintf('Resolving inheritance for "%s" (parent: %s).', $childId, $parentId));
3939
}
4040

41+
public function formatUnusedAutowiringPatterns(CompilerPassInterface $pass, $id, array $patterns)
42+
{
43+
return $this->format($pass, sprintf('Autowiring\'s patterns "%s" for service "%s" don\'t match any method.', implode('", "', $patterns), $id));
44+
}
45+
4146
public function format(CompilerPassInterface $pass, $message)
4247
{
4348
return sprintf('%s: %s', get_class($pass), $message);

src/Symfony/Component/DependencyInjection/Definition.php

+21-3
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ class Definition
3636
private $abstract = false;
3737
private $lazy = false;
3838
private $decoratedService;
39-
private $autowired = false;
39+
private $autowired = array();
4040
private $autowiringTypes = array();
4141

4242
protected $arguments;
@@ -661,20 +661,38 @@ public function setAutowiringTypes(array $types)
661661
* @return bool
662662
*/
663663
public function isAutowired()
664+
{
665+
return !empty($this->autowired);
666+
}
667+
668+
/**
669+
* Gets autowired methods.
670+
*
671+
* @return string[]
672+
*/
673+
public function getAutowired()
664674
{
665675
return $this->autowired;
666676
}
667677

668678
/**
669679
* Sets autowired.
670680
*
671-
* @param bool $autowired
681+
* Allowed values:
682+
* - true: constructor autowiring
683+
* - array('__construct', 'set*', 'setBar'): autowire whitelisted methods only
684+
*
685+
* @param string[]|bool $autowired
672686
*
673687
* @return Definition The current instance
674688
*/
675689
public function setAutowired($autowired)
676690
{
677-
$this->autowired = $autowired;
691+
if (is_array($autowired)) {
692+
$this->autowired = $autowired;
693+
} else {
694+
$this->autowired = $autowired ? array('__construct') : array();
695+
}
678696

679697
return $this;
680698
}

src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php

+13
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,19 @@ private function parseDefinition(\DOMElement $service, $file)
239239
$definition->addAutowiringType($type->textContent);
240240
}
241241

242+
$autowireTags = array();
243+
foreach ($this->getChildren($service, 'autowire') as $type) {
244+
$autowireTags[] = $type->textContent;
245+
}
246+
247+
if (!empty($autowireTags)) {
248+
if ($service->hasAttribute('autowire')) {
249+
throw new InvalidArgumentException(sprintf('The "autowire" attribute cannot be used together with "<autowire>" tags for service "%s" in %s.', (string) $service->getAttribute('id'), $file));
250+
}
251+
252+
$definition->setAutowired($autowireTags);
253+
}
254+
242255
if ($value = $service->getAttribute('decorates')) {
243256
$renameId = $service->hasAttribute('decoration-inner-name') ? $service->getAttribute('decoration-inner-name') : null;
244257
$priority = $service->hasAttribute('decoration-priority') ? $service->getAttribute('decoration-priority') : 0;

src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd

+1
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@
100100
<xsd:element name="tag" type="tag" minOccurs="0" maxOccurs="unbounded" />
101101
<xsd:element name="property" type="property" minOccurs="0" maxOccurs="unbounded" />
102102
<xsd:element name="autowiring-type" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
103+
<xsd:element name="autowire" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
103104
</xsd:choice>
104105
<xsd:attribute name="id" type="xsd:string" />
105106
<xsd:attribute name="class" type="xsd:string" />

0 commit comments

Comments
 (0)