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); |
|
| 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); |
|
| 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
Bug
introduced
by
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 |