Files
nextcloud-server-mirror/lib/private/AppFramework/Utility/SimpleContainer.php

269 lines
8.5 KiB
PHP

<?php
/**
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
* SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OC\AppFramework\Utility;
use Closure;
use OCP\AppFramework\QueryException;
use OCP\IContainer;
use Pimple\Container;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use ReflectionClass;
use ReflectionException;
use ReflectionNamedType;
use ReflectionParameter;
use RuntimeException;
use function class_exists;
/**
* SimpleContainer is a simple implementation of a container on basis of Pimple
*/
class SimpleContainer implements ContainerInterface, IContainer {
/** @psalm-suppress ImpureStaticProperty A static property is the only way to pass the information from config to autoload */
public static bool $useLazyObjects = false;
protected Container $container;
/** @var array<string,string> */
private array $aliases = [];
public function __construct() {
$this->container = new Container();
}
/**
* @template T
* @param class-string<T>|string $id
* @return ($id is class-string<T> ? T : mixed)
*/
#[\Override]
public function get(string $id): mixed {
return $this->query($this->sanitizeName($id));
}
#[\Override]
public function has(string $id): bool {
// If a service is no registered but is an existing class, we can probably load it
return isset($this->aliases[$id]) || isset($this->container[$id]) || class_exists($id);
}
/**
* @param ReflectionClass $class the class to instantiate
* @param list<class-string> $chain
* @return object the created class
* @suppress PhanUndeclaredClassInstanceof
*/
private function buildClass(ReflectionClass $class, array $chain): object {
$constructor = $class->getConstructor();
if ($constructor === null) {
/* No constructor, return a instance directly */
return $class->newInstance();
}
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, $chain): void {
/** @psalm-suppress DirectConstructorCall For lazy ghosts we have to call the constructor directly */
$object->__construct(...$this->buildClassConstructorParameters($constructor, $chain));
});
} else {
return $class->newInstanceArgs($this->buildClassConstructorParameters($constructor, $chain));
}
}
/**
* @param list<class-string> $chain
*/
private function buildClassConstructorParameters(\ReflectionMethod $constructor, array $chain): array {
return array_map(function (ReflectionParameter $parameter) use ($chain) {
$parameterType = $parameter->getType();
$resolveName = $parameter->getName();
// try to find out if it is a class or a simple parameter
if ($parameterType !== null && ($parameterType instanceof ReflectionNamedType) && !$parameterType->isBuiltin()) {
$resolveName = $parameterType->getName();
}
try {
$builtIn = $parameterType !== null && ($parameterType instanceof ReflectionNamedType)
&& $parameterType->isBuiltin();
return $this->query($resolveName, !$builtIn, $chain);
} catch (ContainerExceptionInterface $e) {
// Service not found, use the default value when available
if ($parameter->isDefaultValueAvailable()) {
return $parameter->getDefaultValue();
}
if ($parameterType !== null && ($parameterType instanceof ReflectionNamedType) && !$parameterType->isBuiltin()) {
$resolveName = $parameter->getName();
try {
return $this->query($resolveName, chain: $chain);
} catch (ContainerExceptionInterface $e2) {
// Pass null if typed and nullable
if ($parameter->allowsNull() && ($parameterType instanceof ReflectionNamedType)) {
return null;
}
// don't lose the error we got while trying to query by type
throw new QueryException($e->getMessage(), (int)$e->getCode(), $e);
}
}
throw $e;
}
}, $constructor->getParameters());
}
/**
* @template T
*
* Try to instantiate by using reflection to find out how to build the class.
*
* @param class-string<T>|string $name
* @param list<class-string> $chain
* @return ($name is class-string<T> ? T : mixed)
* @internal
* @throws ContainerExceptionInterface if the class could not be found or instantiated
*/
public function resolve(string $name, array $chain = []): mixed {
$baseMsg = 'Could not resolve ' . $name . '!';
try {
$class = new ReflectionClass($name);
if ($class->isInstantiable()) {
return $this->buildClass($class, $chain);
} else {
throw new QueryException($baseMsg
. ' Class can not be instantiated');
}
} catch (ReflectionException $e) {
// Class does not exist
throw new QueryNotFoundException($baseMsg . ' ' . $e->getMessage());
}
}
/**
* @param string $name Already sanitized name
* @param list<class-string> $chain
*/
protected function query(string $name, bool $autoload = true, array $chain = []): mixed {
$name = $this->resolveAlias($name);
if (isset($this->container[$name])) {
return $this->container[$name];
}
if (!$autoload) {
throw new QueryNotFoundException('Could not 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, static fn () => $object);
return $object;
}
/**
* A value is stored in the container with its corresponding name
*
* @since 6.0.0
* @internal apps should use \OCP\AppFramework\Bootstrap\IRegistrationContext::registerParameter
*/
public function registerParameter(string $name, mixed $value): void {
$this->container[$name] = $value;
}
/**
* A service is registered in the container where a closure is passed in which will actually
* create the service on demand.
* In case the parameter $shared is set to true (the default usage) the once created service will remain in
* memory and be reused on subsequent calls.
* In case the parameter is false the service will be recreated on every call.
*
* @param \Closure(IContainer): mixed $closure
* @internal apps should use \OCP\AppFramework\Bootstrap\IRegistrationContext::registerService
*/
public function registerService(string $name, Closure $closure, bool $shared = true): void {
$wrapped = fn () => $closure($this);
$name = $this->sanitizeName($name);
if (isset($this->container[$name])) {
unset($this->container[$name]);
}
if (isset($this->aliases[$name])) {
unset($this->aliases[$name]);
}
if ($shared) {
$this->container[$name] = $wrapped;
} else {
$this->container[$name] = $this->container->factory($wrapped);
}
}
/**
* Shortcut for returning a service from a service under a different key,
* e.g. to tell the container to return a class when queried for an
* interface
* @param string $alias the alias that should be registered
* @param string $target the target that should be resolved instead
*/
public function registerAlias(string $alias, string $target): void {
$this->aliases[$alias] = $target;
}
protected function resolveAlias(string $name) : string {
while (isset($this->aliases[$name])) {
$name = $this->aliases[$name];
}
return $name;
}
protected function registerDeprecatedAlias(string $alias, string $target): void {
$this->registerService($alias, function (ContainerInterface $container) use ($target, $alias): mixed {
try {
$logger = $container->get(LoggerInterface::class);
$logger->debug('The requested alias "' . $alias . '" is deprecated. Please request "' . $target . '" directly. This alias will be removed in a future Nextcloud version.', [
'app' => $this->appName ?? 'serverDI',
]);
} catch (ContainerExceptionInterface $e) {
// Could not get logger. Continue
}
return $container->get($target);
}, false);
}
/**
* @param string $name
* @return string
*/
protected function sanitizeName($name) {
if (isset($name[0]) && $name[0] === '\\') {
return ltrim($name, '\\');
}
return $name;
}
/**
* @internal Used by tests
*/
public function removeFromInternalContainer(string $service): void {
unset($this->container[$service]);
}
/**
* @internal Used by server container on app containers
*/
public function setInInternalContainer(string $service, mixed $value): void {
$this->container[$service] = $value;
}
}