diff --git a/lib/private/AppFramework/DependencyInjection/DIContainer.php b/lib/private/AppFramework/DependencyInjection/DIContainer.php index 76261fe6b92c5..78951d99364db 100644 --- a/lib/private/AppFramework/DependencyInjection/DIContainer.php +++ b/lib/private/AppFramework/DependencyInjection/DIContainer.php @@ -310,21 +310,29 @@ public function has($id): bool { return false; } - public function query(string $name, bool $autoload = true) { + /** + * @inheritDoc + * @param list $chain + */ + public function query(string $name, bool $autoload = true, array $chain = []) { if ($name === 'AppName' || $name === 'appName') { return $this->appName; } $isServerClass = str_starts_with($name, 'OCP\\') || str_starts_with($name, 'OC\\'); if ($isServerClass && !$this->has($name)) { - return $this->getServer()->query($name, $autoload); + /** @var ServerContainer $server */ + $server = $this->getServer(); + return $server->query($name, $autoload, $chain); } try { - return $this->queryNoFallback($name); + return $this->queryNoFallback($name, $chain); } catch (QueryException $firstException) { try { - return $this->getServer()->query($name, $autoload); + /** @var ServerContainer $server */ + $server = $this->getServer(); + return $server->query($name, $autoload, $chain); } catch (QueryException $secondException) { if ($firstException->getCode() === 1) { throw $secondException; @@ -339,23 +347,23 @@ public function query(string $name, bool $autoload = true) { * @return mixed * @throws QueryException if the query could not be resolved */ - public function queryNoFallback($name) { + public function queryNoFallback($name, array $chain) { $name = $this->sanitizeName($name); if ($this->offsetExists($name)) { - return parent::query($name); + return parent::query($name, chain: $chain); } elseif ($this->appName === 'settings' && str_starts_with($name, 'OC\\Settings\\')) { - return parent::query($name); + return parent::query($name, chain: $chain); } elseif ($this->appName === 'core' && str_starts_with($name, 'OC\\Core\\')) { - return parent::query($name); + return parent::query($name, chain: $chain); } elseif (str_starts_with($name, \OC\AppFramework\App::buildAppNamespace($this->appName) . '\\')) { - return parent::query($name); + return parent::query($name, chain: $chain); } elseif ( str_starts_with($name, 'OC\\AppFramework\\Services\\') || str_starts_with($name, 'OC\\AppFramework\\Middleware\\') ) { /* AppFramework services are scoped to the application */ - return parent::query($name); + return parent::query($name, chain: $chain); } throw new QueryException('Could not resolve ' . $name . '!' diff --git a/lib/private/AppFramework/Utility/SimpleContainer.php b/lib/private/AppFramework/Utility/SimpleContainer.php index 49decf471f2bf..f5a2b6f84b7a9 100644 --- a/lib/private/AppFramework/Utility/SimpleContainer.php +++ b/lib/private/AppFramework/Utility/SimpleContainer.php @@ -19,6 +19,7 @@ use ReflectionException; use ReflectionNamedType; use ReflectionParameter; +use RuntimeException; use function class_exists; /** @@ -52,10 +53,11 @@ public function has(string $id): bool { /** * @param ReflectionClass $class the class to instantiate + * @param list $chain * @return object the created class * @suppress PhanUndeclaredClassInstanceof */ - private function buildClass(ReflectionClass $class): object { + private function buildClass(ReflectionClass $class, array $chain): object { $constructor = $class->getConstructor(); if ($constructor === null) { /* No constructor, return a instance directly */ @@ -64,17 +66,20 @@ private function buildClass(ReflectionClass $class): object { if (PHP_VERSION_ID >= 80400 && self::$useLazyObjects && !$class->isInternal()) { /* For PHP>=8.4, use a lazy ghost to delay constructor and dependency resolving */ /** @psalm-suppress UndefinedMethod */ - return $class->newLazyGhost(function (object $object) use ($constructor): void { + return $class->newLazyGhost(function (object $object) use ($constructor, $chain): void { /** @psalm-suppress DirectConstructorCall For lazy ghosts we have to call the constructor directly */ - $object->__construct(...$this->buildClassConstructorParameters($constructor)); + $object->__construct(...$this->buildClassConstructorParameters($constructor, $chain)); }); } else { - return $class->newInstanceArgs($this->buildClassConstructorParameters($constructor)); + return $class->newInstanceArgs($this->buildClassConstructorParameters($constructor, $chain)); } } - private function buildClassConstructorParameters(\ReflectionMethod $constructor): array { - return array_map(function (ReflectionParameter $parameter) { + /** + * @param list $chain + */ + private function buildClassConstructorParameters(\ReflectionMethod $constructor, array $chain): array { + return array_map(function (ReflectionParameter $parameter) use ($chain) { $parameterType = $parameter->getType(); $resolveName = $parameter->getName(); @@ -87,7 +92,7 @@ private function buildClassConstructorParameters(\ReflectionMethod $constructor) try { $builtIn = $parameterType !== null && ($parameterType instanceof ReflectionNamedType) && $parameterType->isBuiltin(); - return $this->query($resolveName, !$builtIn); + return $this->query($resolveName, !$builtIn, $chain); } catch (ContainerExceptionInterface $e) { // Service not found, use the default value when available if ($parameter->isDefaultValueAvailable()) { @@ -97,7 +102,7 @@ private function buildClassConstructorParameters(\ReflectionMethod $constructor) if ($parameterType !== null && ($parameterType instanceof ReflectionNamedType) && !$parameterType->isBuiltin()) { $resolveName = $parameter->getName(); try { - return $this->query($resolveName); + return $this->query($resolveName, chain: $chain); } catch (ContainerExceptionInterface $e2) { // Pass null if typed and nullable if ($parameter->allowsNull() && ($parameterType instanceof ReflectionNamedType)) { @@ -114,12 +119,16 @@ private function buildClassConstructorParameters(\ReflectionMethod $constructor) }, $constructor->getParameters()); } - public function resolve($name) { + /** + * @inheritDoc + * @param list $chain + */ + public function resolve($name, array $chain = []) { $baseMsg = 'Could not resolve ' . $name . '!'; try { $class = new ReflectionClass($name); if ($class->isInstantiable()) { - return $this->buildClass($class); + return $this->buildClass($class, $chain); } else { throw new QueryException($baseMsg . ' Class can not be instantiated'); @@ -130,14 +139,22 @@ public function resolve($name) { } } - public function query(string $name, bool $autoload = true) { + /** + * @inheritDoc + * @param list $chain + */ + public function query(string $name, bool $autoload = true, array $chain = []) { $name = $this->sanitizeName($name); if (isset($this->container[$name])) { return $this->container[$name]; } if ($autoload) { - $object = $this->resolve($name); + if (in_array($name, $chain, true)) { + throw new RuntimeException('Tried to query ' . $name . ', but it is already in the chain: ' . implode(', ', $chain)); + } + + $object = $this->resolve($name, array_merge($chain, [$name])); $this->registerService($name, function () use ($object) { return $object; }); diff --git a/lib/private/ServerContainer.php b/lib/private/ServerContainer.php index b5bcbdaeb6fd2..42cdf37fce92b 100644 --- a/lib/private/ServerContainer.php +++ b/lib/private/ServerContainer.php @@ -111,6 +111,7 @@ public function has($id, bool $noRecursion = false): bool { /** * @template T * @param class-string|string $name + * @param list $chain * @return T|mixed * @psalm-template S as class-string|string * @psalm-param S $name @@ -118,13 +119,13 @@ public function has($id, bool $noRecursion = false): bool { * @throws QueryException * @deprecated 20.0.0 use \Psr\Container\ContainerInterface::get */ - public function query(string $name, bool $autoload = true) { + public function query(string $name, bool $autoload = true, array $chain = []) { $name = $this->sanitizeName($name); if (str_starts_with($name, 'OCA\\')) { // Skip server container query for app namespace classes try { - return parent::query($name, false); + return parent::query($name, false, $chain); } catch (QueryException $e) { // Continue with general autoloading then } @@ -132,7 +133,7 @@ public function query(string $name, bool $autoload = true) { // the apps container first. if (($appContainer = $this->getAppContainerForService($name)) !== null) { try { - return $appContainer->queryNoFallback($name); + return $appContainer->queryNoFallback($name, $chain); } catch (QueryException $e) { // Didn't find the service or the respective app container // In this case the service won't be part of the core container, @@ -144,14 +145,14 @@ public function query(string $name, bool $autoload = true) { $segments = explode('\\', $name); try { $appContainer = $this->getAppContainer(strtolower($segments[1]), $segments[1]); - return $appContainer->queryNoFallback($name); + return $appContainer->queryNoFallback($name, $chain); } catch (QueryException $e) { // Didn't find the service or the respective app container, // ignore it and fall back to the core container. } } - return parent::query($name, $autoload); + return parent::query($name, $autoload, $chain); } /**