| 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 |
|