569 lines
15 KiB
PHP
569 lines
15 KiB
PHP
<?php
|
|
/**
|
|
* Created by PhpStorm.
|
|
* User: whwyy
|
|
* Date: 2018/4/24 0024
|
|
* Time: 17:27
|
|
*/
|
|
declare(strict_types=1);
|
|
|
|
namespace Kiri\Di;
|
|
|
|
|
|
use Closure;
|
|
use Exception;
|
|
use Kiri\Di\Interface\InjectTargetInterface;
|
|
use Kiri\Router\Interface\ValidatorInterface;
|
|
use Kiri\Server\Task\OnTaskFinish;
|
|
use Psr\Container\ContainerInterface;
|
|
use ReflectionAttribute;
|
|
use ReflectionClass;
|
|
use ReflectionException;
|
|
use ReflectionFunction;
|
|
use ReflectionMethod;
|
|
use ReflectionParameter;
|
|
|
|
|
|
/**
|
|
* Class Container
|
|
*/
|
|
class Container implements ContainerInterface
|
|
{
|
|
|
|
/**
|
|
* @var array
|
|
*
|
|
* instance class by className
|
|
*/
|
|
private array $_singletons = [];
|
|
|
|
|
|
/**
|
|
* @var array
|
|
*
|
|
* implements \ReflectClass
|
|
*/
|
|
private array $_reflection = [];
|
|
|
|
|
|
/**
|
|
* @var array
|
|
*/
|
|
private array $_parameters = [];
|
|
|
|
|
|
/**
|
|
* @var array
|
|
*/
|
|
private array $_interfaces = [];
|
|
|
|
|
|
/**
|
|
* @var Container|null
|
|
*/
|
|
private static ?Container $container = null;
|
|
|
|
|
|
/**
|
|
* Construct \ContainerInterface
|
|
*/
|
|
private function __construct()
|
|
{
|
|
$this->_singletons[ContainerInterface::class] = $this;
|
|
}
|
|
|
|
|
|
/**
|
|
* @return static
|
|
*/
|
|
public static function instance(): static
|
|
{
|
|
if (static::$container === null) {
|
|
static::$container = new Container();
|
|
}
|
|
return static::$container;
|
|
}
|
|
|
|
|
|
/**
|
|
* @param string $id
|
|
* @return mixed
|
|
* @throws
|
|
*/
|
|
public function get(string $id): object
|
|
{
|
|
if (isset($this->_singletons[$id]))
|
|
return $this->_singletons[$id];
|
|
if (isset($this->_interfaces[$id])) {
|
|
return $this->_singletons[$id] = $this->make($this->_interfaces[$id]);
|
|
} else {
|
|
return $this->_singletons[$id] = $this->make($id);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* @param string $id
|
|
* @return object
|
|
* @throws
|
|
*/
|
|
public function parse(string $id): object
|
|
{
|
|
if (!isset($this->_singletons[$id])) {
|
|
return $this->make($id);
|
|
}
|
|
return $this->_singletons[$id];
|
|
}
|
|
|
|
|
|
/**
|
|
* @param string $interface
|
|
* @param string $class
|
|
* @return void
|
|
*/
|
|
public function set(string $interface, string $class): void
|
|
{
|
|
$this->_interfaces[$interface] = $class;
|
|
}
|
|
|
|
|
|
/**
|
|
* @param string $interface
|
|
* @param object $object
|
|
* @return object
|
|
*/
|
|
public function bind(string $interface, object $object): object
|
|
{
|
|
$this->_singletons[$interface] = $object;
|
|
return $object;
|
|
}
|
|
|
|
|
|
/**
|
|
* @param string $className
|
|
* @return ReflectionClass
|
|
* @throws
|
|
*/
|
|
public function getReflectionClass(string $className): ReflectionClass
|
|
{
|
|
if (!isset($this->_reflection[$className])) {
|
|
$this->_reflection[$className] = new ReflectionClass($className);
|
|
}
|
|
return $this->_reflection[$className];
|
|
}
|
|
|
|
|
|
public function forgetReflection(string $className): void
|
|
{
|
|
unset($this->_reflection[$className]);
|
|
}
|
|
|
|
|
|
public function forgetMethodParams(string $className, ?string $method = null): void
|
|
{
|
|
if (!isset($this->_parameters[$className])) {
|
|
return;
|
|
}
|
|
|
|
if ($method === null) {
|
|
unset($this->_parameters[$className]);
|
|
return;
|
|
}
|
|
|
|
unset($this->_parameters[$className][$method]);
|
|
if ($this->_parameters[$className] === []) {
|
|
unset($this->_parameters[$className]);
|
|
}
|
|
}
|
|
|
|
|
|
public function forgetClass(string $className): void
|
|
{
|
|
unset($this->_singletons[$className]);
|
|
$this->forgetReflection($className);
|
|
$this->forgetMethodParams($className);
|
|
}
|
|
|
|
|
|
public function forgetNamespace(string $prefix): void
|
|
{
|
|
foreach (array_keys($this->_singletons) as $className) {
|
|
if ($className !== ContainerInterface::class && str_starts_with($className, $prefix)) {
|
|
unset($this->_singletons[$className]);
|
|
}
|
|
}
|
|
|
|
foreach (array_keys($this->_reflection) as $className) {
|
|
if (str_starts_with($className, $prefix)) {
|
|
unset($this->_reflection[$className]);
|
|
}
|
|
}
|
|
|
|
foreach (array_keys($this->_parameters) as $className) {
|
|
if (str_starts_with($className, $prefix)) {
|
|
unset($this->_parameters[$className]);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* 清理所有非基础设施类的单例引用,释放 Swoole 常驻进程中的内存
|
|
* 保留框架核心类(Kiri、Database、Psr、Symfony)的单例
|
|
* 应在请求/任务完成后调用,防止单例映射无限增长
|
|
* @return void
|
|
*/
|
|
public function clearNonInfrastructure(): void
|
|
{
|
|
$keepPrefixes = [
|
|
ContainerInterface::class,
|
|
'Kiri\\', 'Database\\', 'Psr\\', 'Symfony\\',
|
|
'Swoole\\', 'MongoDB\\',
|
|
];
|
|
|
|
foreach (array_keys($this->_singletons) as $className) {
|
|
$shouldKeep = false;
|
|
foreach ($keepPrefixes as $prefix) {
|
|
if (str_starts_with($className, $prefix)) {
|
|
$shouldKeep = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!$shouldKeep) {
|
|
unset($this->_singletons[$className]);
|
|
}
|
|
}
|
|
|
|
foreach (array_keys($this->_reflection) as $className) {
|
|
$shouldKeep = false;
|
|
foreach ($keepPrefixes as $prefix) {
|
|
if (str_starts_with($className, $prefix)) {
|
|
$shouldKeep = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!$shouldKeep) {
|
|
unset($this->_reflection[$className]);
|
|
}
|
|
}
|
|
|
|
foreach (array_keys($this->_parameters) as $className) {
|
|
$shouldKeep = false;
|
|
foreach ($keepPrefixes as $prefix) {
|
|
if (str_starts_with($className, $prefix)) {
|
|
$shouldKeep = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!$shouldKeep) {
|
|
unset($this->_parameters[$className]);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* 获取当前单例缓存统计信息,用于内存监控
|
|
* @return array{singletons: int, reflections: int, params: int}
|
|
*/
|
|
public function getMemoryStats(): array
|
|
{
|
|
return [
|
|
'singletons' => count($this->_singletons),
|
|
'reflections' => count($this->_reflection),
|
|
'params' => count($this->_parameters),
|
|
];
|
|
}
|
|
|
|
|
|
/**
|
|
* @param string $className
|
|
* @param array $construct
|
|
* @param array $config
|
|
* @return object|null
|
|
* @throws
|
|
*/
|
|
public function make(string $className, array $construct = [], array $config = []): ?object
|
|
{
|
|
$reflect = $this->getReflectionClass($className);
|
|
if (!$reflect->isInstantiable()) {
|
|
throw new ReflectionException('Class ' . $className . ' cannot be instantiated');
|
|
}
|
|
|
|
if (empty($construct) && ($handler = $reflect->getConstructor()) !== null) {
|
|
$construct = $this->getMethodParams($handler);
|
|
}
|
|
|
|
$isController = class_exists(\Kiri\Router\Base\Controller::class) && $reflect->isSubclassOf(\Kiri\Router\Base\Controller::class);
|
|
$needsProxy = !$isController && class_exists(\Kiri\Router\Annotate\DeferRegistry::class) && \Kiri\Router\Annotate\DeferRegistry::hasAny($className);
|
|
|
|
if ($needsProxy) {
|
|
$newInstance = \Kiri\Router\Annotate\DeferProxyGenerator::create($className, $construct);
|
|
} else {
|
|
$newInstance = $reflect->newInstanceArgs($construct);
|
|
}
|
|
|
|
return $this->runInit($reflect, static::configure($newInstance, $config));
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* @param ReflectionClass $reflect
|
|
* @param array $construct
|
|
* @param array $config
|
|
* @return object|null
|
|
* @throws ReflectionException
|
|
*/
|
|
public function makeReflection(ReflectionClass $reflect, array $construct = [], array $config = []): ?object
|
|
{
|
|
if (isset($this->_singletons[$reflect->getName()])) {
|
|
return $this->_singletons[$reflect->getName()];
|
|
}
|
|
|
|
if (!$reflect->isInstantiable()) {
|
|
throw new ReflectionException('Class ' . $reflect->getName() . ' cannot be instantiated');
|
|
}
|
|
|
|
if (empty($construct) && ($handler = $reflect->getConstructor()) !== null) {
|
|
$construct = $this->getMethodParams($handler);
|
|
}
|
|
$isController = class_exists(\Kiri\Router\Base\Controller::class) && $reflect->isSubclassOf(\Kiri\Router\Base\Controller::class);
|
|
$needsProxy = !$isController && class_exists(\Kiri\Router\Annotate\DeferRegistry::class) && \Kiri\Router\Annotate\DeferRegistry::hasAny($reflect->getName());
|
|
|
|
if ($needsProxy) {
|
|
$newInstance = \Kiri\Router\Annotate\DeferProxyGenerator::create($reflect->getName(), $construct);
|
|
} else {
|
|
$newInstance = $reflect->newInstanceArgs($construct);
|
|
}
|
|
|
|
return $this->runInit($reflect, static::configure($newInstance, $config));
|
|
}
|
|
|
|
|
|
/**
|
|
* @param ReflectionClass $reflect
|
|
* @param object $object
|
|
* @return void
|
|
*/
|
|
protected function injectClassTarget(ReflectionClass $reflect, object $object): void
|
|
{
|
|
$this->resolveProperties($reflect, $object);
|
|
$attributes = $reflect->getAttributes();
|
|
foreach ($attributes as $attribute) {
|
|
if (class_exists($attribute->getName())) {
|
|
$instance = $attribute->newInstance();
|
|
if ($instance instanceof InjectTargetInterface) {
|
|
$instance->dispatch($object::class);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* @param ReflectionClass $reflect
|
|
* @param object $object
|
|
* @return object
|
|
*/
|
|
protected function runInit(ReflectionClass $reflect, object $object): object
|
|
{
|
|
$this->injectClassTarget($reflect, $object);
|
|
if ($reflect->getName() === 'Symfony\Component\Console\Application') {
|
|
return $object;
|
|
}
|
|
if (method_exists($object, 'init')) {
|
|
call_user_func([$object, 'init']);
|
|
}
|
|
return $object;
|
|
}
|
|
|
|
|
|
/**
|
|
* @param ReflectionClass $reflectionClass
|
|
* @param object $class
|
|
* @return void
|
|
*/
|
|
public function resolveProperties(ReflectionClass $reflectionClass, object $class): void
|
|
{
|
|
$properties = $reflectionClass->getProperties();
|
|
foreach ($properties as $property) {
|
|
$propertyAttributes = $property->getAttributes();
|
|
foreach ($propertyAttributes as $attribute) {
|
|
if (!class_exists($attribute->getName()) || $this->isValidatorInterface($attribute)) {
|
|
continue;
|
|
}
|
|
$instance = $attribute->newInstance();
|
|
$instance->dispatch($class, $property->getName());
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* @param ReflectionAttribute $attribute
|
|
* @return bool
|
|
*/
|
|
protected function isValidatorInterface(ReflectionAttribute $attribute): bool
|
|
{
|
|
return in_array(ValidatorInterface::class, class_implements($attribute->getName()));
|
|
}
|
|
|
|
/**
|
|
* @param string $className
|
|
* @param string $method
|
|
* @return ReflectionMethod
|
|
* @throws
|
|
*/
|
|
public function getMethod(string $className, string $method): ReflectionMethod
|
|
{
|
|
$reflection = $this->getReflectionClass($className);
|
|
|
|
return $reflection->getMethod($method);
|
|
}
|
|
|
|
|
|
/**
|
|
* @param string $className
|
|
* @return ReflectionMethod[]
|
|
* @throws
|
|
*/
|
|
public function getMethods(string $className): array
|
|
{
|
|
$reflection = $this->getReflectionClass($className);
|
|
|
|
return $reflection->getMethods();
|
|
}
|
|
|
|
|
|
/**
|
|
* @param ReflectionMethod $parameters
|
|
* @return array
|
|
* @throws
|
|
*/
|
|
public function getMethodParams(ReflectionMethod $parameters): array
|
|
{
|
|
$className = $parameters->getDeclaringClass()->getName();
|
|
$methodName = $parameters->getName();
|
|
if (!isset($this->_parameters[$className]))
|
|
$this->_parameters[$className] = [];
|
|
if (!isset($this->_parameters[$className][$methodName])) {
|
|
return $this->_parameters[$className][$methodName] = $this->resolveMethodParams($parameters);
|
|
} else {
|
|
return $this->_parameters[$className][$methodName];
|
|
}
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* @param Closure $parameters
|
|
* @return array
|
|
* @throws
|
|
*/
|
|
public function getFunctionParams(Closure $parameters): array
|
|
{
|
|
return $this->resolveMethodParams(new ReflectionFunction($parameters));
|
|
}
|
|
|
|
|
|
/**
|
|
* @param ReflectionMethod|ReflectionFunction $parameters
|
|
* @return array
|
|
* @throws
|
|
*/
|
|
public function resolveMethodParams(ReflectionMethod|ReflectionFunction $parameters): array
|
|
{
|
|
$params = [];
|
|
if ($parameters->getNumberOfParameters() < 1) {
|
|
return $params;
|
|
}
|
|
$parametersArray = $parameters->getParameters();
|
|
$class = $parameters->getDeclaringClass()->getName();
|
|
foreach ($parametersArray as $parameter) {
|
|
$parameterAttributes = $parameter->getAttributes();
|
|
$name = $parameter->getName();
|
|
if (count($parameterAttributes) > 0) {
|
|
$attribute = $parameterAttributes[0]->newInstance();
|
|
$params[$name] = $attribute->dispatch($class, $parameters->getName());
|
|
} else {
|
|
$params[$name] = $this->contractParams($parameter);
|
|
}
|
|
}
|
|
return $params;
|
|
}
|
|
|
|
|
|
/**
|
|
* @param $parameter
|
|
* @return bool|int|mixed|object|string|null
|
|
* @throws
|
|
*/
|
|
protected function contractParams($parameter): mixed
|
|
{
|
|
if ($parameter->isDefaultValueAvailable()) {
|
|
return $parameter->getDefaultValue();
|
|
}
|
|
if ($parameter->getType() === null) {
|
|
return $parameter->getType();
|
|
}
|
|
$value = $parameter->getType()->getName();
|
|
if (class_exists($value) || interface_exists($value)) {
|
|
return $this->get($value);
|
|
} else {
|
|
return $this->getTypeValue($parameter);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* @param ReflectionParameter $parameter
|
|
* @return string|int|bool|null|array
|
|
* @throws
|
|
*/
|
|
private function getTypeValue(ReflectionParameter $parameter): string|int|bool|null|array
|
|
{
|
|
$class = $parameter->getDeclaringClass()->getName();
|
|
$name = $parameter->getName();
|
|
return match ($parameter->getType()->getName()) {
|
|
'string' => '',
|
|
'int', 'float' => 0,
|
|
'', 'mixed' => NULL,
|
|
'bool' => false,
|
|
'object' => throw new Exception('Param type ' . $class . '::' . $name . ' must has default value.'),
|
|
'array' => [],
|
|
default => null
|
|
};
|
|
}
|
|
|
|
|
|
/**
|
|
* @param object $object
|
|
* @param array $config
|
|
* @return object
|
|
*/
|
|
public static function configure(object $object, array $config): object
|
|
{
|
|
foreach ($config as $key => $value) {
|
|
if (!property_exists($object, $key)) {
|
|
continue;
|
|
}
|
|
$object->{$key} = $value;
|
|
}
|
|
return $object;
|
|
}
|
|
|
|
|
|
/**
|
|
* @param string $id
|
|
* @return bool
|
|
*/
|
|
public function has(string $id): bool
|
|
{
|
|
// TODO: Implement has() method.
|
|
return isset($this->_singletons[$id]) && isset($this->_reflection[$id]);
|
|
}
|
|
|
|
|
|
}
|