PluginProbe
Packeta / trunk
Packeta vtrunk
2.3.2 2.3.1 trunk 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3.0 1.3.1 1.3.2 1.4 1.4.1 1.4.2 1.4.3 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 All 56 releases
packeta / deps / nette / di / src / DI / Resolver.php

Resolver.php in Packeta trunk, at deps/nette/di/src/DI/Resolver.php

503 lines 24.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * This file is part of the Nette Framework (https://nette.org)
5 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
6 */
7 declare (strict_types=1);
8 namespace Packetery\Nette\DI;
9
10 use Packetery\Nette;
11 use Packetery\Nette\DI\Definitions\Definition;
12 use Packetery\Nette\DI\Definitions\Reference;
13 use Packetery\Nette\DI\Definitions\Statement;
14 use Packetery\Nette\PhpGenerator\Helpers as PhpHelpers;
15 use Packetery\Nette\Utils\Arrays;
16 use Packetery\Nette\Utils\Callback;
17 use Packetery\Nette\Utils\Reflection;
18 use Packetery\Nette\Utils\Strings;
19 use Packetery\Nette\Utils\Validators;
20 /**
21 * Services resolver
22 * @internal
23 */
24 class Resolver
25 {
26 use \Packetery\Nette\SmartObject;
27 /** @var ContainerBuilder */
28 private $builder;
29 /** @var Definition|null */
30 private $currentService;
31 /** @var string|null */
32 private $currentServiceType;
33 /** @var bool */
34 private $currentServiceAllowed = \false;
35 /** @var \SplObjectStorage circular reference detector */
36 private $recursive;
37 public function __construct(ContainerBuilder $builder)
38 {
39 $this->builder = $builder;
40 $this->recursive = new \SplObjectStorage();
41 }
42 public function getContainerBuilder() : ContainerBuilder
43 {
44 return $this->builder;
45 }
46 public function resolveDefinition(Definition $def) : void
47 {
48 if ($this->recursive->contains($def)) {
49 $names = \array_map(function ($item) {
50 return $item->getName();
51 }, \iterator_to_array($this->recursive));
52 throw new ServiceCreationException(\sprintf('Circular reference detected for services: %s.', \implode(', ', $names)));
53 }
54 try {
55 $this->recursive->attach($def);
56 $def->resolveType($this);
57 if (!$def->getType()) {
58 throw new ServiceCreationException('Type of service is unknown.');
59 }
60 } catch (\Throwable $e) {
61 throw $this->completeException($e, $def);
62 } finally {
63 $this->recursive->detach($def);
64 }
65 }
66 public function resolveReferenceType(Reference $ref) : ?string
67 {
68 if ($ref->isSelf()) {
69 return $this->currentServiceType;
70 } elseif ($ref->isType()) {
71 return \ltrim($ref->getValue(), '\\');
72 }
73 $def = $this->resolveReference($ref);
74 if (!$def->getType()) {
75 $this->resolveDefinition($def);
76 }
77 return $def->getType();
78 }
79 public function resolveEntityType(Statement $statement) : ?string
80 {
81 $entity = $this->normalizeEntity($statement);
82 if (\is_array($entity)) {
83 if ($entity[0] instanceof Reference || $entity[0] instanceof Statement) {
84 $entity[0] = $this->resolveEntityType($entity[0] instanceof Statement ? $entity[0] : new Statement($entity[0]));
85 if (!$entity[0]) {
86 return null;
87 }
88 }
89 try {
90 $reflection = Callback::toReflection($entity[0] === '' ? $entity[1] : $entity);
91 \assert($reflection instanceof \ReflectionMethod || $reflection instanceof \ReflectionFunction);
92 $refClass = $reflection instanceof \ReflectionMethod ? $reflection->getDeclaringClass() : null;
93 } catch (\ReflectionException $e) {
94 $refClass = $reflection = null;
95 }
96 if (isset($e) || $refClass && (!$reflection->isPublic() || $refClass->isTrait() && !$reflection->isStatic())) {
97 throw new ServiceCreationException(\sprintf('Method %s() is not callable.', Callback::toString($entity)), 0, $e ?? null);
98 }
99 $this->addDependency($reflection);
100 $type = \Packetery\Nette\Utils\Type::fromReflection($reflection) ?? ($annotation = Helpers::getReturnTypeAnnotation($reflection));
101 if ($type && !\in_array($type->getSingleName(), ['object', 'mixed'], \true)) {
102 if (isset($annotation)) {
103 \trigger_error('Annotation @return should be replaced with native return type at ' . Callback::toString($entity), \E_USER_DEPRECATED);
104 }
105 return Helpers::ensureClassType($type, \sprintf('return type of %s()', Callback::toString($entity)), \true);
106 }
107 return null;
108 } elseif ($entity instanceof Reference) {
109 // alias or factory
110 return $this->resolveReferenceType($entity);
111 } elseif (\is_string($entity)) {
112 // class
113 if (!\class_exists($entity)) {
114 throw new ServiceCreationException(\sprintf(\interface_exists($entity) ? "Interface %s can not be used as 'create' or 'factory', did you mean 'implement'?" : "Class '%s' not found.", $entity));
115 }
116 return $entity;
117 }
118 return null;
119 }
120 public function completeDefinition(Definition $def) : void
121 {
122 $this->currentService = \in_array($def, $this->builder->getDefinitions(), \true) ? $def : null;
123 $this->currentServiceType = $def->getType();
124 $this->currentServiceAllowed = \false;
125 try {
126 $def->complete($this);
127 $this->addDependency(new \ReflectionClass($def->getType()));
128 } catch (\Throwable $e) {
129 throw $this->completeException($e, $def);
130 } finally {
131 $this->currentService = $this->currentServiceType = null;
132 }
133 }
134 public function completeStatement(Statement $statement, bool $currentServiceAllowed = \false) : Statement
135 {
136 $this->currentServiceAllowed = $currentServiceAllowed;
137 $entity = $this->normalizeEntity($statement);
138 $arguments = $this->convertReferences($statement->arguments);
139 $getter = function (string $type, bool $single) {
140 return $single ? $this->getByType($type) : \array_values(\array_filter($this->builder->findAutowired($type), function ($obj) {
141 return $obj !== $this->currentService;
142 }));
143 };
144 switch (\true) {
145 case \is_string($entity) && Strings::contains($entity, '?'):
146 // PHP literal
147 break;
148 case $entity === 'not':
149 if (\count($arguments) !== 1) {
150 throw new ServiceCreationException(\sprintf('Function %s() expects 1 parameter, %s given.', $entity, \count($arguments)));
151 }
152 $entity = ['', '!'];
153 break;
154 case $entity === 'bool':
155 case $entity === 'int':
156 case $entity === 'float':
157 case $entity === 'string':
158 if (\count($arguments) !== 1) {
159 throw new ServiceCreationException(\sprintf('Function %s() expects 1 parameter, %s given.', $entity, \count($arguments)));
160 }
161 $arguments = [$arguments[0], $entity];
162 $entity = [Helpers::class, 'convertType'];
163 break;
164 case \is_string($entity):
165 // create class
166 if (!\class_exists($entity)) {
167 throw new ServiceCreationException(\sprintf("Class '%s' not found.", $entity));
168 } elseif ((new \ReflectionClass($entity))->isAbstract()) {
169 throw new ServiceCreationException(\sprintf('Class %s is abstract.', $entity));
170 } elseif (($rm = (new \ReflectionClass($entity))->getConstructor()) !== null && !$rm->isPublic()) {
171 throw new ServiceCreationException(\sprintf('Class %s has %s constructor.', $entity, $rm->isProtected() ? 'protected' : 'private'));
172 } elseif ($constructor = (new \ReflectionClass($entity))->getConstructor()) {
173 $arguments = self::autowireArguments($constructor, $arguments, $getter);
174 $this->addDependency($constructor);
175 } elseif ($arguments) {
176 throw new ServiceCreationException(\sprintf('Unable to pass arguments, class %s has no constructor.', $entity));
177 }
178 break;
179 case $entity instanceof Reference:
180 $entity = [new Reference(ContainerBuilder::ThisContainer), Container::getMethodName($entity->getValue())];
181 break;
182 case \is_array($entity):
183 if (!\preg_match('#^\\$?(\\\\?' . PhpHelpers::PHP_IDENT . ')+(\\[\\])?$#D', $entity[1])) {
184 throw new ServiceCreationException(\sprintf("Expected function, method or property name, '%s' given.", $entity[1]));
185 }
186 switch (\true) {
187 case $entity[0] === '':
188 // function call
189 if (!Arrays::isList($arguments)) {
190 throw new ServiceCreationException(\sprintf('Unable to pass specified arguments to %s.', $entity[0]));
191 } elseif (!\function_exists($entity[1])) {
192 throw new ServiceCreationException(\sprintf("Function %s doesn't exist.", $entity[1]));
193 }
194 $rf = new \ReflectionFunction($entity[1]);
195 $arguments = self::autowireArguments($rf, $arguments, $getter);
196 $this->addDependency($rf);
197 break;
198 case $entity[0] instanceof Statement:
199 $entity[0] = $this->completeStatement($entity[0], $this->currentServiceAllowed);
200 // break omitted
201 case \is_string($entity[0]):
202 // static method call
203 case $entity[0] instanceof Reference:
204 if ($entity[1][0] === '$') {
205 // property getter, setter or appender
206 Validators::assert($arguments, 'list:0..1', "setup arguments for '" . Callback::toString($entity) . "'");
207 if (!$arguments && \substr($entity[1], -2) === '[]') {
208 throw new ServiceCreationException(\sprintf('Missing argument for %s.', $entity[1]));
209 }
210 } elseif ($type = $entity[0] instanceof Reference ? $this->resolveReferenceType($entity[0]) : $this->resolveEntityType($entity[0] instanceof Statement ? $entity[0] : new Statement($entity[0]))) {
211 $rc = new \ReflectionClass($type);
212 if ($rc->hasMethod($entity[1])) {
213 $rm = $rc->getMethod($entity[1]);
214 if (!$rm->isPublic()) {
215 throw new ServiceCreationException(\sprintf('%s::%s() is not callable.', $type, $entity[1]));
216 }
217 $arguments = self::autowireArguments($rm, $arguments, $getter);
218 $this->addDependency($rm);
219 } elseif (!Arrays::isList($arguments)) {
220 throw new ServiceCreationException(\sprintf('Unable to pass specified arguments to %s::%s().', $type, $entity[1]));
221 }
222 }
223 }
224 }
225 try {
226 $arguments = $this->completeArguments($arguments);
227 } catch (ServiceCreationException $e) {
228 if (!\strpos($e->getMessage(), ' (used in')) {
229 $e->setMessage($e->getMessage() . " (used in {$this->entityToString($entity)})");
230 }
231 throw $e;
232 }
233 return new Statement($entity, $arguments);
234 }
235 public function completeArguments(array $arguments) : array
236 {
237 \array_walk_recursive($arguments, function (&$val) : void {
238 if ($val instanceof Statement) {
239 $entity = $val->getEntity();
240 if ($entity === 'typed' || $entity === 'tagged') {
241 $services = [];
242 $current = $this->currentService ? $this->currentService->getName() : null;
243 foreach ($val->arguments as $argument) {
244 foreach ($entity === 'tagged' ? $this->builder->findByTag($argument) : $this->builder->findAutowired($argument) as $name => $foo) {
245 if ($name !== $current) {
246 $services[] = new Reference($name);
247 }
248 }
249 }
250 $val = $this->completeArguments($services);
251 } else {
252 $val = $this->completeStatement($val, $this->currentServiceAllowed);
253 }
254 } elseif ($val instanceof Definition || $val instanceof Reference) {
255 $val = $this->normalizeEntity(new Statement($val));
256 }
257 });
258 return $arguments;
259 }
260 /** @return string|array|Reference literal, Class, Reference, [Class, member], [, globalFunc], [Reference, member], [Statement, member] */
261 private function normalizeEntity(Statement $statement)
262 {
263 $entity = $statement->getEntity();
264 if (\is_array($entity)) {
265 $item =& $entity[0];
266 } else {
267 $item =& $entity;
268 }
269 if ($item instanceof Definition) {
270 $name = \current(\array_keys($this->builder->getDefinitions(), $item, \true));
271 if ($name === \false) {
272 throw new ServiceCreationException(\sprintf("Service '%s' not found in definitions.", $item->getName()));
273 }
274 $item = new Reference($name);
275 }
276 if ($item instanceof Reference) {
277 $item = $this->normalizeReference($item);
278 }
279 return $entity;
280 }
281 /**
282 * Normalizes reference to 'self' or named reference (or leaves it typed if it is not possible during resolving) and checks existence of service.
283 */
284 public function normalizeReference(Reference $ref) : Reference
285 {
286 $service = $ref->getValue();
287 if ($ref->isSelf()) {
288 return $ref;
289 } elseif ($ref->isName()) {
290 if (!$this->builder->hasDefinition($service)) {
291 throw new ServiceCreationException(\sprintf("Reference to missing service '%s'.", $service));
292 }
293 return $this->currentService && $service === $this->currentService->getName() ? new Reference(Reference::Self) : $ref;
294 }
295 try {
296 return $this->getByType($service);
297 } catch (NotAllowedDuringResolvingException $e) {
298 return new Reference($service);
299 }
300 }
301 public function resolveReference(Reference $ref) : Definition
302 {
303 return $ref->isSelf() ? $this->currentService : $this->builder->getDefinition($ref->getValue());
304 }
305 /**
306 * Returns named reference to service resolved by type (or 'self' reference for local-autowiring).
307 * @throws ServiceCreationException when multiple found
308 * @throws MissingServiceException when not found
309 */
310 public function getByType(string $type) : Reference
311 {
312 if ($this->currentService && $this->currentServiceAllowed && \is_a($this->currentServiceType, $type, \true)) {
313 return new Reference(Reference::Self);
314 }
315 $name = $this->builder->getByType($type, \true);
316 if (!$this->currentServiceAllowed && $this->currentService === $this->builder->getDefinition($name)) {
317 throw new MissingServiceException();
318 }
319 return new Reference($name);
320 }
321 /**
322 * Adds item to the list of dependencies.
323 * @param \ReflectionClass|\ReflectionFunctionAbstract|string $dep
324 * @return static
325 */
326 public function addDependency($dep)
327 {
328 $this->builder->addDependency($dep);
329 return $this;
330 }
331 private function completeException(\Throwable $e, Definition $def) : ServiceCreationException
332 {
333 if ($e instanceof ServiceCreationException && Strings::startsWith($e->getMessage(), "Service '")) {
334 return $e;
335 }
336 $name = $def->getName();
337 $type = $def->getType();
338 if ($name && !\ctype_digit($name)) {
339 $message = "Service '{$name}'" . ($type ? " (type of {$type})" : '') . ': ';
340 } elseif ($type) {
341 $message = "Service of type {$type}: ";
342 } elseif ($def instanceof Definitions\ServiceDefinition && $def->getEntity()) {
343 $message = 'Service (' . $this->entityToString($def->getEntity()) . '): ';
344 } else {
345 $message = '';
346 }
347 $message .= $type ? \str_replace("{$type}::", \preg_replace('~.*\\\\~', '', $type) . '::', $e->getMessage()) : $e->getMessage();
348 return $e instanceof ServiceCreationException ? $e->setMessage($message) : new ServiceCreationException($message, 0, $e);
349 }
350 private function entityToString($entity) : string
351 {
352 $referenceToText = function (Reference $ref) : string {
353 return $ref->isSelf() && $this->currentService ? '@' . $this->currentService->getName() : '@' . $ref->getValue();
354 };
355 if (\is_string($entity)) {
356 return $entity . '::__construct()';
357 } elseif ($entity instanceof Reference) {
358 $entity = $referenceToText($entity);
359 } elseif (\is_array($entity)) {
360 if (\strpos($entity[1], '$') === \false) {
361 $entity[1] .= '()';
362 }
363 if ($entity[0] instanceof Reference) {
364 $entity[0] = $referenceToText($entity[0]);
365 } elseif (!\is_string($entity[0])) {
366 return $entity[1];
367 }
368 return \implode('::', $entity);
369 }
370 return (string) $entity;
371 }
372 private function convertReferences(array $arguments) : array
373 {
374 \array_walk_recursive($arguments, function (&$val) : void {
375 if (\is_string($val) && \strlen($val) > 1 && $val[0] === '@' && $val[1] !== '@') {
376 $pair = \explode('::', \substr($val, 1), 2);
377 if (!isset($pair[1])) {
378 // @service
379 $val = new Reference($pair[0]);
380 } elseif (\preg_match('#^[A-Z][a-zA-Z0-9_]*$#D', $pair[1])) {
381 // @service::CONSTANT
382 $val = ContainerBuilder::literal($this->resolveReferenceType(new Reference($pair[0])) . '::' . $pair[1]);
383 } else {
384 // @service::property
385 $val = new Statement([new Reference($pair[0]), '$' . $pair[1]]);
386 }
387 } elseif (\is_string($val) && \substr($val, 0, 2) === '@@') {
388 // escaped text @@
389 $val = \substr($val, 1);
390 }
391 });
392 return $arguments;
393 }
394 /**
395 * Add missing arguments using autowiring.
396 * @param (callable(string $type, bool $single): (object|object[]|null)) $getter
397 * @throws ServiceCreationException
398 */
399 public static function autowireArguments(\ReflectionFunctionAbstract $method, array $arguments, callable $getter) : array
400 {
401 $optCount = 0;
402 $useName = \false;
403 $num = -1;
404 $res = [];
405 foreach ($method->getParameters() as $num => $param) {
406 $paramName = $param->name;
407 if ($param->isVariadic()) {
408 if ($useName && Arrays::some($arguments, function ($val, $key) {
409 return \is_int($key);
410 })) {
411 throw new ServiceCreationException(\sprintf('Cannot use positional argument after named or omitted argument in %s.', Reflection::toString($param)));
412 } elseif (\array_key_exists($paramName, $arguments)) {
413 if (!\is_array($arguments[$paramName])) {
414 throw new ServiceCreationException(\sprintf('Parameter %s must be array, %s given.', Reflection::toString($param), \gettype($arguments[$paramName])));
415 }
416 $res = \array_merge($res, $arguments[$paramName]);
417 unset($arguments[$paramName]);
418 } else {
419 $res = \array_merge($res, $arguments);
420 $arguments = [];
421 }
422 $optCount = 0;
423 break;
424 } elseif (\array_key_exists($key = $paramName, $arguments) || \array_key_exists($key = $num, $arguments)) {
425 $res[$useName ? $paramName : $num] = $arguments[$key];
426 unset($arguments[$key], $arguments[$num]);
427 // unset $num to enable overwriting in configuration
428 } elseif (($aw = self::autowireArgument($param, $getter)) !== null) {
429 $res[$useName ? $paramName : $num] = $aw;
430 } elseif (\PHP_VERSION_ID >= 80000) {
431 if ($param->isOptional()) {
432 $useName = \true;
433 } else {
434 $res[$num] = null;
435 \trigger_error(\sprintf('The parameter %s should have a declared value in the configuration.', Reflection::toString($param)), \E_USER_DEPRECATED);
436 }
437 } else {
438 $res[$num] = $param->isDefaultValueAvailable() ? Reflection::getParameterDefaultValue($param) : null;
439 if (!$param->isOptional()) {
440 \trigger_error(\sprintf('The parameter %s should have a declared value in the configuration.', Reflection::toString($param)), \E_USER_DEPRECATED);
441 }
442 }
443 if (\PHP_VERSION_ID < 80000) {
444 $optCount = $param->isOptional() && $res[$num] === ($param->isDefaultValueAvailable() ? Reflection::getParameterDefaultValue($param) : null) ? $optCount + 1 : 0;
445 }
446 }
447 // extra parameters
448 while (!$useName && !$optCount && \array_key_exists(++$num, $arguments)) {
449 $res[$num] = $arguments[$num];
450 unset($arguments[$num]);
451 }
452 if ($arguments) {
453 throw new ServiceCreationException(\sprintf('Unable to pass specified arguments to %s.', Reflection::toString($method)));
454 } elseif ($optCount) {
455 $res = \array_slice($res, 0, -$optCount);
456 }
457 return $res;
458 }
459 /**
460 * Resolves missing argument using autowiring.
461 * @param (callable(string $type, bool $single): (object|object[]|null)) $getter
462 * @throws ServiceCreationException
463 * @return mixed
464 */
465 private static function autowireArgument(\ReflectionParameter $parameter, callable $getter)
466 {
467 $desc = Reflection::toString($parameter);
468 $type = \Packetery\Nette\Utils\Type::fromReflection($parameter);
469 if ($type && $type->isClass()) {
470 $class = $type->getSingleName();
471 try {
472 $res = $getter($class, \true);
473 } catch (MissingServiceException $e) {
474 $res = null;
475 } catch (ServiceCreationException $e) {
476 throw new ServiceCreationException("{$e->getMessage()} (required by {$desc})", 0, $e);
477 }
478 if ($res !== null || $parameter->allowsNull() || $parameter->isOptional()) {
479 return $res;
480 } elseif (\class_exists($class) || \interface_exists($class)) {
481 throw new ServiceCreationException(\sprintf('Service of type %s required by %s not found. Did you add it to configuration file?', $class, $desc));
482 } else {
483 throw new ServiceCreationException(\sprintf("Class '%s' required by %s not found. Check the parameter type and 'use' statements.", $class, $desc));
484 }
485 } elseif ($itemType = self::isArrayOf($parameter, $type)) {
486 return $getter($itemType, \false);
487 } elseif ($type && $parameter->allowsNull() || $parameter->isOptional() || $parameter->isDefaultValueAvailable()) {
488 // !optional + defaultAvailable, !optional + !defaultAvailable since 8.1.0 = func($a = null, $b)
489 // optional + !defaultAvailable, optional + defaultAvailable since 8.0.0 = i.e. Exception::__construct, mysqli::mysqli, ...
490 // optional + !defaultAvailable = variadics
491 // in other cases the optional and defaultAvailable are identical
492 return null;
493 } else {
494 throw new ServiceCreationException(\sprintf('Parameter %s has %s, so its value must be specified.', $desc, $type && !$type->isSingle() ? 'complex type and no default value' : 'no class type or default value'));
495 }
496 }
497 private static function isArrayOf(\ReflectionParameter $parameter, ?Nette\Utils\Type $type) : ?string
498 {
499 $method = $parameter->getDeclaringFunction();
500 return $method instanceof \ReflectionMethod && $type && $type->getSingleName() === 'array' && \preg_match('#@param[ \\t]+(?|([\\w\\\\]+)\\[\\]|list<([\\w\\\\]+)>|array<int,\\s*([\\w\\\\]+)>)[ \\t]+\\$' . $parameter->name . '#', (string) $method->getDocComment(), $m) && ($itemType = Reflection::expandClassName($m[1], $method->getDeclaringClass())) && (\class_exists($itemType) || \interface_exists($itemType)) ? $itemType : null;
501 }
502 }
503