diff --git a/composer.json b/composer.json index 10dee766..40434382 100644 --- a/composer.json +++ b/composer.json @@ -16,6 +16,7 @@ "dragonmantank/cron-expression": "^3.3", "jelix/version": "^2.0", "koriym/attributes": "^1.0", + "psr/container": "^2.0", "psy/psysh": "^0.11.2", "symfony/console": "~5.0 || ~4.0 || ~3.0", "symfony/polyfill-ctype": "^1.19", diff --git a/phpstan.neon b/phpstan.neon index f5f4499d..80cc3e85 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -6,5 +6,6 @@ parameters: ignoreErrors: - '#Used constant OS_TYPE_(LINUX|WINDOWS) not found#' - '#Constant .* not found#' + - '#PHPDoc tag @throws with type Psr\\Container\\ContainerExceptionInterface is not subtype of Throwable#' dynamicConstantNames: - SWOOLE_VERSION diff --git a/src/ZM/Container/ContainerInterface.php b/src/ZM/Container/ContainerInterface.php new file mode 100644 index 00000000..3bb170ce --- /dev/null +++ b/src/ZM/Container/ContainerInterface.php @@ -0,0 +1,108 @@ +shared) + || $this->isAlias($abstract); + } + + /** + * 获取类别名(如存在) + * + * @param string $abstract 类或接口名 + * @return string 别名,不存在时返回传入的类或接口名 + */ + public function getAlias(string $abstract): string + { + if (!isset(self::$aliases[$abstract])) { + return $abstract; + } + + return $this->getAlias(self::$aliases[$abstract]); + } + + /** + * 注册一个类别名 + * + * @param string $abstract 类或接口名 + * @param string $alias 别名 + */ + public function alias(string $abstract, string $alias): void + { + if ($alias === $abstract) { + throw new InvalidArgumentException("[{$abstract}] is same as [{$alias}]"); + } + + self::$aliases[$alias] = $abstract; + } + + /** + * 注册绑定 + * + * @param string $abstract 类或接口名 + * @param null|Closure|string $concrete 返回类实例的闭包,或是类名 + * @param bool $shared 是否共享 + */ + public function bind(string $abstract, $concrete = null, bool $shared = false): void + { + $this->dropStaleInstances($abstract); + + // 如果没有提供闭包,则默认为自动解析类名 + if (is_null($concrete)) { + $concrete = $abstract; + } + + // 如果不是闭包,则认为是类名,此时将其包装在一个闭包中,以方便后续处理 + if (!$concrete instanceof Closure) { + $concrete = $this->getClosure($abstract, $concrete); + } + + self::$bindings[$abstract] = compact('concrete', 'shared'); + } + + /** + * 注册绑定 + * + * 在已经绑定时不会重复注册 + * + * @param string $abstract 类或接口名 + * @param null|Closure|string $concrete 返回类实例的闭包,或是类名 + * @param bool $shared 是否共享 + */ + public function bindIf(string $abstract, $concrete = null, bool $shared = false): void + { + if (!$this->bound($abstract)) { + $this->bind($abstract, $concrete, $shared); + } + } + + /** + * 注册一个单例绑定 + * + * @param string $abstract 类或接口名 + * @param null|Closure|string $concrete 返回类实例的闭包,或是类名 + */ + public function singleton(string $abstract, $concrete = null): void + { + $this->bind($abstract, $concrete, true); + } + + /** + * 注册一个单例绑定 + * + * 在已经绑定时不会重复注册 + * + * @param string $abstract 类或接口名 + * @param null|Closure|string $concrete 返回类实例的闭包,或是类名 + */ + public function singletonIf(string $abstract, $concrete = null): void + { + if (!$this->bound($abstract)) { + $this->singleton($abstract, $concrete); + } + } + + /** + * 注册一个已有的实例,效果等同于单例绑定 + * + * @param string $abstract 类或接口名 + * @param mixed $instance 实例 + * @return mixed + */ + public function instance(string $abstract, $instance) + { + if (isset(self::$instances[$abstract])) { + return self::$instances[$abstract]; + } + + self::$instances[$abstract] = $instance; + + return $instance; + } + + /** + * 获取一个解析对应类实例的闭包 + * + * @param string $abstract 类或接口名 + */ + public function factory(string $abstract): Closure + { + return function () use ($abstract) { + return $this->make($abstract); + }; + } + + /** + * 清除所有绑定和实例 + */ + public function flush(): void + { + self::$aliases = []; + self::$bindings = []; + self::$instances = []; + + $this->shared = []; + $this->buildStack = []; + $this->with = []; + } + + /** + * 获取一个绑定的实例 + * + * @param string $abstract 类或接口名 + * @param array $parameters 参数 + * @throws EntryResolutionException + * @throws ReflectionException + * @return mixed 实例 + */ + public function make(string $abstract, array $parameters = []) + { + $abstract = $this->getAlias($abstract); + + $needs_contextual_build = !empty($parameters); + + if (isset($this->shared[$abstract])) { + return $this->shared[$abstract]; + } + + // 如果已经存在在实例池中(通常意味着单例绑定),则直接返回该实例 + if (isset(self::$instances[$abstract]) && !$needs_contextual_build) { + return self::$instances[$abstract]; + } + + $this->with[] = $parameters; + + $concrete = $this->getConcrete($abstract); + + // 构造该类的实例,并递归解析所有依赖 + if ($this->isBuildable($concrete, $abstract)) { + $object = $this->build($concrete); + } else { + $object = $this->make($concrete); + } + + // 如果该类存在扩展器(装饰器),则逐个应用到实例 + foreach ($this->getExtenders($abstract) as $extender) { + $object = $extender($object, $this); + } + + // 如果该类被注册为单例,则需要将其存放在实例池中,方便后续取用同一实例 + if (!$needs_contextual_build && $this->isShared($abstract)) { + $this->shared[$abstract] = $object; + } + + // 弹出本次构造的覆盖参数 + array_pop($this->with); + + return $object; + } + + /** + * 实例化具体的类实例 + * + * @param Closure|string $concrete 类名或对应的闭包 + * @throws EntryResolutionException + * @throws ReflectionException + * @return mixed + */ + public function build($concrete) + { + // 如果传入的是闭包,则直接执行并返回 + if ($concrete instanceof Closure) { + return $concrete($this, $this->getLastParameterOverride()); + } + + try { + $reflection = new ReflectionClass($concrete); + } catch (ReflectionException $e) { + throw new EntryResolutionException("指定的类 {$concrete} 不存在"); + } + + if (!$reflection->isInstantiable()) { + $this->notInstantiable($concrete); + } + + $this->buildStack[] = $concrete; + + $constructor = $reflection->getConstructor(); + + // 如果不存在构造函数,则代表不需要进一步解析,直接实例化即可 + if (is_null($constructor)) { + array_pop($this->buildStack); + return new $concrete(); + } + + $dependencies = $constructor->getParameters(); + + // 获取所有依赖的实例 + $instances = $this->resolveDependencies($dependencies); + + array_pop($this->buildStack); + + return $reflection->newInstanceArgs($instances); + } + + /** + * 调用对应的方法,并自动注入依赖 + * + * @param callable $callback 对应的方法 + * @param array $parameters 参数 + * @return mixed + */ + public function call(callable $callback, array $parameters = []) + { + // TODO: Implement call() method. + } + + /** + * Finds an entry of the container by its identifier and returns it. + * + * @param string $id identifier of the entry to look for + * + * @throws NotFoundExceptionInterface no entry was found for **this** identifier + * @throws ContainerExceptionInterface error while retrieving the entry + * + * @return mixed entry + */ + public function get(string $id) + { + try { + return $this->make($id); + } catch (Exception $e) { + if ($this->has($id)) { + throw new EntryResolutionException('', 0, $e); + } + + throw new EntryNotFoundException('', 0, $e); + } + } + + /** + * Returns true if the container can return an entry for the given identifier. + * Returns false otherwise. + * + * `has($id)` returning true does not mean that `get($id)` will not throw an exception. + * It does however mean that `get($id)` will not throw a `NotFoundExceptionInterface`. + * + * @param string $id identifier of the entry to look for + */ + public function has(string $id): bool + { + return $this->bound($id); + } + + /** + * 扩展一个类或接口 + * + * @param string $abstract 类或接口名 + * @param Closure $closure 扩展闭包 + */ + public function extend(string $abstract, Closure $closure): void + { + $abstract = $this->getAlias($abstract); + + // 如果该类已经被解析过,则直接将扩展器应用到该类的实例上 + // 否则,将扩展器存入扩展器池,等待解析 + if (isset(self::$instances[$abstract])) { + self::$instances[$abstract] = $closure(self::$instances[$abstract], $this); + } else { + self::$extenders[$abstract][] = $closure; + } + } + + /** + * 获取对应类型的所有扩展器 + * + * @param string $abstract 类或接口名 + * @return Closure[] + */ + protected function getExtenders(string $abstract): array + { + $abstract = $this->getAlias($abstract); + + return self::$extenders[$abstract] ?? []; + } + + /** + * 判断传入的是否为别名 + */ + protected function isAlias(string $name): bool + { + return array_key_exists($name, self::$aliases); + } + + /** + * 抛弃所有过时的实例和别名 + * + * @param string $abstract 类或接口名 + */ + protected function dropStaleInstances(string $abstract): void + { + unset( + self::$instances[$abstract], + self::$aliases[$abstract], + $this->shared[$abstract] + ); + } + + /** + * 获取一个解析对应类的闭包 + * + * @param string $abstract 类或接口名 + * @param string $concrete 实际类名 + */ + protected function getClosure(string $abstract, string $concrete): Closure + { + return static function ($container, $parameters = []) use ($abstract, $concrete) { + $method = $abstract === $concrete ? 'build' : 'make'; + + return $container->{$method}($concrete, $parameters); + }; + } + + /** + * 获取最后一次的覆盖参数 + */ + protected function getLastParameterOverride(): array + { + return $this->with[count($this->with) - 1] ?? []; + } + + /** + * 抛出实例化异常 + * + * @throws EntryResolutionException + */ + protected function notInstantiable(string $concrete, string $reason = ''): void + { + if (!empty($this->buildStack)) { + $previous = implode(', ', $this->buildStack); + $message = "类 {$concrete} 无法实例化,其被 {$previous} 依赖"; + } else { + $message = "类 {$concrete} 无法实例化"; + } + + throw new EntryResolutionException("{$message}:{$reason}"); + } + + /** + * 解析依赖 + * + * @param ReflectionParameter[] $dependencies + * @throws EntryResolutionException + * @throws ReflectionException + */ + protected function resolveDependencies(array $dependencies): array + { + $results = []; + + foreach ($dependencies as $dependency) { + // 如果此依赖存在覆盖参数,则使用覆盖参数 + // 否则,将尝试解析参数 + if ($this->hasParameterOverride($dependency)) { + $results[] = $this->getParameterOverride($dependency); + continue; + } + + // 如果存在临时注入的依赖,则使用临时注入的依赖 + if ($this->hasParameterTypeOverride($dependency)) { + $results[] = $this->getParameterTypeOverride($dependency); + continue; + } + + // 获取参数类型 + $type = $dependency->getType(); + // 没有声明类型或为基本类型 + if (!$type instanceof ReflectionNamedType || $type->isBuiltin()) { + $class_name = null; + } else { + // 获取类名 + $class_name = $type->getName(); + // 如果存在父类 + if (!is_null($class = $dependency->getDeclaringClass())) { + if ($class_name === 'self') { + $class_name = $class->getName(); + } elseif ($class_name === 'parent') { + $class_name = $class->getParentClass()->getName(); + } + } + } + + // 如果类名为空,则代表此依赖是基本类型,且无法对其进行依赖解析 + $results[] = is_null($class_name) + ? $this->resolvePrimitive($dependency) + : $this->resolveClass($dependency); + } + + return $results; + } + + /** + * 判断传入的参数是否存在覆盖参数 + */ + protected function hasParameterOverride(ReflectionParameter $parameter): bool + { + return array_key_exists($parameter->name, $this->getLastParameterOverride()); + } + + /** + * 获取覆盖参数 + * + * @return mixed + */ + protected function getParameterOverride(ReflectionParameter $parameter) + { + return $this->getLastParameterOverride()[$parameter->name]; + } + + /** + * 判断传入的参数是否存在临时注入的参数 + */ + protected function hasParameterTypeOverride(ReflectionParameter $parameter): bool + { + if (!$parameter->hasType()) { + return false; + } + + $type = $parameter->getType(); + + if (!$type instanceof ReflectionNamedType || $type->isBuiltin()) { + return false; + } + + return array_key_exists($type->getName(), $this->getLastParameterOverride()); + } + + /** + * 获取临时注入的参数 + * + * @return mixed + */ + protected function getParameterTypeOverride(ReflectionParameter $parameter) + { + $type = $parameter->getType(); + + if (!$type instanceof ReflectionNamedType) { + return []; + } + + return $this->getLastParameterOverride()[$type->getName()]; + } + + /** + * 解析基本类型 + * + * @throws EntryResolutionException 如参数不存在默认值,则抛出异常 + * @return mixed 对应类型的默认值 + */ + protected function resolvePrimitive(ReflectionParameter $parameter) + { + if ($parameter->isDefaultValueAvailable()) { + return $parameter->getDefaultValue(); + } + + throw new EntryResolutionException("无法解析类 {$parameter->getDeclaringClass()->getName()} 的参数 {$parameter}"); + } + + /** + * 解析类 + * + * @throws EntryResolutionException 如果无法解析类,则抛出异常 + * @throws ReflectionException 实际上不会抛出,请无视 + * @return mixed + */ + protected function resolveClass(ReflectionParameter $parameter) + { + try { + // 尝试解析 + return $this->make($parameter->getClass()->name); + } catch (EntryResolutionException $e) { + // 如果参数是可选的,则返回默认值 + if ($parameter->isOptional()) { + return $parameter->getDefaultValue(); + } + + throw $e; + } + } + + /** + * 获取类名的实际类型 + * + * @param string $abstract 类或接口名 + * @return Closure|string + */ + protected function getConcrete(string $abstract) + { + if (isset(self::$bindings[$abstract])) { + return self::$bindings[$abstract]['concrete']; + } + + return $abstract; + } + + /** + * 判断传入的实际类型是否可以构造 + * + * @param mixed $concrete 实际类型 + * @param string $abstract 类或接口名 + */ + protected function isBuildable($concrete, string $abstract): bool + { + return $concrete === $abstract || $concrete instanceof Closure; + } + + /** + * 判断传入的类型是否为共享实例 + * + * @param string $abstract 类或接口名 + */ + protected function isShared(string $abstract): bool + { + return isset($this->instances[$abstract]) + || (isset($this->bindings[$abstract]['shared']) + && $this->bindings[$abstract]['shared'] === true); + } +} diff --git a/tests/ZM/Container/WorkerContainerTest.php b/tests/ZM/Container/WorkerContainerTest.php new file mode 100644 index 00000000..4f6d44e2 --- /dev/null +++ b/tests/ZM/Container/WorkerContainerTest.php @@ -0,0 +1,120 @@ +container = new WorkerContainer(); + $this->container->flush(); + } + + public function testInstance(): void + { + $this->container->instance('test', 'test'); + $this->assertEquals('test', $this->container->make('test')); + + $t2 = new WorkerContainer(); + $this->assertEquals('test', $t2->make('test')); + } + + public function testAlias(): void + { + $this->container->alias(MessageUtil::class, 'bar'); + $this->container->alias('bar', 'baz'); + $this->container->alias('baz', 'bas'); + $this->assertInstanceOf(MessageUtil::class, $this->container->make('bas')); + } + + public function testGetAlias(): void + { + $this->container->alias(MessageUtil::class, 'bar'); + $this->assertEquals(MessageUtil::class, $this->container->getAlias('bar')); + } + + public function testBindClosure(): void + { + $this->container->bind('test', function () { + return 'test'; + }); + $this->assertEquals('test', $this->container->make('test')); + } + + public function testFlush(): void + { + $this->container->bind('test', function () { + return 'test'; + }); + $this->container->flush(); + $this->expectException(EntryResolutionException::class); + $this->container->make('test'); + } + + public function testBindIf(): void + { + $this->container->bind('test', function () { + return 'test'; + }); + $this->container->bindIf('test', function () { + return 'test2'; + }); + $this->assertEquals('test', $this->container->make('test')); + } + + public function testGet(): void + { + $this->testMake(); + } + + public function testBound(): void + { + $this->container->bind('test', function () { + return 'test'; + }); + $this->assertTrue($this->container->bound('test')); + $this->assertFalse($this->container->bound('test2')); + } + + public function testFactory(): void + { + $this->container->bind('test', function () { + return 'test'; + }); + $factory = $this->container->factory('test'); + $this->assertEquals($this->container->make('test'), $factory()); + } + + public function testMake(): void + { + $this->container->bind('test', function () { + return 'test'; + }); + $this->assertEquals('test', $this->container->make('test')); + } + + public function testHas(): void + { + $this->testBound(); + } + + public function testBuild(): void + { + $this->assertEquals('test', $this->container->build(function () { + return 'test'; + })); + $this->assertInstanceOf(MessageUtil::class, $this->container->build(MessageUtil::class)); + } +}