*/ private array $data = []; /** @var array> */ private array $classCache = []; /** @var array> */ private array $loaderClassNames; private ?Configuration $configuration = null; private ?BindingContainer $bindingContainer = null; private InjectableFactory $injectableFactory; /** * @param class-string $configurationClassName * @param array> $loaderClassNames * @param array $services * @throws NotFoundExceptionInterface */ public function __construct( string $configurationClassName, array $loaderClassNames = [], array $services = [], ?BindingContainer $bindingContainer = null ) { $this->loaderClassNames = $loaderClassNames; foreach ($services as $name => $service) { if (!is_string($name) || !is_object($service)) { throw new RuntimeException("Container: Bad service passed."); } $this->setForced($name, $service); } $this->bindingContainer = $bindingContainer; /** @var InjectableFactory $injectableFactory */ $injectableFactory = $this->get(self::ID_INJECTABLE_FACTORY); $this->injectableFactory = $injectableFactory; $this->configuration = $this->injectableFactory->create($configurationClassName); } /** * Obtain a service object. * * @throws NotFoundExceptionInterface If not gettable. */ public function get(string $id): object { if (!$this->isSet($id)) { $this->load($id); if (!$this->isSet($id)) { throw new NotFoundException("Could not load '{$id}' service."); } } return $this->data[$id]; } /** * Check whether a service can be obtained. */ public function has(string $id): bool { if ($this->isSet($id)) { return true; } if (array_key_exists($id, $this->loaderClassNames)) { return true; } $loadMethodName = 'load' . ucfirst($id); if (method_exists($this, $loadMethodName)) { return true; } if (!$this->configuration) { return false; } if ($this->configuration->getLoaderClassName($id)) { return true; } if ($this->configuration->getServiceClassName($id)) { return true; } return false; } private function isSet(string $id): bool { return isset($this->data[$id]); } private function initClass(string $id): void { if ($this->isSet($id)) { try { $object = $this->get($id); } catch (NotFoundExceptionInterface) { throw new LogicException(); } $this->classCache[$id] = new ReflectionClass($object); return; } if ($id === self::ID_CONTAINER) { $this->classCache[$id] = new ReflectionClass(Container::class); return; } if ($id === self::ID_INJECTABLE_FACTORY) { $this->classCache[$id] = new ReflectionClass(InjectableFactory::class); return; } $loaderClassName = $this->getLoaderClassName($id); if ($loaderClassName) { $this->initClassByLoader($id, $loaderClassName); return; } assert($this->configuration !== null); $className = $this->configuration->getServiceClassName($id); if ($className === null) { throw new RuntimeException("No class-name for service '{$id}'."); } $this->classCache[$id] = new ReflectionClass($className); } /** * @param class-string $loaderClassName * @throws RuntimeException */ private function initClassByLoader(string $id, string $loaderClassName): void { $loaderClass = new ReflectionClass($loaderClassName); $loadMethod = $loaderClass->getMethod('load'); if (!$loadMethod->hasReturnType()) { throw new RuntimeException("Loader method for service '{$id}' does not have a return type."); } $returnType = $loadMethod->getReturnType(); if (!$returnType instanceof ReflectionNamedType) { throw new RuntimeException("Loader method for service '{$id}' does not have a named return type."); } /** @var class-string $className */ $className = $returnType->getName(); $this->classCache[$id] = new ReflectionClass($className); } /** * Get a class of a service. * * @return ReflectionClass * @throws RuntimeException If not gettable. */ public function getClass(string $id): ReflectionClass { if (!$this->has($id)) { throw new RuntimeException("Service '{$id}' does not exist."); } if (!isset($this->classCache[$id])) { $this->initClass($id); } return $this->classCache[$id]; } /** * Set a service object. Must be configured as settable. * * @throws NotSettableException Is not settable or already set. */ public function set(string $id, object $object): void { assert($this->configuration !== null); if (!$this->configuration->isSettable($id)) { throw new NotSettableException("Service '{$id}' is not settable."); } if ($this->isSet($id)) { throw new NotSettableException("Service '{$id}' is already set."); } $this->setForced($id, $object); } protected function setForced(string $id, object $object): void { $this->data[$id] = $object; } private function getLoader(string $name): ?Loader { $loaderClassName = $this->getLoaderClassName($name); if (!$loaderClassName) { return null; } return $this->injectableFactory->create($loaderClassName); } /** * @return ?class-string */ private function getLoaderClassName(string $id): ?string { $loader = $this->loaderClassNames[$id] ?? null; if ($loader) { return $loader; } assert($this->configuration !== null); return $this->configuration->getLoaderClassName($id); } /** * @throws NotFoundExceptionInterface */ private function load(string $id): void { if ($id === 'container') { $this->setForced('container', $this->loadContainer()); return; } if ($id === 'injectableFactory') { $this->setForced('injectableFactory', $this->loadInjectableFactory()); return; } $loader = $this->getLoader($id); if ($loader) { $this->data[$id] = $loader->load(); return; } assert($this->configuration !== null); $className = $this->configuration->getServiceClassName($id); if (!$className || !class_exists($className)) { throw new RuntimeException("Could not load '{$id}' service."); } $dependencyList = $this->configuration->getServiceDependencyList($id); if (!is_null($dependencyList)) { $dependencyObjectList = []; foreach ($dependencyList as $item) { $dependencyObjectList[] = $this->get($item); } $reflector = new ReflectionClass($className); $this->data[$id] = $reflector->newInstanceArgs($dependencyObjectList); return; } $this->data[$id] = $this->injectableFactory->create($className); } private function loadContainer(): Container { return $this; } private function loadInjectableFactory(): InjectableFactory { return new InjectableFactory($this, $this->bindingContainer); } }