AutowirePass.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\DependencyInjection\Compiler;
  11. use Symfony\Component\Config\Resource\ClassExistenceResource;
  12. use Symfony\Component\DependencyInjection\Config\AutowireServiceResource;
  13. use Symfony\Component\DependencyInjection\ContainerBuilder;
  14. use Symfony\Component\DependencyInjection\Definition;
  15. use Symfony\Component\DependencyInjection\Exception\AutowiringFailedException;
  16. use Symfony\Component\DependencyInjection\Exception\RuntimeException;
  17. use Symfony\Component\DependencyInjection\LazyProxy\ProxyHelper;
  18. use Symfony\Component\DependencyInjection\TypedReference;
  19. /**
  20. * Inspects existing service definitions and wires the autowired ones using the type hints of their classes.
  21. *
  22. * @author Kévin Dunglas <dunglas@gmail.com>
  23. * @author Nicolas Grekas <p@tchwork.com>
  24. */
  25. class AutowirePass extends AbstractRecursivePass
  26. {
  27. private $definedTypes = array();
  28. private $types;
  29. private $ambiguousServiceTypes = array();
  30. private $autowired = array();
  31. private $lastFailure;
  32. private $throwOnAutowiringException;
  33. private $autowiringExceptions = array();
  34. /**
  35. * @param bool $throwOnAutowireException If false, retrieved errors via getAutowiringExceptions
  36. */
  37. public function __construct($throwOnAutowireException = true)
  38. {
  39. $this->throwOnAutowiringException = $throwOnAutowireException;
  40. }
  41. /**
  42. * @return AutowiringFailedException[]
  43. */
  44. public function getAutowiringExceptions()
  45. {
  46. return $this->autowiringExceptions;
  47. }
  48. /**
  49. * {@inheritdoc}
  50. */
  51. public function process(ContainerBuilder $container)
  52. {
  53. // clear out any possibly stored exceptions from before
  54. $this->autowiringExceptions = array();
  55. try {
  56. parent::process($container);
  57. } finally {
  58. $this->definedTypes = array();
  59. $this->types = null;
  60. $this->ambiguousServiceTypes = array();
  61. $this->autowired = array();
  62. }
  63. }
  64. /**
  65. * Creates a resource to help know if this service has changed.
  66. *
  67. * @param \ReflectionClass $reflectionClass
  68. *
  69. * @return AutowireServiceResource
  70. *
  71. * @deprecated since version 3.3, to be removed in 4.0. Use ContainerBuilder::getReflectionClass() instead.
  72. */
  73. public static function createResourceForClass(\ReflectionClass $reflectionClass)
  74. {
  75. @trigger_error('The '.__METHOD__.'() method is deprecated since Symfony 3.3 and will be removed in 4.0. Use ContainerBuilder::getReflectionClass() instead.', E_USER_DEPRECATED);
  76. $metadata = array();
  77. foreach ($reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $reflectionMethod) {
  78. if (!$reflectionMethod->isStatic()) {
  79. $metadata[$reflectionMethod->name] = self::getResourceMetadataForMethod($reflectionMethod);
  80. }
  81. }
  82. return new AutowireServiceResource($reflectionClass->name, $reflectionClass->getFileName(), $metadata);
  83. }
  84. /**
  85. * {@inheritdoc}
  86. */
  87. protected function processValue($value, $isRoot = false)
  88. {
  89. try {
  90. return $this->doProcessValue($value, $isRoot);
  91. } catch (AutowiringFailedException $e) {
  92. if ($this->throwOnAutowiringException) {
  93. throw $e;
  94. }
  95. $this->autowiringExceptions[] = $e;
  96. return parent::processValue($value, $isRoot);
  97. }
  98. }
  99. private function doProcessValue($value, $isRoot = false)
  100. {
  101. if ($value instanceof TypedReference) {
  102. if ($ref = $this->getAutowiredReference($value, $value->getRequiringClass() ? sprintf('for "%s" in "%s"', $value->getType(), $value->getRequiringClass()) : '')) {
  103. return $ref;
  104. }
  105. $this->container->log($this, $this->createTypeNotFoundMessage($value, 'it'));
  106. }
  107. $value = parent::processValue($value, $isRoot);
  108. if (!$value instanceof Definition || !$value->isAutowired() || $value->isAbstract() || !$value->getClass()) {
  109. return $value;
  110. }
  111. if (!$reflectionClass = $this->container->getReflectionClass($value->getClass(), false)) {
  112. $this->container->log($this, sprintf('Skipping service "%s": Class or interface "%s" cannot be loaded.', $this->currentId, $value->getClass()));
  113. return $value;
  114. }
  115. $autowiredMethods = $this->getMethodsToAutowire($reflectionClass);
  116. $methodCalls = $value->getMethodCalls();
  117. try {
  118. $constructor = $this->getConstructor($value, false);
  119. } catch (RuntimeException $e) {
  120. throw new AutowiringFailedException($this->currentId, $e->getMessage(), 0, $e);
  121. }
  122. if ($constructor) {
  123. array_unshift($methodCalls, array($constructor, $value->getArguments()));
  124. }
  125. $methodCalls = $this->autowireCalls($reflectionClass, $methodCalls, $autowiredMethods);
  126. if ($constructor) {
  127. list(, $arguments) = array_shift($methodCalls);
  128. if ($arguments !== $value->getArguments()) {
  129. $value->setArguments($arguments);
  130. }
  131. }
  132. if ($methodCalls !== $value->getMethodCalls()) {
  133. $value->setMethodCalls($methodCalls);
  134. }
  135. return $value;
  136. }
  137. /**
  138. * Gets the list of methods to autowire.
  139. *
  140. * @param \ReflectionClass $reflectionClass
  141. *
  142. * @return \ReflectionMethod[]
  143. */
  144. private function getMethodsToAutowire(\ReflectionClass $reflectionClass)
  145. {
  146. $methodsToAutowire = array();
  147. foreach ($reflectionClass->getMethods() as $reflectionMethod) {
  148. $r = $reflectionMethod;
  149. if ($r->isConstructor()) {
  150. continue;
  151. }
  152. while (true) {
  153. if (false !== $doc = $r->getDocComment()) {
  154. if (false !== stripos($doc, '@required') && preg_match('#(?:^/\*\*|\n\s*+\*)\s*+@required(?:\s|\*/$)#i', $doc)) {
  155. $methodsToAutowire[strtolower($reflectionMethod->name)] = $reflectionMethod;
  156. break;
  157. }
  158. if (false === stripos($doc, '@inheritdoc') || !preg_match('#(?:^/\*\*|\n\s*+\*)\s*+(?:\{@inheritdoc\}|@inheritdoc)(?:\s|\*/$)#i', $doc)) {
  159. break;
  160. }
  161. }
  162. try {
  163. $r = $r->getPrototype();
  164. } catch (\ReflectionException $e) {
  165. break; // method has no prototype
  166. }
  167. }
  168. }
  169. return $methodsToAutowire;
  170. }
  171. /**
  172. * @param \ReflectionClass $reflectionClass
  173. * @param array $methodCalls
  174. * @param \ReflectionMethod[] $autowiredMethods
  175. *
  176. * @return array
  177. */
  178. private function autowireCalls(\ReflectionClass $reflectionClass, array $methodCalls, array $autowiredMethods)
  179. {
  180. foreach ($methodCalls as $i => $call) {
  181. list($method, $arguments) = $call;
  182. if ($method instanceof \ReflectionFunctionAbstract) {
  183. $reflectionMethod = $method;
  184. } elseif (isset($autowiredMethods[$lcMethod = strtolower($method)]) && $autowiredMethods[$lcMethod]->isPublic()) {
  185. $reflectionMethod = $autowiredMethods[$lcMethod];
  186. unset($autowiredMethods[$lcMethod]);
  187. } else {
  188. $reflectionMethod = $this->getReflectionMethod(new Definition($reflectionClass->name), $method);
  189. }
  190. $arguments = $this->autowireMethod($reflectionMethod, $arguments);
  191. if ($arguments !== $call[1]) {
  192. $methodCalls[$i][1] = $arguments;
  193. }
  194. }
  195. foreach ($autowiredMethods as $lcMethod => $reflectionMethod) {
  196. $method = $reflectionMethod->name;
  197. if (!$reflectionMethod->isPublic()) {
  198. $class = $reflectionClass->name;
  199. throw new AutowiringFailedException($this->currentId, sprintf('Cannot autowire service "%s": method "%s()" must be public.', $this->currentId, $class !== $this->currentId ? $class.'::'.$method : $method));
  200. }
  201. $methodCalls[] = array($method, $this->autowireMethod($reflectionMethod, array()));
  202. }
  203. return $methodCalls;
  204. }
  205. /**
  206. * Autowires the constructor or a method.
  207. *
  208. * @param \ReflectionFunctionAbstract $reflectionMethod
  209. * @param array $arguments
  210. *
  211. * @return array The autowired arguments
  212. *
  213. * @throws AutowiringFailedException
  214. */
  215. private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, array $arguments)
  216. {
  217. $class = $reflectionMethod instanceof \ReflectionMethod ? $reflectionMethod->class : $this->currentId;
  218. $method = $reflectionMethod->name;
  219. $parameters = $reflectionMethod->getParameters();
  220. if (method_exists('ReflectionMethod', 'isVariadic') && $reflectionMethod->isVariadic()) {
  221. array_pop($parameters);
  222. }
  223. foreach ($parameters as $index => $parameter) {
  224. if (array_key_exists($index, $arguments) && '' !== $arguments[$index]) {
  225. continue;
  226. }
  227. $type = ProxyHelper::getTypeHint($reflectionMethod, $parameter, true);
  228. if (!$type) {
  229. if (isset($arguments[$index])) {
  230. continue;
  231. }
  232. // no default value? Then fail
  233. if (!$parameter->isDefaultValueAvailable()) {
  234. // For core classes, isDefaultValueAvailable() can
  235. // be false when isOptional() returns true. If the
  236. // argument *is* optional, allow it to be missing
  237. if ($parameter->isOptional()) {
  238. continue;
  239. }
  240. throw new AutowiringFailedException($this->currentId, sprintf('Cannot autowire service "%s": argument "$%s" of method "%s()" must have a type-hint or be given a value explicitly.', $this->currentId, $parameter->name, $class !== $this->currentId ? $class.'::'.$method : $method));
  241. }
  242. // specifically pass the default value
  243. $arguments[$index] = $parameter->getDefaultValue();
  244. continue;
  245. }
  246. if (!$value = $this->getAutowiredReference($ref = new TypedReference($type, $type, !$parameter->isOptional() ? $class : ''), 'for '.sprintf('argument "$%s" of method "%s()"', $parameter->name, $class.'::'.$method))) {
  247. $failureMessage = $this->createTypeNotFoundMessage($ref, sprintf('argument "$%s" of method "%s()"', $parameter->name, $class !== $this->currentId ? $class.'::'.$method : $method));
  248. if ($parameter->isDefaultValueAvailable()) {
  249. $value = $parameter->getDefaultValue();
  250. } elseif (!$parameter->allowsNull()) {
  251. throw new AutowiringFailedException($this->currentId, $failureMessage);
  252. }
  253. $this->container->log($this, $failureMessage);
  254. }
  255. $arguments[$index] = $value;
  256. }
  257. if ($parameters && !isset($arguments[++$index])) {
  258. while (0 <= --$index) {
  259. $parameter = $parameters[$index];
  260. if (!$parameter->isDefaultValueAvailable() || $parameter->getDefaultValue() !== $arguments[$index]) {
  261. break;
  262. }
  263. unset($arguments[$index]);
  264. }
  265. }
  266. // it's possible index 1 was set, then index 0, then 2, etc
  267. // make sure that we re-order so they're injected as expected
  268. ksort($arguments);
  269. return $arguments;
  270. }
  271. /**
  272. * @return TypedReference|null A reference to the service matching the given type, if any
  273. */
  274. private function getAutowiredReference(TypedReference $reference, $deprecationMessage)
  275. {
  276. $this->lastFailure = null;
  277. $type = $reference->getType();
  278. if ($type !== (string) $reference || ($this->container->has($type) && !$this->container->findDefinition($type)->isAbstract())) {
  279. return $reference;
  280. }
  281. if (null === $this->types) {
  282. $this->populateAvailableTypes();
  283. }
  284. if (isset($this->definedTypes[$type])) {
  285. return new TypedReference($this->types[$type], $type);
  286. }
  287. if (isset($this->types[$type])) {
  288. $message = 'Autowiring services based on the types they implement is deprecated since Symfony 3.3 and won\'t be supported in version 4.0.';
  289. if ($aliasSuggestion = $this->getAliasesSuggestionForType($type = $reference->getType(), $deprecationMessage)) {
  290. $message .= ' '.$aliasSuggestion;
  291. } else {
  292. $message .= sprintf(' You should %s the "%s" service to "%s" instead.', isset($this->types[$this->types[$type]]) ? 'alias' : 'rename (or alias)', $this->types[$type], $type);
  293. }
  294. @trigger_error($message, E_USER_DEPRECATED);
  295. return new TypedReference($this->types[$type], $type);
  296. }
  297. if (!$reference->canBeAutoregistered() || isset($this->types[$type]) || isset($this->ambiguousServiceTypes[$type])) {
  298. return;
  299. }
  300. if (isset($this->autowired[$type])) {
  301. return $this->autowired[$type] ? new TypedReference($this->autowired[$type], $type) : null;
  302. }
  303. return $this->createAutowiredDefinition($type);
  304. }
  305. /**
  306. * Populates the list of available types.
  307. */
  308. private function populateAvailableTypes()
  309. {
  310. $this->types = array();
  311. foreach ($this->container->getDefinitions() as $id => $definition) {
  312. $this->populateAvailableType($id, $definition);
  313. }
  314. }
  315. /**
  316. * Populates the list of available types for a given definition.
  317. *
  318. * @param string $id
  319. * @param Definition $definition
  320. */
  321. private function populateAvailableType($id, Definition $definition)
  322. {
  323. // Never use abstract services
  324. if ($definition->isAbstract()) {
  325. return;
  326. }
  327. foreach ($definition->getAutowiringTypes(false) as $type) {
  328. $this->definedTypes[$type] = true;
  329. $this->types[$type] = $id;
  330. unset($this->ambiguousServiceTypes[$type]);
  331. }
  332. if ($definition->isDeprecated() || !$reflectionClass = $this->container->getReflectionClass($definition->getClass(), false)) {
  333. return;
  334. }
  335. foreach ($reflectionClass->getInterfaces() as $reflectionInterface) {
  336. $this->set($reflectionInterface->name, $id);
  337. }
  338. do {
  339. $this->set($reflectionClass->name, $id);
  340. } while ($reflectionClass = $reflectionClass->getParentClass());
  341. }
  342. /**
  343. * Associates a type and a service id if applicable.
  344. *
  345. * @param string $type
  346. * @param string $id
  347. */
  348. private function set($type, $id)
  349. {
  350. if (isset($this->definedTypes[$type])) {
  351. return;
  352. }
  353. // is this already a type/class that is known to match multiple services?
  354. if (isset($this->ambiguousServiceTypes[$type])) {
  355. $this->ambiguousServiceTypes[$type][] = $id;
  356. return;
  357. }
  358. // check to make sure the type doesn't match multiple services
  359. if (!isset($this->types[$type]) || $this->types[$type] === $id) {
  360. $this->types[$type] = $id;
  361. return;
  362. }
  363. // keep an array of all services matching this type
  364. if (!isset($this->ambiguousServiceTypes[$type])) {
  365. $this->ambiguousServiceTypes[$type] = array($this->types[$type]);
  366. unset($this->types[$type]);
  367. }
  368. $this->ambiguousServiceTypes[$type][] = $id;
  369. }
  370. /**
  371. * Registers a definition for the type if possible or throws an exception.
  372. *
  373. * @param string $type
  374. *
  375. * @return TypedReference|null A reference to the registered definition
  376. */
  377. private function createAutowiredDefinition($type)
  378. {
  379. if (!($typeHint = $this->container->getReflectionClass($type, false)) || !$typeHint->isInstantiable()) {
  380. return;
  381. }
  382. $currentId = $this->currentId;
  383. $this->currentId = $type;
  384. $this->autowired[$type] = $argumentId = sprintf('autowired.%s', $type);
  385. $argumentDefinition = new Definition($type);
  386. $argumentDefinition->setPublic(false);
  387. $argumentDefinition->setAutowired(true);
  388. try {
  389. $originalThrowSetting = $this->throwOnAutowiringException;
  390. $this->throwOnAutowiringException = true;
  391. $this->processValue($argumentDefinition, true);
  392. $this->container->setDefinition($argumentId, $argumentDefinition);
  393. } catch (AutowiringFailedException $e) {
  394. $this->autowired[$type] = false;
  395. $this->lastFailure = $e->getMessage();
  396. $this->container->log($this, $this->lastFailure);
  397. return;
  398. } finally {
  399. $this->throwOnAutowiringException = $originalThrowSetting;
  400. $this->currentId = $currentId;
  401. }
  402. $this->container->log($this, sprintf('Type "%s" has been auto-registered for service "%s".', $type, $this->currentId));
  403. return new TypedReference($argumentId, $type);
  404. }
  405. private function createTypeNotFoundMessage(TypedReference $reference, $label)
  406. {
  407. if (!$r = $this->container->getReflectionClass($type = $reference->getType(), false)) {
  408. // either $type does not exist or a parent class does not exist
  409. try {
  410. $resource = new ClassExistenceResource($type, false);
  411. // isFresh() will explode ONLY if a parent class/trait does not exist
  412. $resource->isFresh(0);
  413. $parentMsg = false;
  414. } catch (\ReflectionException $e) {
  415. $parentMsg = $e->getMessage();
  416. }
  417. $message = sprintf('has type "%s" but this class %s.', $type, $parentMsg ? sprintf('is missing a parent class (%s)', $parentMsg) : 'was not found');
  418. } else {
  419. $message = $this->container->has($type) ? 'this service is abstract' : 'no such service exists';
  420. $message = sprintf('references %s "%s" but %s.%s', $r->isInterface() ? 'interface' : 'class', $type, $message, $this->createTypeAlternatives($reference));
  421. }
  422. $message = sprintf('Cannot autowire service "%s": %s %s', $this->currentId, $label, $message);
  423. if (null !== $this->lastFailure) {
  424. $message = $this->lastFailure."\n".$message;
  425. $this->lastFailure = null;
  426. }
  427. return $message;
  428. }
  429. private function createTypeAlternatives(TypedReference $reference)
  430. {
  431. // try suggesting available aliases first
  432. if ($message = $this->getAliasesSuggestionForType($type = $reference->getType())) {
  433. return ' '.$message;
  434. }
  435. if (isset($this->ambiguousServiceTypes[$type])) {
  436. $message = sprintf('one of these existing services: "%s"', implode('", "', $this->ambiguousServiceTypes[$type]));
  437. } elseif (isset($this->types[$type])) {
  438. $message = sprintf('the existing "%s" service', $this->types[$type]);
  439. } elseif ($reference->getRequiringClass() && !$reference->canBeAutoregistered()) {
  440. return ' It cannot be auto-registered because it is from a different root namespace.';
  441. } else {
  442. return;
  443. }
  444. return sprintf(' You should maybe alias this %s to %s.', class_exists($type, false) ? 'class' : 'interface', $message);
  445. }
  446. /**
  447. * @deprecated since version 3.3, to be removed in 4.0.
  448. */
  449. private static function getResourceMetadataForMethod(\ReflectionMethod $method)
  450. {
  451. $methodArgumentsMetadata = array();
  452. foreach ($method->getParameters() as $parameter) {
  453. try {
  454. $class = $parameter->getClass();
  455. } catch (\ReflectionException $e) {
  456. // type-hint is against a non-existent class
  457. $class = false;
  458. }
  459. $isVariadic = method_exists($parameter, 'isVariadic') && $parameter->isVariadic();
  460. $methodArgumentsMetadata[] = array(
  461. 'class' => $class,
  462. 'isOptional' => $parameter->isOptional(),
  463. 'defaultValue' => ($parameter->isOptional() && !$isVariadic) ? $parameter->getDefaultValue() : null,
  464. );
  465. }
  466. return $methodArgumentsMetadata;
  467. }
  468. private function getAliasesSuggestionForType($type, $extraContext = null)
  469. {
  470. $aliases = array();
  471. foreach (class_parents($type) + class_implements($type) as $parent) {
  472. if ($this->container->has($parent) && !$this->container->findDefinition($parent)->isAbstract()) {
  473. $aliases[] = $parent;
  474. }
  475. }
  476. $extraContext = $extraContext ? ' '.$extraContext : '';
  477. if (1 < $len = count($aliases)) {
  478. $message = sprintf('Try changing the type-hint%s to one of its parents: ', $extraContext);
  479. for ($i = 0, --$len; $i < $len; ++$i) {
  480. $message .= sprintf('%s "%s", ', class_exists($aliases[$i], false) ? 'class' : 'interface', $aliases[$i]);
  481. }
  482. $message .= sprintf('or %s "%s".', class_exists($aliases[$i], false) ? 'class' : 'interface', $aliases[$i]);
  483. return $message;
  484. }
  485. if ($aliases) {
  486. return sprintf('Try changing the type-hint%s to "%s" instead.', $extraContext, $aliases[0]);
  487. }
  488. }
  489. }