gyselroth /
micro-container
| 1 | <?php |
||||||
| 2 | |||||||
| 3 | declare(strict_types=1); |
||||||
| 4 | |||||||
| 5 | /** |
||||||
| 6 | * Micro\Container |
||||||
| 7 | * |
||||||
| 8 | * @copyright Copryright (c) 2018-2019 gyselroth GmbH (https://gyselroth.com) |
||||||
| 9 | * @license MIT https://opensource.org/licenses/MIT |
||||||
| 10 | */ |
||||||
| 11 | |||||||
| 12 | namespace Micro\Container; |
||||||
| 13 | |||||||
| 14 | use ProxyManager\Factory\LazyLoadingValueHolderFactory; |
||||||
| 15 | use Psr\Container\ContainerInterface; |
||||||
| 16 | use ReflectionClass; |
||||||
| 17 | use ReflectionMethod; |
||||||
| 18 | use ReflectionParameter; |
||||||
| 19 | use RuntimeException; |
||||||
| 20 | |||||||
| 21 | class RuntimeContainer |
||||||
| 22 | { |
||||||
| 23 | /** |
||||||
| 24 | * Config. |
||||||
| 25 | * |
||||||
| 26 | * @var Config |
||||||
| 27 | */ |
||||||
| 28 | protected $config; |
||||||
| 29 | |||||||
| 30 | /** |
||||||
| 31 | * Service registry. |
||||||
| 32 | * |
||||||
| 33 | * @var array |
||||||
| 34 | */ |
||||||
| 35 | protected $service = []; |
||||||
| 36 | |||||||
| 37 | /** |
||||||
| 38 | * Parent container. |
||||||
| 39 | * |
||||||
| 40 | * @var ContainerInterface|RuntimeContainer |
||||||
| 41 | */ |
||||||
| 42 | protected $parent; |
||||||
| 43 | |||||||
| 44 | /** |
||||||
| 45 | * Children container. |
||||||
| 46 | * |
||||||
| 47 | * @var ContainerInterface[] |
||||||
| 48 | */ |
||||||
| 49 | protected $children = []; |
||||||
| 50 | |||||||
| 51 | /** |
||||||
| 52 | * Parent service. |
||||||
| 53 | * |
||||||
| 54 | * @var mixed |
||||||
| 55 | */ |
||||||
| 56 | protected $parent_service; |
||||||
| 57 | |||||||
| 58 | /** |
||||||
| 59 | * Create container. |
||||||
| 60 | */ |
||||||
| 61 | 64 | public function __construct(iterable $config, $parent, ContainerInterface $interface) |
|||||
| 62 | { |
||||||
| 63 | 64 | $this->config = new Config($config, $this); |
|||||
| 64 | 64 | $this->parent = $parent; |
|||||
| 65 | 64 | $this->service[ContainerInterface::class] = $interface; |
|||||
| 66 | 64 | } |
|||||
| 67 | |||||||
| 68 | /** |
||||||
| 69 | * Get parent container. |
||||||
| 70 | */ |
||||||
| 71 | 58 | public function getParent() |
|||||
| 72 | { |
||||||
| 73 | 58 | return $this->parent; |
|||||
| 74 | } |
||||||
| 75 | |||||||
| 76 | /** |
||||||
| 77 | * Set parent service on container. |
||||||
| 78 | */ |
||||||
| 79 | 2 | public function setParentService($service) |
|||||
| 80 | { |
||||||
| 81 | 2 | $this->parent_service = $service; |
|||||
| 82 | |||||||
| 83 | 2 | return $this; |
|||||
| 84 | } |
||||||
| 85 | |||||||
| 86 | /** |
||||||
| 87 | * Get config. |
||||||
| 88 | */ |
||||||
| 89 | 2 | public function getConfig(): Config |
|||||
| 90 | { |
||||||
| 91 | 2 | return $this->config; |
|||||
| 92 | } |
||||||
| 93 | |||||||
| 94 | /** |
||||||
| 95 | * Get service. |
||||||
| 96 | */ |
||||||
| 97 | 64 | public function get(string $name, ?array $parameters = null) |
|||||
| 98 | { |
||||||
| 99 | try { |
||||||
| 100 | 64 | return $this->resolve($name, $parameters); |
|||||
| 101 | 20 | } catch (Exception\ServiceNotFound $e) { |
|||||
| 102 | 16 | return $this->wrapService($name, $parameters); |
|||||
| 103 | } |
||||||
| 104 | } |
||||||
| 105 | |||||||
| 106 | /** |
||||||
| 107 | * Resolve service. |
||||||
| 108 | */ |
||||||
| 109 | 64 | public function resolve(string $name, ?array $parameters = null) |
|||||
| 110 | { |
||||||
| 111 | 64 | if (isset($this->service[$name])) { |
|||||
| 112 | 7 | return $this->service[$name]; |
|||||
| 113 | } |
||||||
| 114 | |||||||
| 115 | 62 | if ($this->config->has($name)) { |
|||||
| 116 | 54 | return $this->wrapService($name, $parameters); |
|||||
| 117 | } |
||||||
| 118 | |||||||
| 119 | 17 | if (null !== $this->parent_service) { |
|||||
| 120 | 1 | $parents = array_merge([$name], class_implements($this->parent_service), class_parents($this->parent_service)); |
|||||
| 121 | |||||||
| 122 | 1 | if (in_array($name, $parents, true) && $this->parent_service instanceof $name) { |
|||||
| 123 | return $this->parent_service; |
||||||
| 124 | } |
||||||
| 125 | } |
||||||
| 126 | |||||||
| 127 | 17 | if (null !== $this->parent) { |
|||||
| 128 | 1 | return $this->parent->resolve($name, $parameters); |
|||||
|
0 ignored issues
–
show
|
|||||||
| 129 | } |
||||||
| 130 | |||||||
| 131 | 16 | throw new Exception\ServiceNotFound("service $name was not found in service tree"); |
|||||
| 132 | } |
||||||
| 133 | |||||||
| 134 | /** |
||||||
| 135 | * Store service. |
||||||
| 136 | */ |
||||||
| 137 | 52 | protected function storeService(string $name, array $config, $service) |
|||||
| 138 | { |
||||||
| 139 | 52 | if (false === $config['singleton']) { |
|||||
| 140 | 6 | return $service; |
|||||
| 141 | } |
||||||
| 142 | 46 | $this->service[$name] = $service; |
|||||
| 143 | |||||||
| 144 | 46 | if (isset($this->children[$name])) { |
|||||
| 145 | 2 | $this->children[$name]->setParentService($service); |
|||||
|
0 ignored issues
–
show
The method
setParentService() does not exist on Psr\Container\ContainerInterface.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces. This is most likely a typographical error or the method has been renamed. Loading history...
|
|||||||
| 146 | } |
||||||
| 147 | |||||||
| 148 | 46 | return $service; |
|||||
| 149 | } |
||||||
| 150 | |||||||
| 151 | /** |
||||||
| 152 | * Wrap resolved service in callable if enabled. |
||||||
| 153 | */ |
||||||
| 154 | 62 | protected function wrapService(string $name, ?array $parameters = null) |
|||||
| 155 | { |
||||||
| 156 | 62 | $config = $this->config->get($name); |
|||||
| 157 | 59 | if (true === $config['wrap']) { |
|||||
| 158 | 1 | $that = $this; |
|||||
| 159 | |||||||
| 160 | 1 | return function () use ($that, $name, $parameters) { |
|||||
| 161 | 1 | return $that->autoWireClass($name, $parameters); |
|||||
| 162 | 1 | }; |
|||||
| 163 | } |
||||||
| 164 | |||||||
| 165 | 58 | return $this->autoWireClass($name, $parameters); |
|||||
| 166 | } |
||||||
| 167 | |||||||
| 168 | /** |
||||||
| 169 | * Auto wire. |
||||||
| 170 | */ |
||||||
| 171 | 59 | protected function autoWireClass(string $name, ?array $parameters = null) |
|||||
| 172 | { |
||||||
| 173 | 59 | $config = $this->config->get($name); |
|||||
| 174 | 59 | $class = $config['use']; |
|||||
| 175 | |||||||
| 176 | 59 | if (null !== $parameters) { |
|||||
| 177 | 3 | $config['singleton'] = false; |
|||||
| 178 | } |
||||||
| 179 | |||||||
| 180 | 59 | if (preg_match('#^\{([^{}]+)\}$#', $class, $match)) { |
|||||
| 181 | 1 | return $this->wireReference($name, $match[1], $config); |
|||||
| 182 | } |
||||||
| 183 | |||||||
| 184 | 59 | $reflection = new ReflectionClass($class); |
|||||
| 185 | |||||||
| 186 | 59 | if (isset($config['factory'])) { |
|||||
| 187 | 3 | $factory = $reflection->getMethod($config['factory']); |
|||||
| 188 | 3 | $args = $this->autoWireMethod($name, $factory, $config, $parameters); |
|||||
| 189 | 3 | $instance = call_user_func_array([$class, $config['factory']], $args); |
|||||
| 190 | |||||||
| 191 | 3 | return $this->prepareService($name, $instance, $reflection, $config); |
|||||
| 192 | } |
||||||
| 193 | |||||||
| 194 | 56 | $constructor = $reflection->getConstructor(); |
|||||
| 195 | |||||||
| 196 | 56 | if (null === $constructor) { |
|||||
| 197 | 8 | return $this->createInstance($name, $reflection, [], $config); |
|||||
| 198 | } |
||||||
| 199 | |||||||
| 200 | 48 | $args = $this->autoWireMethod($name, $constructor, $config, $parameters); |
|||||
| 201 | |||||||
| 202 | 42 | return $this->createInstance($name, $reflection, $args, $config); |
|||||
| 203 | } |
||||||
| 204 | |||||||
| 205 | /** |
||||||
| 206 | * Wire named referenced service. |
||||||
| 207 | */ |
||||||
| 208 | 1 | protected function wireReference(string $name, string $reference, array $config) |
|||||
| 209 | { |
||||||
| 210 | 1 | $service = $this->get($reference); |
|||||
| 211 | 1 | $reflection = new ReflectionClass(get_class($service)); |
|||||
| 212 | 1 | $config = $this->config->get($name); |
|||||
| 213 | 1 | $service = $this->prepareService($name, $service, $reflection, $config); |
|||||
| 214 | |||||||
| 215 | 1 | return $service; |
|||||
| 216 | } |
||||||
| 217 | |||||||
| 218 | /** |
||||||
| 219 | * Get instance (virtual or real instance). |
||||||
| 220 | */ |
||||||
| 221 | 50 | protected function createInstance(string $name, ReflectionClass $class, array $arguments, array $config) |
|||||
| 222 | { |
||||||
| 223 | 50 | if (true === $config['lazy']) { |
|||||
| 224 | 2 | return $this->getProxyInstance($name, $class, $arguments, $config); |
|||||
| 225 | } |
||||||
| 226 | |||||||
| 227 | 48 | return $this->getRealInstance($name, $class, $arguments, $config); |
|||||
| 228 | } |
||||||
| 229 | |||||||
| 230 | /** |
||||||
| 231 | * Create proxy instance. |
||||||
| 232 | */ |
||||||
| 233 | 2 | protected function getProxyInstance(string $name, ReflectionClass $class, array $arguments, array $config) |
|||||
| 234 | { |
||||||
| 235 | 2 | $factory = new LazyLoadingValueHolderFactory(); |
|||||
| 236 | 2 | $that = $this; |
|||||
| 237 | |||||||
| 238 | 2 | return $factory->createProxy( |
|||||
| 239 | 2 | $class->getName(), |
|||||
| 240 | 2 | function (&$wrappedObject, $proxy, $method, $parameters, &$initializer) use ($that, $name,$class,$arguments,$config) { |
|||||
| 241 | 1 | $wrappedObject = $that->getRealInstance($name, $class, $arguments, $config); |
|||||
| 242 | 1 | $initializer = null; |
|||||
| 243 | 2 | } |
|||||
| 244 | ); |
||||||
| 245 | } |
||||||
| 246 | |||||||
| 247 | /** |
||||||
| 248 | * Create real instance. |
||||||
| 249 | */ |
||||||
| 250 | 49 | protected function getRealInstance(string $name, ReflectionClass $class, array $arguments, array $config) |
|||||
| 251 | { |
||||||
| 252 | 49 | $instance = $class->newInstanceArgs($arguments); |
|||||
| 253 | 49 | $instance = $this->prepareService($name, $instance, $class, $config); |
|||||
| 254 | |||||||
| 255 | 49 | return $instance; |
|||||
| 256 | } |
||||||
| 257 | |||||||
| 258 | /** |
||||||
| 259 | * Prepare service (execute sub selects and excute setter injections). |
||||||
| 260 | */ |
||||||
| 261 | 52 | protected function prepareService(string $name, $service, ReflectionClass $class, array $config) |
|||||
| 262 | { |
||||||
| 263 | 52 | $this->storeService($name, $config, $service); |
|||||
| 264 | |||||||
| 265 | 52 | foreach ($config['calls'] as $call) { |
|||||
| 266 | 15 | if (!is_array($call)) { |
|||||
| 267 | 1 | continue; |
|||||
| 268 | } |
||||||
| 269 | |||||||
| 270 | 15 | if (!isset($call['method'])) { |
|||||
| 271 | throw new Exception\InvalidConfiguration('method is required for setter injection in service '.$name); |
||||||
| 272 | } |
||||||
| 273 | |||||||
| 274 | 15 | $arguments = []; |
|||||
| 275 | 15 | $result = null; |
|||||
| 276 | |||||||
| 277 | try { |
||||||
| 278 | 15 | $method = $class->getMethod($call['method']); |
|||||
| 279 | } catch (\ReflectionException $e) { |
||||||
| 280 | throw new Exception\InvalidConfiguration('method '.$call['method'].' is not callable in class '.$class->getName().' for service '.$name); |
||||||
| 281 | } |
||||||
| 282 | |||||||
| 283 | 15 | if (isset($call['batch']) && is_array($call['batch'])) { |
|||||
| 284 | 1 | foreach ($call['batch'] as $sub) { |
|||||
| 285 | 1 | $args = array_combine($call['arguments'], $sub); |
|||||
| 286 | 1 | $arguments = $this->autoWireMethod($name, $method, $call, $args); |
|||||
|
0 ignored issues
–
show
It seems like
$args can also be of type false; however, parameter $parameters of Micro\Container\RuntimeContainer::autoWireMethod() does only seem to accept array|null, maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||||
| 287 | 1 | $result = call_user_func_array([&$service, $call['method']], $arguments); |
|||||
| 288 | } |
||||||
| 289 | } else { |
||||||
| 290 | 14 | $arguments = $this->autoWireMethod($name, $method, $call); |
|||||
| 291 | 14 | $result = call_user_func_array([&$service, $call['method']], $arguments); |
|||||
| 292 | } |
||||||
| 293 | |||||||
| 294 | 15 | if (isset($call['select']) && true === $call['select']) { |
|||||
| 295 | 2 | $service = $result; |
|||||
| 296 | } |
||||||
| 297 | } |
||||||
| 298 | |||||||
| 299 | 52 | $this->storeService($name, $config, $service); |
|||||
| 300 | |||||||
| 301 | 52 | return $service; |
|||||
| 302 | } |
||||||
| 303 | |||||||
| 304 | /** |
||||||
| 305 | * Autowire method. |
||||||
| 306 | */ |
||||||
| 307 | 56 | protected function autoWireMethod(string $name, ReflectionMethod $method, array $config, ?array $parameters = null): array |
|||||
| 308 | { |
||||||
| 309 | 56 | $params = $method->getParameters(); |
|||||
| 310 | 56 | $args = []; |
|||||
| 311 | |||||||
| 312 | 56 | foreach ($params as $param) { |
|||||
| 313 | 56 | $type = $param->getClass(); |
|||||
| 314 | 56 | $param_name = $param->getName(); |
|||||
| 315 | |||||||
| 316 | 56 | $hint = $param->getType(); |
|||||
| 317 | 56 | if (null === $hint) { |
|||||
| 318 | 8 | $hint = 'string'; |
|||||
| 319 | } else { |
||||||
| 320 | 49 | $hint = $hint->getName(); |
|||||
| 321 | } |
||||||
| 322 | |||||||
| 323 | 56 | if (isset($parameters[$param_name])) { |
|||||
| 324 | 4 | $args[$param_name] = $parameters[$param_name]; |
|||||
| 325 | 55 | } elseif (isset($config['arguments'][$param_name])) { |
|||||
| 326 | 44 | $args[$param_name] = $this->parseParam($config['arguments'][$param_name], $name, $hint); |
|||||
| 327 | 23 | } elseif (null !== $type) { |
|||||
| 328 | 10 | $args[$param_name] = $this->resolveServiceArgument($name, $type, $param); |
|||||
| 329 | 15 | } elseif ($param->isDefaultValueAvailable()) { |
|||||
| 330 | 12 | $args[$param_name] = $param->getDefaultValue(); |
|||||
| 331 | 4 | } elseif ($param->allowsNull()) { |
|||||
| 332 | 1 | $args[$param_name] = null; |
|||||
| 333 | } else { |
||||||
| 334 | 3 | throw new Exception\InvalidConfiguration('no value found for argument '.$param_name.' in method '.$method->getName().' for service '.$name); |
|||||
| 335 | } |
||||||
| 336 | |||||||
| 337 | 50 | if (!$param->canBePassedByValue()) { |
|||||
| 338 | 2 | $value = &$args[$param_name]; |
|||||
| 339 | 2 | $args[$param_name] = &$value; |
|||||
| 340 | } |
||||||
| 341 | } |
||||||
| 342 | |||||||
| 343 | 50 | return $args; |
|||||
| 344 | } |
||||||
| 345 | |||||||
| 346 | /** |
||||||
| 347 | * Resolve service argument. |
||||||
| 348 | */ |
||||||
| 349 | 10 | protected function resolveServiceArgument(string $name, ReflectionClass $type, ReflectionParameter $param) |
|||||
| 350 | { |
||||||
| 351 | 10 | $type_class = $type->getName(); |
|||||
| 352 | |||||||
| 353 | 10 | if ($type_class === $name) { |
|||||
| 354 | 1 | throw new RuntimeException('class '.$type_class.' can not depend on itself'); |
|||||
| 355 | } |
||||||
| 356 | |||||||
| 357 | try { |
||||||
| 358 | 9 | return $this->traverseTree($name, $type_class); |
|||||
| 359 | 2 | } catch (\Exception $e) { |
|||||
| 360 | 2 | if ($param->isDefaultValueAvailable() && null === $param->getDefaultValue()) { |
|||||
| 361 | 1 | return null; |
|||||
| 362 | } |
||||||
| 363 | |||||||
| 364 | 1 | throw $e; |
|||||
| 365 | } |
||||||
| 366 | } |
||||||
| 367 | |||||||
| 368 | /** |
||||||
| 369 | * Parse param value. |
||||||
| 370 | */ |
||||||
| 371 | 44 | protected function parseParam($param, string $name, string $type = 'string') |
|||||
| 372 | { |
||||||
| 373 | 44 | if (is_iterable($param)) { |
|||||
| 374 | 1 | foreach ($param as $key => $value) { |
|||||
| 375 | 1 | $param[$key] = $this->parseParam($value, $name, 'string'); |
|||||
| 376 | } |
||||||
| 377 | |||||||
| 378 | 1 | return $param; |
|||||
| 379 | } |
||||||
| 380 | |||||||
| 381 | 44 | if (is_string($param)) { |
|||||
| 382 | 42 | $param = $this->config->getEnv($param, $type); |
|||||
| 383 | 41 | if (is_string($param)) { |
|||||
| 384 | 37 | if (preg_match('#^\{\{([^{}]+)\}\}$#', $param, $matches)) { |
|||||
| 385 | 1 | return '{'.$matches[1].'}'; |
|||||
| 386 | } |
||||||
| 387 | 36 | if (preg_match('#^\{([^{}]+)\}$#', $param, $matches)) { |
|||||
| 388 | 1 | return $this->traverseTree($name, $matches[1]); |
|||||
| 389 | } |
||||||
| 390 | } |
||||||
| 391 | |||||||
| 392 | 40 | return $param; |
|||||
| 393 | } |
||||||
| 394 | 2 | if ($param instanceof \Closure) { |
|||||
| 395 | 1 | $param = $param->bindTo($this); |
|||||
| 396 | } |
||||||
| 397 | |||||||
| 398 | 2 | return $param; |
|||||
| 399 | } |
||||||
| 400 | |||||||
| 401 | /** |
||||||
| 402 | * Locate service. |
||||||
| 403 | */ |
||||||
| 404 | 10 | protected function traverseTree(string $current_service, string $service) |
|||||
| 405 | { |
||||||
| 406 | 10 | if (isset($this->children[$current_service])) { |
|||||
| 407 | 1 | return $this->children[$current_service]->get($service); |
|||||
| 408 | } |
||||||
| 409 | |||||||
| 410 | 10 | $config = $this->config->get($current_service); |
|||||
| 411 | 10 | if (isset($config['services'])) { |
|||||
| 412 | 2 | $this->children[$current_service] = new self($config['services'], $this, $this->service[ContainerInterface::class]); |
|||||
| 413 | |||||||
| 414 | 2 | return $this->children[$current_service]->get($service); |
|||||
| 415 | } |
||||||
| 416 | |||||||
| 417 | 9 | return $this->get($service); |
|||||
| 418 | } |
||||||
| 419 | } |
||||||
| 420 |
This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.
This is most likely a typographical error or the method has been renamed.