mirror of
https://github.com/nextcloud/server.git
synced 2026-11-15 07:32:11 +01:00
269 lines
8.5 KiB
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;
|
|
}
|
|
}
|