From 1d778fd78e4a3bab9b98d3d405714f3df4b03823 Mon Sep 17 00:00:00 2001 From: sunxyw Date: Mon, 4 Apr 2022 20:51:26 +0800 Subject: [PATCH 01/22] add worker container --- composer.json | 1 + phpstan.neon | 1 + src/ZM/Container/ContainerInterface.php | 108 +++ src/ZM/Container/EntryNotFoundException.php | 12 + src/ZM/Container/EntryResolutionException.php | 12 + src/ZM/Container/WorkerContainer.php | 624 ++++++++++++++++++ tests/ZM/Container/WorkerContainerTest.php | 120 ++++ 7 files changed, 878 insertions(+) create mode 100644 src/ZM/Container/ContainerInterface.php create mode 100644 src/ZM/Container/EntryNotFoundException.php create mode 100644 src/ZM/Container/EntryResolutionException.php create mode 100644 src/ZM/Container/WorkerContainer.php create mode 100644 tests/ZM/Container/WorkerContainerTest.php 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)); + } +} From 33d3341bb343ead272fe491fcc1cca87f26a6eae Mon Sep 17 00:00:00 2001 From: sunxyw Date: Mon, 4 Apr 2022 22:10:58 +0800 Subject: [PATCH 02/22] add request container --- src/ZM/Container/Container.php | 79 ++++++++++++++++++++++++++++ tests/ZM/Container/ContainerTest.php | 30 +++++++++++ 2 files changed, 109 insertions(+) create mode 100644 src/ZM/Container/Container.php create mode 100644 tests/ZM/Container/ContainerTest.php diff --git a/src/ZM/Container/Container.php b/src/ZM/Container/Container.php new file mode 100644 index 00000000..72d05345 --- /dev/null +++ b/src/ZM/Container/Container.php @@ -0,0 +1,79 @@ +parent = $parent; + } + + /** + * 获取父容器 + */ + public function getParent(): ContainerInterface + { + return $this->parent; + } + + /** + * 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) || $this->parent->has($id); + } + + /** + * 获取一个绑定的实例 + * + * @param string $abstract 类或接口名 + * @param array $parameters 参数 + * @throws EntryResolutionException + * @throws ReflectionException + * @return mixed 实例 + */ + public function make(string $abstract, array $parameters = []) + { + if (isset($this->shared[$abstract])) { + return $this->shared[$abstract]; + } + + // 此类没有,父类有,则从父类中获取 + if (!$this->bound($abstract) && $this->parent->bound($abstract)) { + return $this->parent->make($abstract, $parameters); + } + + return parent::make($abstract, $parameters); + } + + /** + * 清除所有绑定和实例 + */ + public function flush(): void + { + parent::flush(); + $this->parent->flush(); + } +} diff --git a/tests/ZM/Container/ContainerTest.php b/tests/ZM/Container/ContainerTest.php new file mode 100644 index 00000000..09951178 --- /dev/null +++ b/tests/ZM/Container/ContainerTest.php @@ -0,0 +1,30 @@ +instance('foo', 'bar'); + + $container = new Container($worker_container); + $container->instance('baz', 'qux'); + + // 获取父容器的实例 + $this->assertEquals('bar', $container->get('foo')); + + // 获取自身容器的实例 + $this->assertEquals('qux', $container->get('baz')); + } +} From 441c8661605ed37fd487940a1ebd0d77453ac646 Mon Sep 17 00:00:00 2001 From: sunxyw Date: Tue, 5 Apr 2022 00:13:22 +0800 Subject: [PATCH 03/22] improve SingletonTrait compatibility --- phpstan.neon | 1 + src/ZM/Utils/SingletonTrait.php | 22 +++++++++++++--------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/phpstan.neon b/phpstan.neon index 80cc3e85..b20bc6c3 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -7,5 +7,6 @@ parameters: - '#Used constant OS_TYPE_(LINUX|WINDOWS) not found#' - '#Constant .* not found#' - '#PHPDoc tag @throws with type Psr\\Container\\ContainerExceptionInterface is not subtype of Throwable#' + - '#Unsafe usage of new static#' dynamicConstantNames: - SWOOLE_VERSION diff --git a/src/ZM/Utils/SingletonTrait.php b/src/ZM/Utils/SingletonTrait.php index b7fe5e08..cfe8d81b 100644 --- a/src/ZM/Utils/SingletonTrait.php +++ b/src/ZM/Utils/SingletonTrait.php @@ -1,30 +1,34 @@ Date: Tue, 5 Apr 2022 00:24:03 +0800 Subject: [PATCH 04/22] add container helper functions --- src/ZM/Container/Container.php | 3 --- src/ZM/Container/WorkerContainer.php | 22 +++++++++++----- src/ZM/global_functions.php | 39 ++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 9 deletions(-) diff --git a/src/ZM/Container/Container.php b/src/ZM/Container/Container.php index 72d05345..af95aab7 100644 --- a/src/ZM/Container/Container.php +++ b/src/ZM/Container/Container.php @@ -4,8 +4,6 @@ declare(strict_types=1); namespace ZM\Container; -use ReflectionException; - class Container extends WorkerContainer { /** @@ -51,7 +49,6 @@ class Container extends WorkerContainer * @param string $abstract 类或接口名 * @param array $parameters 参数 * @throws EntryResolutionException - * @throws ReflectionException * @return mixed 实例 */ public function make(string $abstract, array $parameters = []) diff --git a/src/ZM/Container/WorkerContainer.php b/src/ZM/Container/WorkerContainer.php index 5cb1fe74..4fcad19d 100644 --- a/src/ZM/Container/WorkerContainer.php +++ b/src/ZM/Container/WorkerContainer.php @@ -13,9 +13,12 @@ use ReflectionClass; use ReflectionException; use ReflectionNamedType; use ReflectionParameter; +use ZM\Utils\SingletonTrait; class WorkerContainer implements ContainerInterface { + use SingletonTrait; + /** * @var array */ @@ -210,7 +213,6 @@ class WorkerContainer implements ContainerInterface * @param string $abstract 类或接口名 * @param array $parameters 参数 * @throws EntryResolutionException - * @throws ReflectionException * @return mixed 实例 */ public function make(string $abstract, array $parameters = []) @@ -260,7 +262,6 @@ class WorkerContainer implements ContainerInterface * * @param Closure|string $concrete 类名或对应的闭包 * @throws EntryResolutionException - * @throws ReflectionException * @return mixed */ public function build($concrete) @@ -293,7 +294,12 @@ class WorkerContainer implements ContainerInterface $dependencies = $constructor->getParameters(); // 获取所有依赖的实例 - $instances = $this->resolveDependencies($dependencies); + try { + $instances = $this->resolveDependencies($dependencies); + } catch (EntryResolutionException $e) { + array_pop($this->buildStack); + throw $e; + } array_pop($this->buildStack); @@ -448,7 +454,6 @@ class WorkerContainer implements ContainerInterface * * @param ReflectionParameter[] $dependencies * @throws EntryResolutionException - * @throws ReflectionException */ protected function resolveDependencies(array $dependencies): array { @@ -566,7 +571,6 @@ class WorkerContainer implements ContainerInterface * 解析类 * * @throws EntryResolutionException 如果无法解析类,则抛出异常 - * @throws ReflectionException 实际上不会抛出,请无视 * @return mixed */ protected function resolveClass(ReflectionParameter $parameter) @@ -576,10 +580,16 @@ class WorkerContainer implements ContainerInterface return $this->make($parameter->getClass()->name); } catch (EntryResolutionException $e) { // 如果参数是可选的,则返回默认值 - if ($parameter->isOptional()) { + if ($parameter->isDefaultValueAvailable()) { + array_pop($this->with); return $parameter->getDefaultValue(); } + if ($parameter->isVariadic()) { + array_pop($this->with); + return []; + } + throw $e; } } diff --git a/src/ZM/global_functions.php b/src/ZM/global_functions.php index 460768e8..bfaca2da 100644 --- a/src/ZM/global_functions.php +++ b/src/ZM/global_functions.php @@ -13,6 +13,8 @@ use ZM\API\ZMRobot; use ZM\Config\ZMConfig; use ZM\ConnectionManager\ManagerGM; use ZM\Console\Console; +use ZM\Container\Container; +use ZM\Container\ContainerInterface; use ZM\Context\Context; use ZM\Context\ContextInterface; use ZM\Event\EventManager; @@ -40,6 +42,7 @@ function get_class_path(string $class_name): ?string /** * 检查炸毛框架运行的环境 + * * @internal */ function _zm_env_check() @@ -416,6 +419,7 @@ function server(): Server /** * 获取缓存当前框架pid的临时目录 + * * @internal */ function _zm_pid_dir(): string @@ -433,6 +437,7 @@ function _zm_pid_dir(): string * 随机返回一个 ZMRobot 实例,效果等同于 {@link ZMRobot::getRandom()}。 * * 在单机器人模式下,会直接返回该机器人实例。 + * * @throws RobotNotFoundException */ function bot(): ZMRobot @@ -627,6 +632,7 @@ function zm_internal_errcode($code): string } /** +<<<<<<< HEAD * 将可能为数组的参数转换为字符串 * * 如传入字符串则为原样返回 @@ -638,6 +644,39 @@ function implode_when_necessary($string_or_array): string return is_array($string_or_array) ? implode(', ', $string_or_array) : $string_or_array; } +/** + * 获取容器(请求级)实例 + */ +function container(): ContainerInterface +{ + return Container::getInstance(); +} + +/** + * 解析类实例(使用容器) + * + * @return mixed + */ +function resolve(string $abstract, array $parameters = []) +{ + return Container::getInstance()->make($abstract, $parameters); +} + +/** + * 获取容器实例 + * + * @param null|string $abstract 类或接口名,不传入则返回容器实例 + * @return ContainerInterface|mixed + */ +function app(string $abstract = null, array $parameters = []) +{ + if (is_null($abstract)) { + return container(); + } + + return resolve($abstract, $parameters); +} + /** * 以下为废弃的函数,将于未来移除 */ From 9f7ebceeb946b939dbb601db18513a6c9a11bd43 Mon Sep 17 00:00:00 2001 From: sunxyw Date: Tue, 5 Apr 2022 00:24:37 +0800 Subject: [PATCH 05/22] add phpdoc to ZMBuf --- src/ZM/Store/ZMBuf.php | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/ZM/Store/ZMBuf.php b/src/ZM/Store/ZMBuf.php index 0f2f7e59..a19c24ec 100644 --- a/src/ZM/Store/ZMBuf.php +++ b/src/ZM/Store/ZMBuf.php @@ -10,13 +10,39 @@ declare(strict_types=1); namespace ZM\Store; +use ZM\Context\ContextInterface; + class ZMBuf { + /** + * 注册的事件 + * + * @deprecated 不再使用 + * + * @var array + */ public static $events = []; + /** + * 全局单例容器 + * + * @var array + */ public static $instance = []; + /** + * 上下文容器 + * + * @var array + */ public static $context_class = []; + /** + * 终端输入流? + * + * 目前等用于 STDIN + * + * @var resource + */ public static $terminal; } From 90290bee942130277d277ce5c100ce1eefcffa2b Mon Sep 17 00:00:00 2001 From: sunxyw Date: Tue, 5 Apr 2022 01:06:47 +0800 Subject: [PATCH 06/22] add container generics type hint --- src/ZM/Container/Container.php | 10 ++++------ src/ZM/Container/ContainerInterface.php | 3 ++- src/ZM/Container/WorkerContainer.php | 3 ++- src/ZM/global_functions.php | 9 ++++++--- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/ZM/Container/Container.php b/src/ZM/Container/Container.php index af95aab7..d6c18433 100644 --- a/src/ZM/Container/Container.php +++ b/src/ZM/Container/Container.php @@ -13,12 +13,9 @@ class Container extends WorkerContainer */ protected $parent; - /** - * @param ContainerInterface $parent 父容器 - */ - public function __construct(ContainerInterface $parent) + public function __construct() { - $this->parent = $parent; + $this->parent = WorkerContainer::getInstance(); } /** @@ -46,10 +43,11 @@ class Container extends WorkerContainer /** * 获取一个绑定的实例 * + * @template T * @param string $abstract 类或接口名 * @param array $parameters 参数 * @throws EntryResolutionException - * @return mixed 实例 + * @return T 实例 */ public function make(string $abstract, array $parameters = []) { diff --git a/src/ZM/Container/ContainerInterface.php b/src/ZM/Container/ContainerInterface.php index 3bb170ce..53372b1e 100644 --- a/src/ZM/Container/ContainerInterface.php +++ b/src/ZM/Container/ContainerInterface.php @@ -91,9 +91,10 @@ interface ContainerInterface extends PsrContainerInterface /** * 获取一个绑定的实例 * + * @template T * @param string $abstract 类或接口名 * @param array $parameters 参数 - * @return mixed 实例 + * @return T 实例 */ public function make(string $abstract, array $parameters = []); diff --git a/src/ZM/Container/WorkerContainer.php b/src/ZM/Container/WorkerContainer.php index 4fcad19d..a5bd2ee2 100644 --- a/src/ZM/Container/WorkerContainer.php +++ b/src/ZM/Container/WorkerContainer.php @@ -210,10 +210,11 @@ class WorkerContainer implements ContainerInterface /** * 获取一个绑定的实例 * + * @template T * @param string $abstract 类或接口名 * @param array $parameters 参数 * @throws EntryResolutionException - * @return mixed 实例 + * @return T 实例 */ public function make(string $abstract, array $parameters = []) { diff --git a/src/ZM/global_functions.php b/src/ZM/global_functions.php index bfaca2da..13dd3c69 100644 --- a/src/ZM/global_functions.php +++ b/src/ZM/global_functions.php @@ -655,7 +655,9 @@ function container(): ContainerInterface /** * 解析类实例(使用容器) * - * @return mixed + * @template T + * @param string $abstract + * @return T */ function resolve(string $abstract, array $parameters = []) { @@ -665,8 +667,9 @@ function resolve(string $abstract, array $parameters = []) /** * 获取容器实例 * - * @param null|string $abstract 类或接口名,不传入则返回容器实例 - * @return ContainerInterface|mixed + * @template T + * @param null|string $abstract + * @return ContainerInterface|T */ function app(string $abstract = null, array $parameters = []) { From cad5636f92b84fdca2fe6847cb5c8ca04f95ebdf Mon Sep 17 00:00:00 2001 From: sunxyw Date: Tue, 5 Apr 2022 01:07:51 +0800 Subject: [PATCH 07/22] add basic container usage example --- src/Module/Example/Hello.php | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/Module/Example/Hello.php b/src/Module/Example/Hello.php index 8dba02f8..6e7a2fb2 100644 --- a/src/Module/Example/Hello.php +++ b/src/Module/Example/Hello.php @@ -12,12 +12,15 @@ use ZM\Annotation\Http\RequestMapping; use ZM\Annotation\Swoole\OnCloseEvent; use ZM\Annotation\Swoole\OnOpenEvent; use ZM\Annotation\Swoole\OnRequestEvent; +use ZM\Annotation\Swoole\OnStart; use ZM\API\CQ; use ZM\API\OneBotV11; use ZM\API\TuringAPI; use ZM\Config\ZMConfig; use ZM\ConnectionManager\ConnectionObject; use ZM\Console\Console; +use ZM\Container\WorkerContainer; +use ZM\Context\ContextInterface; use ZM\Event\EventDispatcher; use ZM\Exception\InterruptException; use ZM\Module\QQBot; @@ -244,4 +247,31 @@ class Hello { bot()->all()->allGroups()->sendGroupMsg(0, ctx()->getMessage()); } + + /* + * @OnStart(-1) + */ + #[OnStart(-1)] + public function initContainer() + { + $worker_container = new WorkerContainer(); + $worker_container->bindIf('echo', function ($container, $parameters) { + return 'Hello, ' . $parameters[0]; + }); + container()->bind(ContextInterface::class, function () { + return ctx(); + }); + } + + /** + * 容器呐 + * + * @CQCommand("container") + */ + #[CQCommand('container')] + public function container(): string + { + resolve(ContextInterface::class)->reply('Good'); // ctx()->reply('good') + return resolve('echo', ['hello']); // Hello, hello + } } From 1deea5bebca1e2e89cb3b8063f6c2a1c898ffdea Mon Sep 17 00:00:00 2001 From: sunxyw Date: Tue, 5 Apr 2022 01:14:06 +0800 Subject: [PATCH 08/22] refactor container type hint --- src/ZM/Container/Container.php | 4 ++-- src/ZM/Container/ContainerInterface.php | 6 +++--- src/ZM/Container/WorkerContainer.php | 4 ++-- src/ZM/global_functions.php | 8 ++++---- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/ZM/Container/Container.php b/src/ZM/Container/Container.php index d6c18433..a235aea0 100644 --- a/src/ZM/Container/Container.php +++ b/src/ZM/Container/Container.php @@ -44,10 +44,10 @@ class Container extends WorkerContainer * 获取一个绑定的实例 * * @template T - * @param string $abstract 类或接口名 + * @param class-string $abstract 类或接口名 * @param array $parameters 参数 * @throws EntryResolutionException - * @return T 实例 + * @return Closure|mixed|T 实例 */ public function make(string $abstract, array $parameters = []) { diff --git a/src/ZM/Container/ContainerInterface.php b/src/ZM/Container/ContainerInterface.php index 53372b1e..e667a547 100644 --- a/src/ZM/Container/ContainerInterface.php +++ b/src/ZM/Container/ContainerInterface.php @@ -92,9 +92,9 @@ interface ContainerInterface extends PsrContainerInterface * 获取一个绑定的实例 * * @template T - * @param string $abstract 类或接口名 - * @param array $parameters 参数 - * @return T 实例 + * @param class-string $abstract 类或接口名 + * @param array $parameters 参数 + * @return Closure|mixed|T 实例 */ public function make(string $abstract, array $parameters = []); diff --git a/src/ZM/Container/WorkerContainer.php b/src/ZM/Container/WorkerContainer.php index a5bd2ee2..41d0c634 100644 --- a/src/ZM/Container/WorkerContainer.php +++ b/src/ZM/Container/WorkerContainer.php @@ -211,10 +211,10 @@ class WorkerContainer implements ContainerInterface * 获取一个绑定的实例 * * @template T - * @param string $abstract 类或接口名 + * @param class-string $abstract 类或接口名 * @param array $parameters 参数 * @throws EntryResolutionException - * @return T 实例 + * @return Closure|mixed|T 实例 */ public function make(string $abstract, array $parameters = []) { diff --git a/src/ZM/global_functions.php b/src/ZM/global_functions.php index 13dd3c69..5bbe0d2b 100644 --- a/src/ZM/global_functions.php +++ b/src/ZM/global_functions.php @@ -656,8 +656,8 @@ function container(): ContainerInterface * 解析类实例(使用容器) * * @template T - * @param string $abstract - * @return T + * @param class-string $abstract + * @return Closure|mixed|T */ function resolve(string $abstract, array $parameters = []) { @@ -668,8 +668,8 @@ function resolve(string $abstract, array $parameters = []) * 获取容器实例 * * @template T - * @param null|string $abstract - * @return ContainerInterface|T + * @param null|class-string $abstract + * @return Closure|ContainerInterface|mixed|T */ function app(string $abstract = null, array $parameters = []) { From 58e405cccd25b1b24883a5d06b7fec714b85e63b Mon Sep 17 00:00:00 2001 From: sunxyw Date: Tue, 5 Apr 2022 01:45:18 +0800 Subject: [PATCH 09/22] Revert "add basic container usage example" This reverts commit 86e379c6bd8eb829a947bce463ff68c9de2d4200. --- src/Module/Example/Hello.php | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/Module/Example/Hello.php b/src/Module/Example/Hello.php index 6e7a2fb2..89511251 100644 --- a/src/Module/Example/Hello.php +++ b/src/Module/Example/Hello.php @@ -12,15 +12,12 @@ use ZM\Annotation\Http\RequestMapping; use ZM\Annotation\Swoole\OnCloseEvent; use ZM\Annotation\Swoole\OnOpenEvent; use ZM\Annotation\Swoole\OnRequestEvent; -use ZM\Annotation\Swoole\OnStart; use ZM\API\CQ; use ZM\API\OneBotV11; use ZM\API\TuringAPI; use ZM\Config\ZMConfig; use ZM\ConnectionManager\ConnectionObject; use ZM\Console\Console; -use ZM\Container\WorkerContainer; -use ZM\Context\ContextInterface; use ZM\Event\EventDispatcher; use ZM\Exception\InterruptException; use ZM\Module\QQBot; @@ -30,7 +27,6 @@ use ZM\Utils\ZMUtil; /** * Class Hello - * * @since 2.0 */ class Hello @@ -138,7 +134,6 @@ class Hello * 问法2:从1到20的随机数 * @CQCommand("随机数") * @CQCommand(pattern="*从*到*的随机数") - * * @return string */ public function randNum() @@ -176,7 +171,6 @@ class Hello /** * 使用自定义参数的路由参数 * @RequestMapping("/whoami/{name}") - * * @param array $param 参数 * @return string 返回的 HTML Body */ @@ -188,7 +182,6 @@ class Hello /** * 在机器人连接后向终端输出信息 * @OnOpenEvent("qq") - * * @param ConnectionObject $conn WebSocket 连接对象 */ public function onConnect(ConnectionObject $conn) @@ -208,7 +201,6 @@ class Hello /** * 阻止 Chrome 自动请求 /favicon.ico 导致的多条请求并发和干扰 * @OnRequestEvent(rule="ctx()->getRequest()->server['request_uri'] == '/favicon.ico'",level=200) - * * @throws InterruptException */ public function onRequest() From 46604293e96abc3aa4dc8d4b43b70593308d6d84 Mon Sep 17 00:00:00 2001 From: sunxyw Date: Tue, 5 Apr 2022 15:29:15 +0800 Subject: [PATCH 10/22] remove unnecessary exceptions hint --- src/ZM/global_functions.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/ZM/global_functions.php b/src/ZM/global_functions.php index 5bbe0d2b..422fdf45 100644 --- a/src/ZM/global_functions.php +++ b/src/ZM/global_functions.php @@ -241,8 +241,6 @@ function set_coroutine_params(array $params): void /** * 获取当前上下文 - * - * @throws ZMKnownException */ function context(): ContextInterface { @@ -251,8 +249,6 @@ function context(): ContextInterface /** * 获取当前上下文 - * - * @throws ZMKnownException */ function ctx(): ContextInterface { From f5d34198e7e9655272b72c1b08e21b39d4104ec7 Mon Sep 17 00:00:00 2001 From: sunxyw Date: Thu, 7 Apr 2022 19:09:02 +0800 Subject: [PATCH 11/22] add container call method --- src/ZM/Container/BoundMethod.php | 137 ++++++++++++++++++++++++ src/ZM/Container/ContainerInterface.php | 7 +- src/ZM/Container/WorkerContainer.php | 30 ++---- 3 files changed, 148 insertions(+), 26 deletions(-) create mode 100644 src/ZM/Container/BoundMethod.php diff --git a/src/ZM/Container/BoundMethod.php b/src/ZM/Container/BoundMethod.php new file mode 100644 index 00000000..e2a6ae40 --- /dev/null +++ b/src/ZM/Container/BoundMethod.php @@ -0,0 +1,137 @@ +make($callback[0]); + } + + if (!is_callable($callback)) { + throw new InvalidArgumentException('Callback is not callable.'); + } + + return call_user_func_array($callback, self::getMethodDependencies($container, $callback, $parameters)); + } + + /** + * 判断调用的是否为非静态方法 + * + * @param array|string $callback + * @throws ReflectionException + */ + protected static function isCallingNonStaticMethod($callback): bool + { + if (is_array($callback) && is_string($callback[0])) { + $reflection = new ReflectionMethod($callback[0], $callback[1]); + return !$reflection->isStatic(); + } + return false; + } + + /** + * Get all dependencies for a given method. + * + * @param callable|string $callback + * @throws ReflectionException + */ + protected static function getMethodDependencies(ContainerInterface $container, $callback, array $parameters = []): array + { + $dependencies = []; + + foreach (static::getCallReflector($callback)->getParameters() as $parameter) { + static::addDependencyForCallParameter($container, $parameter, $parameters, $dependencies); + } + + return array_merge($dependencies, array_values($parameters)); + } + + /** + * Get the proper reflection instance for the given callback. + * + * @param callable|string $callback + * @throws \ReflectionException + * @return ReflectionFunctionAbstract + */ + protected static function getCallReflector($callback) + { + if (is_string($callback) && str_contains($callback, '::')) { + $callback = explode('::', $callback); + } elseif (is_object($callback) && !$callback instanceof Closure) { + $callback = [$callback, '__invoke']; + } + + return is_array($callback) + ? new ReflectionMethod($callback[0], $callback[1]) + : new ReflectionFunction($callback); + } + + /** + * Get the dependency for the given call parameter. + * + * @throws EntryResolutionException + */ + protected static function addDependencyForCallParameter( + ContainerInterface $container, + ReflectionParameter $parameter, + array &$parameters, + array &$dependencies + ): void { + if (array_key_exists($param_name = $parameter->getName(), $parameters)) { + $dependencies[] = $parameters[$param_name]; + + unset($parameters[$param_name]); + } elseif (!is_null($class_name = ReflectionUtil::getParameterClassName($parameter))) { + if (array_key_exists($class_name, $parameters)) { + $dependencies[] = $parameters[$class_name]; + + unset($parameters[$class_name]); + } elseif ($parameter->isVariadic()) { + $variadic_dependencies = $container->make($class_name); + + $dependencies = array_merge($dependencies, is_array($variadic_dependencies) + ? $variadic_dependencies + : [$variadic_dependencies]); + } else { + $dependencies[] = $container->make($class_name); + } + } elseif ($parameter->isDefaultValueAvailable()) { + $dependencies[] = $parameter->getDefaultValue(); + } elseif (!array_key_exists($param_name, $parameters) && !$parameter->isOptional()) { + $message = "无法解析类 {$parameter->getDeclaringClass()->getName()} 的依赖 {$parameter}"; + + throw new EntryResolutionException($message); + } + } +} diff --git a/src/ZM/Container/ContainerInterface.php b/src/ZM/Container/ContainerInterface.php index e667a547..26616c66 100644 --- a/src/ZM/Container/ContainerInterface.php +++ b/src/ZM/Container/ContainerInterface.php @@ -101,9 +101,10 @@ interface ContainerInterface extends PsrContainerInterface /** * 调用对应的方法,并自动注入依赖 * - * @param callable $callback 对应的方法 - * @param array $parameters 参数 + * @param callable $callback 对应的方法 + * @param array $parameters 参数 + * @param null|string $default_method 默认方法 * @return mixed */ - public function call(callable $callback, array $parameters = []); + public function call(callable $callback, array $parameters = [], string $default_method = null); } diff --git a/src/ZM/Container/WorkerContainer.php b/src/ZM/Container/WorkerContainer.php index 41d0c634..8f54b999 100644 --- a/src/ZM/Container/WorkerContainer.php +++ b/src/ZM/Container/WorkerContainer.php @@ -13,6 +13,7 @@ use ReflectionClass; use ReflectionException; use ReflectionNamedType; use ReflectionParameter; +use ZM\Utils\ReflectionUtil; use ZM\Utils\SingletonTrait; class WorkerContainer implements ContainerInterface @@ -310,13 +311,14 @@ class WorkerContainer implements ContainerInterface /** * 调用对应的方法,并自动注入依赖 * - * @param callable $callback 对应的方法 - * @param array $parameters 参数 + * @param callable|string $callback 对应的方法 + * @param array $parameters 参数 + * @param null|string $default_method 默认方法 * @return mixed */ - public function call(callable $callback, array $parameters = []) + public function call($callback, array $parameters = [], string $default_method = null) { - // TODO: Implement call() method. + return BoundMethod::call($this, $callback, $parameters, $default_method); } /** @@ -474,26 +476,8 @@ class WorkerContainer implements ContainerInterface 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) + $results[] = is_null(ReflectionUtil::getParameterClassName($dependency)) ? $this->resolvePrimitive($dependency) : $this->resolveClass($dependency); } From a43a4a429ee544c80c0f25b6b41ff5cc4e089214 Mon Sep 17 00:00:00 2001 From: sunxyw Date: Thu, 7 Apr 2022 19:09:28 +0800 Subject: [PATCH 12/22] add reflection util --- src/ZM/Utils/ReflectionUtil.php | 43 +++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 src/ZM/Utils/ReflectionUtil.php diff --git a/src/ZM/Utils/ReflectionUtil.php b/src/ZM/Utils/ReflectionUtil.php new file mode 100644 index 00000000..faea361a --- /dev/null +++ b/src/ZM/Utils/ReflectionUtil.php @@ -0,0 +1,43 @@ +getType(); + // 没有声明类型或为基本类型 + if (!$type instanceof ReflectionNamedType || $type->isBuiltin()) { + return null; + } + + // 获取类名 + $class_name = $type->getName(); + + // 如果存在父类 + if (!is_null($class = $parameter->getDeclaringClass())) { + if ($class_name === 'self') { + return $class->getName(); + } + + if ($class_name === 'parent' && $parent = $class->getParentClass()) { + return $parent->getName(); + } + } + + return $class_name; + } +} From e37629aaea508f0347a96a9aff6042ce81281f61 Mon Sep 17 00:00:00 2001 From: sunxyw Date: Thu, 7 Apr 2022 19:09:53 +0800 Subject: [PATCH 13/22] add container call test --- tests/ZM/Container/ContainerCallTest.php | 130 +++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 tests/ZM/Container/ContainerCallTest.php diff --git a/tests/ZM/Container/ContainerCallTest.php b/tests/ZM/Container/ContainerCallTest.php new file mode 100644 index 00000000..213f1f59 --- /dev/null +++ b/tests/ZM/Container/ContainerCallTest.php @@ -0,0 +1,130 @@ +assertEquals('foo', $container->call(Invokable::class, ['echo' => 'foo'])); + } + + public function testCallClassMethodWithDependencies(): void + { + $container = new Container(); + $name = 'Steve' . time(); + $container->bind(FooDependency::class, FooDependencyImpl::class); + $container->bind(BarDependency::class, function () use ($name) { + return new BarDependencyImpl($name); + }); + $this->assertEquals("hello, {$name}", $container->call([Foo::class, 'sayHello'])); + } + + public function testCallClassStaticMethodWithDependencies(): void + { + $container = new Container(); + $name = 'Alex' . time(); + $container->bind(FooDependency::class, FooDependencyImpl::class); + $container->bind(BarDependency::class, function () use ($name) { + return new BarDependencyImpl($name); + }); + $this->assertEquals("hello, {$name}", $container->call([Foo::class, 'staticSayHello'])); + } + + public function testCallClassMethodWithDependenciesInjectedByConstructor(): void + { + $container = new Container(); + $name = 'Donny' . time(); + $container->bind(FooDependency::class, FooDependencyImpl::class); + $container->bind(BarDependency::class, function () use ($name) { + return new BarDependencyImpl($name); + }); + $this->assertEquals('goodbye', $container->call([Foo::class, 'sayGoodbye'])); + } + + public function testCallClassStaticMethodViaDoubleColons(): void + { + $container = new Container(); + $name = 'Herobrine' . time(); + $container->bind(FooDependency::class, FooDependencyImpl::class); + $container->bind(BarDependency::class, function () use ($name) { + return new BarDependencyImpl($name); + }); + $this->assertEquals("hello, {$name}", $container->call(Foo::class . '::staticSayHello')); + } +} + +class Invokable +{ + public function __invoke(string $echo) + { + return $echo; + } +} + +interface FooDependency +{ + public function sayGoodbye(): string; +} + +class FooDependencyImpl implements FooDependency +{ + public function sayGoodbye(): string + { + return 'goodbye'; + } +} + +interface BarDependency +{ + public function getName(): string; +} + +class BarDependencyImpl implements BarDependency +{ + private $name; + + public function __construct(string $name) + { + $this->name = $name; + } + + public function getName(): string + { + return $this->name; + } +} + +class Foo +{ + private $fooDependency; + + public function __construct(FooDependency $fooDependency) + { + $this->fooDependency = $fooDependency; + } + + public function sayHello(BarDependency $barDependency): string + { + return 'hello, ' . $barDependency->getName(); + } + + public static function staticSayHello(BarDependency $barDependency): string + { + return 'hello, ' . $barDependency->getName(); + } + + public function sayGoodbye(): string + { + return $this->fooDependency->sayGoodbye(); + } +} From 721c0834db12fed4423e444b892805eb29373bd2 Mon Sep 17 00:00:00 2001 From: sunxyw Date: Sun, 10 Apr 2022 00:58:07 +0800 Subject: [PATCH 14/22] inject container into framework --- src/Module/Example/Hello.php | 14 ++ src/ZM/Event/SwooleEvent/OnMessage.php | 17 ++ src/ZM/Event/SwooleEvent/OnWorkerStart.php | 10 +- src/ZM/Event/SwooleEvent/OnWorkerStop.php | 6 + src/ZM/Framework.php | 279 +++++++++++++-------- src/ZM/Utils/DataProvider.php | 22 ++ src/ZM/global_functions.php | 14 +- 7 files changed, 244 insertions(+), 118 deletions(-) diff --git a/src/Module/Example/Hello.php b/src/Module/Example/Hello.php index 89511251..7f21bfad 100644 --- a/src/Module/Example/Hello.php +++ b/src/Module/Example/Hello.php @@ -27,6 +27,7 @@ use ZM\Utils\ZMUtil; /** * Class Hello + * * @since 2.0 */ class Hello @@ -68,6 +69,7 @@ class Hello */ public function hello() { + zm_dump(resolve('swoole_server')->worker_id); return '你好啊,我是由炸毛框架构建的机器人!'; } @@ -134,10 +136,12 @@ class Hello * 问法2:从1到20的随机数 * @CQCommand("随机数") * @CQCommand(pattern="*从*到*的随机数") + * * @return string */ public function randNum() { + zm_dump(resolve('swoole_server')->worker_id); // 获取第一个数字类型的参数 $num1 = ctx()->getNumArg('请输入第一个数字'); // 获取第二个数字类型的参数 @@ -171,6 +175,7 @@ class Hello /** * 使用自定义参数的路由参数 * @RequestMapping("/whoami/{name}") + * * @param array $param 参数 * @return string 返回的 HTML Body */ @@ -182,11 +187,18 @@ class Hello /** * 在机器人连接后向终端输出信息 * @OnOpenEvent("qq") + * * @param ConnectionObject $conn WebSocket 连接对象 */ public function onConnect(ConnectionObject $conn) { + zm_dump(resolve('path.base')); + zm_dump(resolve('path.config')); + zm_dump(resolve('app')::$server->worker_id); Console::info('机器人 ' . $conn->getOption('connect_id') . ' 已连接!'); + container()->bind('cid', function () use ($conn) { + return resolve('app')::$server->worker_id . $conn->getOption('connect_id'); + }); } /** @@ -195,12 +207,14 @@ class Hello */ public function onDisconnect(ConnectionObject $conn) { + zm_dump(resolve('cid')); Console::info('机器人 ' . $conn->getOption('connect_id') . ' 已断开连接!'); } /** * 阻止 Chrome 自动请求 /favicon.ico 导致的多条请求并发和干扰 * @OnRequestEvent(rule="ctx()->getRequest()->server['request_uri'] == '/favicon.ico'",level=200) + * * @throws InterruptException */ public function onRequest() diff --git a/src/ZM/Event/SwooleEvent/OnMessage.php b/src/ZM/Event/SwooleEvent/OnMessage.php index a365b66c..b7ddbdfa 100644 --- a/src/ZM/Event/SwooleEvent/OnMessage.php +++ b/src/ZM/Event/SwooleEvent/OnMessage.php @@ -5,15 +5,18 @@ declare(strict_types=1); namespace ZM\Event\SwooleEvent; use Swoole\Coroutine; +use Swoole\Http\Server; use Swoole\WebSocket\Frame; use Throwable; use ZM\Annotation\Swoole\OnMessageEvent; use ZM\Annotation\Swoole\OnSwooleEvent; use ZM\Annotation\Swoole\SwooleHandler; +use ZM\ConnectionManager\ConnectionObject; use ZM\ConnectionManager\ManagerGM; use ZM\Console\Console; use ZM\Console\TermColor; use ZM\Context\Context; +use ZM\Context\ContextInterface; use ZM\Event\EventDispatcher; use ZM\Event\SwooleEvent; @@ -29,6 +32,15 @@ class OnMessage implements SwooleEvent unset(Context::$context[Coroutine::getCid()]); $conn = ManagerGM::get($frame->fd); set_coroutine_params(['server' => $server, 'frame' => $frame, 'connection' => $conn]); + + container()->instance(Server::class, $server); + container()->instance(Frame::class, $frame); + container()->instance(ConnectionObject::class, $conn); + container()->bind(ContextInterface::class, function () { + return ctx(); + }); + container()->alias(ContextInterface::class, Context::class); + $dispatcher1 = new EventDispatcher(OnMessageEvent::class); $dispatcher1->setRuleFunction(function ($v) use ($conn) { return $v->connect_type === $conn->getName() && ($v->getRule() === '' || eval('return ' . $v->getRule() . ';')); @@ -56,6 +68,11 @@ class OnMessage implements SwooleEvent $error_msg = $e->getMessage() . ' at ' . $e->getFile() . '(' . $e->getLine() . ')'; Console::error(zm_internal_errcode('E00017') . 'Uncaught ' . get_class($e) . ' when calling "message": ' . $error_msg); Console::trace(); + } finally { + container()->flush(); + if (Console::getLevel() >= 4) { + Console::debug(sprintf('Request container [fd=%d, cid=%d] flushed.', $frame->fd, Coroutine::getCid())); + } } } } diff --git a/src/ZM/Event/SwooleEvent/OnWorkerStart.php b/src/ZM/Event/SwooleEvent/OnWorkerStart.php index 18cdc98c..893d4b8f 100644 --- a/src/ZM/Event/SwooleEvent/OnWorkerStart.php +++ b/src/ZM/Event/SwooleEvent/OnWorkerStart.php @@ -20,6 +20,7 @@ use ZM\Annotation\Swoole\OnStart; use ZM\Annotation\Swoole\SwooleHandler; use ZM\Config\ZMConfig; use ZM\Console\Console; +use ZM\Container\WorkerContainer; use ZM\Context\Context; use ZM\Context\ContextInterface; use ZM\DB\DB; @@ -52,6 +53,13 @@ class OnWorkerStart implements SwooleEvent SignalListener::signalWorker($server, $worker_id); } unset(Context::$context[Coroutine::getCid()]); + + /* @noinspection PhpExpressionResultUnusedInspection */ + new WorkerContainer(); + if (Console::getLevel() >= 4) { + Console::debug(sprintf('Worker container [id=%d,cid=%d] booted', $worker_id, Coroutine::getCid())); + } + if ($server->taskworker === false) { Framework::saveProcessState(ZM_PROCESS_WORKER, $server->worker_pid, ['worker_id' => $worker_id]); zm_atomic('_#worker_' . $worker_id)->set($server->worker_pid); @@ -274,7 +282,7 @@ class OnWorkerStart implements SwooleEvent (new PDOConfig()) ->withHost($real_conf['host']) ->withPort($real_conf['port']) - // ->withUnixSocket('/tmp/mysql.sock') + // ->withUnixSocket('/tmp/mysql.sock') ->withDbName($real_conf['dbname']) ->withCharset($real_conf['charset']) ->withUsername($real_conf['username']) diff --git a/src/ZM/Event/SwooleEvent/OnWorkerStop.php b/src/ZM/Event/SwooleEvent/OnWorkerStop.php index 17c89976..919916ed 100644 --- a/src/ZM/Event/SwooleEvent/OnWorkerStop.php +++ b/src/ZM/Event/SwooleEvent/OnWorkerStop.php @@ -8,6 +8,7 @@ use Swoole\Server; use ZM\Annotation\Swoole\SwooleHandler; use ZM\Config\ZMConfig; use ZM\Console\Console; +use ZM\Container\WorkerContainer; use ZM\Event\SwooleEvent; use ZM\Framework; use ZM\Store\LightCache; @@ -20,6 +21,11 @@ class OnWorkerStop implements SwooleEvent { public function onCall(Server $server, $worker_id) { + WorkerContainer::getInstance()->flush(); + if (Console::getLevel() >= 4) { + Console::debug(sprintf('Worker container [id=%d] flushed', $worker_id)); + } + if ($worker_id == (ZMConfig::get('worker_cache')['worker'] ?? 0)) { LightCache::savePersistence(); } diff --git a/src/ZM/Framework.php b/src/ZM/Framework.php index e651476b..fd079f22 100644 --- a/src/ZM/Framework.php +++ b/src/ZM/Framework.php @@ -19,6 +19,8 @@ use ZM\Config\ZMConfig; use ZM\ConnectionManager\ManagerGM; use ZM\Console\Console; use ZM\Console\TermColor; +use ZM\Container\Container; +use ZM\Container\ContainerInterface; use ZM\Exception\ZMKnownException; use ZM\Store\LightCache; use ZM\Store\LightCacheInside; @@ -27,70 +29,75 @@ use ZM\Store\ZMAtomic; use ZM\Utils\DataProvider; use ZM\Utils\Terminal; use ZM\Utils\ZMUtil; -use function date_default_timezone_set; -use function define; -use function exec; -use function explode; -use function file_exists; -use function file_get_contents; -use function file_put_contents; -use function get_class; -use function get_included_files; -use function in_array; -use function intval; -use function is_array; -use function is_dir; -use function json_decode; -use function json_encode; -use function mkdir; -use function ob_get_clean; -use function ob_start; -use function posix_getpid; -use function str_pad; -use function strlen; -use function substr; -use function swoole_cpu_num; -use function trim; -use function uuidgen; -use function working_dir; -use function zm_atomic; -use function zm_internal_errcode; class Framework { /** + * 框架运行的参数 + * * @var array */ public static $argv; /** + * 通信服务器实例 + * * @var Server */ public static $server; /** + * 框架加载的文件 + * * @var string[] */ public static $loaded_files = []; + /** + * 是否为单文件模式 + * + * @var bool + */ public static $instant_mode = false; /** - * @var null|array|bool|mixed + * Swoole 配置 + * + * @var null|array */ - private $server_set; + private $swoole_config; /** * @var array */ private $setup_events = []; - public function __construct($args = [], $instant_mode = false) + /** + * 容器 + * + * @var ContainerInterface + */ + private $container; + + /** + * 创建一个新的框架实例 + * + * @param array $args 运行参数 + * @param bool $instant_mode 是否为单文件模式 + */ + public function __construct(array $args = [], bool $instant_mode = false) { $tty_width = $this->getTtyWidth(); self::$instant_mode = $instant_mode; self::$argv = $args; - ZMConfig::setDirectory(DataProvider::getSourceRootDir() . '/config'); + + // 初始化全局容器 + $this->container = new Container(); + $this->bindPathsInContainer(); + $this->registerBaseBindings(); + + // 初始化配置 + ZMConfig::setDirectory($this->container->get('path.config')); ZMConfig::setEnv($args['env'] ?? ''); if (ZMConfig::get('global') === false) { echo zm_internal_errcode('E00007') . 'Global config load failed: ' . ZMConfig::$last_error . "\nError path: " . DataProvider::getSourceRootDir() . "\nPlease init first!\nSee: https://github.com/zhamao-robot/zhamao-framework/issues/37\n"; @@ -98,19 +105,15 @@ class Framework } // 定义常量 - include_once 'global_defines.php'; + require_once 'global_defines.php'; + // 确保目录存在 + DataProvider::createIfNotExists(app('path.data')); + DataProvider::createIfNotExists(app('path.config')); + DataProvider::createIfNotExists(ZMConfig::get('global', 'crash_dir')); + + // 初始化连接池? try { - $sw = ZMConfig::get('global'); - if (!is_dir($sw['zm_data'])) { - mkdir($sw['zm_data']); - } - if (!is_dir($sw['config_dir'])) { - mkdir($sw['config_dir']); - } - if (!is_dir($sw['crash_dir'])) { - mkdir($sw['crash_dir']); - } ManagerGM::init(ZMConfig::get('global', 'swoole')['max_connection'] ?? 2048, 0.5, [ [ 'key' => 'connect_id', @@ -126,98 +129,113 @@ class Framework echo zm_internal_errcode('E00008') . $e->getMessage() . PHP_EOL; exit(1); } + try { + // 初始化日志 Console::init( ZMConfig::get('global', 'info_level') ?? 2, self::$server, $args['log-theme'] ?? 'default', ($o = ZMConfig::get('console_color')) === false ? [] : $o ); + // 是否同步输出到文件 if ((ZMConfig::get('global', 'runtime')['save_console_log_file'] ?? false) !== false) { Console::setOutputFile(ZMConfig::get('global', 'runtime')['save_console_log_file']); } + // 设置默认时区 $timezone = ZMConfig::get('global', 'timezone') ?? 'Asia/Shanghai'; date_default_timezone_set($timezone); - $this->server_set = ZMConfig::get('global', 'swoole'); - $this->server_set['log_level'] = SWOOLE_LOG_DEBUG; + // 读取 Swoole 配置 + $this->swoole_config = ZMConfig::get('global', 'swoole'); + $this->swoole_config['log_level'] = SWOOLE_LOG_DEBUG; + + // 是否启用远程终端 $add_port = ZMConfig::get('global', 'remote_terminal')['status'] ?? false; + // 加载服务器事件 if (!$instant_mode) { $this->loadServerEvents(); } + // 解析命令行参数 $this->parseCliArgs(self::$argv, $add_port); - if (!isset($this->server_set['max_wait_time'])) { - $this->server_set['max_wait_time'] = 5; + // 设置默认最长等待时间 + if (!isset($this->swoole_config['max_wait_time'])) { + $this->swoole_config['max_wait_time'] = 5; } - $worker = $this->server_set['worker_num'] ?? swoole_cpu_num(); + // 设置最大 worker 进程数 + $worker = $this->swoole_config['worker_num'] ?? swoole_cpu_num(); define('ZM_WORKER_NUM', $worker); + // 初始化原子计数器 ZMAtomic::init(); - $out['working_dir'] = DataProvider::getWorkingDir(); - - // 打印初始信息 - $out['listen'] = ZMConfig::get('global', 'host') . ':' . ZMConfig::get('global', 'port'); - if (!isset($this->server_set['worker_num'])) { - if ((ZMConfig::get('global', 'runtime')['swoole_server_mode'] ?? SWOOLE_PROCESS) == SWOOLE_PROCESS) { - $out['worker'] = swoole_cpu_num() . ' (auto)'; - } else { - $out['single_proc_mode'] = 'true'; - } - } else { - $out['worker'] = $this->server_set['worker_num']; - } - $out['environment'] = ($args['env'] ?? null) === null ? 'default' : $args['env']; - $out['log_level'] = Console::getLevel(); - $out['version'] = ZM_VERSION . (LOAD_MODE == 0 ? (' (build ' . ZM_VERSION_ID . ')') : ''); - $out['master_pid'] = posix_getpid(); - if (APP_VERSION !== 'unknown') { - $out['app_version'] = APP_VERSION; - } - if (isset($this->server_set['task_worker_num'])) { - $out['task_worker'] = $this->server_set['task_worker_num']; - } - if ((ZMConfig::get('global', 'sql_config')['sql_host'] ?? '') !== '') { - $conf = ZMConfig::get('global', 'sql_config'); - $out['mysql_pool'] = $conf['sql_database'] . '@' . $conf['sql_host'] . ':' . $conf['sql_port']; - } - if ((ZMConfig::get('global', 'mysql_config')['host'] ?? '') !== '') { - $conf = ZMConfig::get('global', 'mysql_config'); - $out['mysql'] = $conf['dbname'] . '@' . $conf['host'] . ':' . $conf['port']; - } - if (ZMConfig::get('global', 'redis_config')['host'] !== '') { - $conf = ZMConfig::get('global', 'redis_config'); - $out['redis_pool'] = $conf['host'] . ':' . $conf['port']; - } - if (ZMConfig::get('global', 'static_file_server')['status'] !== false) { - $out['static_file_server'] = 'enabled'; - } - if (self::$argv['show-php-ver'] !== false) { - $out['php_version'] = PHP_VERSION; - $out['swoole_version'] = SWOOLE_VERSION; - } - - if ($add_port) { - $conf = ZMConfig::get('global', 'remote_terminal'); - $out['terminal'] = $conf['host'] . ':' . $conf['port']; - } + // 非静默模式下打印启动信息 if (!self::$argv['private-mode']) { + $out['working_dir'] = DataProvider::getWorkingDir(); + $out['listen'] = ZMConfig::get('global', 'host') . ':' . ZMConfig::get('global', 'port'); + if (!isset($this->swoole_config['worker_num'])) { + if ((ZMConfig::get('global', 'runtime')['swoole_server_mode'] ?? SWOOLE_PROCESS) === SWOOLE_PROCESS) { + $out['worker'] = swoole_cpu_num() . ' (auto)'; + } else { + $out['single_proc_mode'] = 'true'; + } + } else { + $out['worker'] = $this->swoole_config['worker_num']; + } + $out['environment'] = ($args['env'] ?? null) === null ? 'default' : $args['env']; + $out['log_level'] = Console::getLevel(); + $out['version'] = ZM_VERSION . (LOAD_MODE === 0 ? (' (build ' . ZM_VERSION_ID . ')') : ''); + $out['master_pid'] = posix_getpid(); + if (APP_VERSION !== 'unknown') { + $out['app_version'] = APP_VERSION; + } + if (isset($this->swoole_config['task_worker_num'])) { + $out['task_worker'] = $this->swoole_config['task_worker_num']; + } + if ((ZMConfig::get('global', 'sql_config')['sql_host'] ?? '') !== '') { + $conf = ZMConfig::get('global', 'sql_config'); + $out['mysql_pool'] = $conf['sql_database'] . '@' . $conf['sql_host'] . ':' . $conf['sql_port']; + } + if ((ZMConfig::get('global', 'mysql_config')['host'] ?? '') !== '') { + $conf = ZMConfig::get('global', 'mysql_config'); + $out['mysql'] = $conf['dbname'] . '@' . $conf['host'] . ':' . $conf['port']; + } + if (ZMConfig::get('global', 'redis_config')['host'] !== '') { + $conf = ZMConfig::get('global', 'redis_config'); + $out['redis_pool'] = $conf['host'] . ':' . $conf['port']; + } + if (ZMConfig::get('global', 'static_file_server')['status'] !== false) { + $out['static_file_server'] = 'enabled'; + } + if (self::$argv['show-php-ver'] !== false) { + $out['php_version'] = PHP_VERSION; + $out['swoole_version'] = SWOOLE_VERSION; + } + + if ($add_port) { + $conf = ZMConfig::get('global', 'remote_terminal'); + $out['terminal'] = $conf['host'] . ':' . $conf['port']; + } self::printProps($out, $tty_width, $args['log-theme'] === null); } + + // 预览模式则直接提出 if ($args['preview'] ?? false) { exit(); } + // 初始化服务器 self::$server = new Server( ZMConfig::get('global', 'host'), ZMConfig::get('global', 'port'), ZMConfig::get('global', 'runtime')['swoole_server_mode'] ?? SWOOLE_PROCESS ); + // 监听远程终端 if ($add_port) { $conf = ZMConfig::get('global', 'remote_terminal') ?? [ 'status' => true, @@ -231,9 +249,11 @@ class Framework $port->set([ 'open_http_protocol' => false, ]); - $port->on('connect', function (?\Swoole\Server $serv, $fd) use ($welcome_msg, $conf) { + $port->on('connect', function (\Swoole\Server $serv, $fd) use ($welcome_msg, $conf) { ManagerGM::pushConnect($fd, 'terminal'); + // 推送欢迎信息 $serv->send($fd, file_get_contents(working_dir() . '/config/motd.txt')); + // 要求输入令牌 if (!empty($conf['token'])) { $serv->send($fd, 'Please input token: '); } else { @@ -294,16 +314,22 @@ class Framework }); } - self::$server->set($this->server_set); + // 设置服务器配置 + self::$server->set($this->swoole_config); Console::setServer(self::$server); + + // 非静默模式下,打印欢迎信息 if (!self::$argv['private-mode']) { self::printMotd($tty_width); } global $asd; $asd = get_included_files(); + // 注册 Swoole Server 的事件 $this->registerServerEvents(); + + // 初始化缓存 $r = ZMConfig::get('global', 'light_cache') ?? [ 'size' => 512, // 最多允许储存的条数(需要2的倍数) 'max_strlen' => 32768, // 单行字符串最大长度(需要2的倍数) @@ -313,8 +339,12 @@ class Framework ]; LightCache::init($r); LightCacheInside::init(); + + // 初始化自旋锁 SpinLock::init($r['size']); - set_error_handler(function ($error_no, $error_msg, $error_file, $error_line) { + + // 注册全局错误处理器 + set_error_handler(static function ($error_no, $error_msg, $error_file, $error_line) { switch ($error_no) { case E_WARNING: $level_tips = 'PHP Warning: '; @@ -343,13 +373,14 @@ class Framework default: $level_tips = 'Unkonw Type Error: '; break; - } // do some handle + } $error = $level_tips . $error_msg . ' in ' . $error_file . ' on ' . $error_line; - Console::warning($error); // 如果 return false 则错误会继续递交给 PHP 标准错误处理 / + Console::warning($error); + // 如果 return false 则错误会继续递交给 PHP 标准错误处理 return true; }, E_ALL | E_STRICT); } catch (Exception $e) { - Console::error('Framework初始化出现错误,请检查!'); + Console::error('框架初始化出现异常,请检查!'); Console::error(zm_internal_errcode('E00010') . $e->getMessage()); Console::debug($e); exit; @@ -358,6 +389,7 @@ class Framework /** * 将各进程的pid写入文件,以备后续崩溃及僵尸进程处理使用 + * * @param int|string $pid * @internal */ @@ -394,6 +426,7 @@ class Framework /** * 用于框架内部获取多进程运行状态的函数 + * * @param mixed $id_or_name * @throws ZMKnownException * @return false|int|mixed @@ -510,8 +543,8 @@ class Framework public static function printProps($out, $tty_width, $colorful = true) { - $max_border = $tty_width < 65 ? $tty_width : 65; - if (LOAD_MODE == 0) { + $max_border = min($tty_width, 65); + if (LOAD_MODE === 0) { echo Console::setColor("* Framework started with source mode.\n", $colorful ? 'yellow' : ''); } echo str_pad('', $max_border, '=') . PHP_EOL; @@ -526,7 +559,7 @@ class Framework } // Console::info("行宽[$current_line]:".$line_width[$current_line]); if ($max_border >= 57) { // 很宽的时候,一行能放两个短行 - if ($line_width[$current_line] == ($max_border - 2)) { // 空行 + if ($line_width[$current_line] === ($max_border - 2)) { // 空行 self::writeNoDouble($k, $v, $line_data, $line_width, $current_line, $colorful, $max_border); } else { // 不是空行,已经有东西了 $tmp_line = $k . ': ' . $v; @@ -582,6 +615,30 @@ class Framework return file_put_contents(DataProvider::getDataFolder() . '.state.json', json_encode($data, 64 | 128 | 256)); } + /** + * 注册所有应用路径到容器 + */ + protected function bindPathsInContainer(): void + { + $this->container->instance('path.working', DataProvider::getWorkingDir()); + $this->container->instance('path.source', DataProvider::getSourceRootDir()); + $this->container->alias('path.source', 'path.base'); + $this->container->instance('path.config', DataProvider::getSourceRootDir() . '/config'); + $this->container->singleton('path.data', function () { + return DataProvider::getDataFolder(); + }); + $this->container->instance('path.framework', DataProvider::getFrameworkRootDir()); + } + + /** + * 注册基础绑定到容器 + */ + protected function registerBaseBindings(): void + { + $this->container->instance('framework', $this); + $this->container->alias('framework', 'app'); + } + private static function printMotd($tty_width) { if (file_exists(DataProvider::getSourceRootDir() . '/config/motd.txt')) { @@ -623,6 +680,7 @@ class Framework /** * 从全局配置文件里读取注入系统事件的类 + * * @throws ReflectionException * @throws ReflectionException */ @@ -659,6 +717,7 @@ class Framework /** * 解析命令行的 $argv 参数们 + * * @param array $args 命令行参数 * @param bool|string $add_port 是否添加端口号 */ @@ -672,15 +731,15 @@ class Framework switch ($x) { case 'worker-num': if (intval($y) >= 1 && intval($y) <= 1024) { - $this->server_set['worker_num'] = intval($y); + $this->swoole_config['worker_num'] = intval($y); } else { - Console::warning(zm_internal_errcode('E00013') . 'Invalid worker num! Turn to default value (' . ($this->server_set['worker_num'] ?? swoole_cpu_num()) . ')'); + Console::warning(zm_internal_errcode('E00013') . 'Invalid worker num! Turn to default value (' . ($this->swoole_config['worker_num'] ?? swoole_cpu_num()) . ')'); } break; case 'task-worker-num': if (intval($y) >= 1 && intval($y) <= 1024) { - $this->server_set['task_worker_num'] = intval($y); - $this->server_set['task_enable_coroutine'] = true; + $this->swoole_config['task_worker_num'] = intval($y); + $this->swoole_config['task_enable_coroutine'] = true; } else { Console::warning(zm_internal_errcode('E00013') . 'Invalid worker num! Turn to default value (0)'); } @@ -696,9 +755,9 @@ class Framework echo "* You are in debug mode, do not use in production!\n"; break; case 'daemon': - $this->server_set['daemonize'] = 1; + $this->swoole_config['daemonize'] = 1; Console::$theme = 'no-color'; - Console::log('已启用守护进程,输出重定向到 ' . $this->server_set['log_file']); + Console::log('已启用守护进程,输出重定向到 ' . $this->swoole_config['log_file']); $terminal_id = null; break; case 'disable-console-input': diff --git a/src/ZM/Utils/DataProvider.php b/src/ZM/Utils/DataProvider.php index 9509f729..bfbcb161 100644 --- a/src/ZM/Utils/DataProvider.php +++ b/src/ZM/Utils/DataProvider.php @@ -7,6 +7,7 @@ namespace ZM\Utils; use Iterator; use JsonSerializable; +use RuntimeException; use Traversable; use ZM\Config\ZMConfig; use ZM\Console\Console; @@ -25,6 +26,7 @@ class DataProvider /** * 返回工作目录,不带最右边文件夹的斜杠(/) + * * @return false|string */ public static function getWorkingDir() @@ -34,6 +36,7 @@ class DataProvider /** * 获取框架所在根目录 + * * @return false|string */ public static function getFrameworkRootDir() @@ -43,6 +46,7 @@ class DataProvider /** * 获取源码根目录,除Phar模式外均与工作目录相同 + * * @return false|string */ public static function getSourceRootDir() @@ -52,6 +56,7 @@ class DataProvider /** * 获取框架反代链接 + * * @return null|array|false|mixed */ public static function getFrameworkLink() @@ -61,6 +66,7 @@ class DataProvider /** * 获取zm_data数据目录,如果二级目录不为空,则自动创建目录并返回 + * * @return null|array|false|mixed|string */ public static function getDataFolder(string $second = '') @@ -79,6 +85,7 @@ class DataProvider /** * 将变量保存在zm_data下的数据目录,传入数组 + * * @param string $filename 文件名 * @param array|int|Iterator|JsonSerializable|string|Traversable $file_array 文件内容数组 * @return false|int 返回文件大小或false @@ -104,6 +111,7 @@ class DataProvider /** * 从json加载变量到内存 + * * @param string $filename 文件名 * @return null|mixed 返回文件内容数据或null */ @@ -118,6 +126,7 @@ class DataProvider /** * 递归或非递归扫描目录,可返回相对目录的文件列表或绝对目录的文件列表 + * * @param string $dir 目录 * @param bool $recursive 是否递归扫描子目录 * @param bool|string $relative 是否返回相对目录,如果为true则返回相对目录,如果为false则返回绝对目录 @@ -161,6 +170,7 @@ class DataProvider /** * 检查路径是否为相对路径(根据第一个字符是否为"/"来判断) + * * @param string $path 路径 * @return bool 返回结果 * @since 2.5 @@ -169,4 +179,16 @@ class DataProvider { return strlen($path) > 0 && $path[0] !== '/'; } + + /** + * 创建目录(如果不存在) + * + * @param string $path 目录路径 + */ + public static function createIfNotExists(string $path): void + { + if (!is_dir($path) && !mkdir($path, 0777, true) && !is_dir($path)) { + throw new RuntimeException(sprintf('无法建立目录:%s', $path)); + } + } } diff --git a/src/ZM/global_functions.php b/src/ZM/global_functions.php index 422fdf45..bf6c05c2 100644 --- a/src/ZM/global_functions.php +++ b/src/ZM/global_functions.php @@ -223,7 +223,7 @@ function get_annotations(): array */ function set_coroutine_params(array $params): void { - $cid = Co::getCid(); + $cid = co::getCid(); if ($cid === -1) { exit(zm_internal_errcode('E00061') . 'Cannot set coroutine params at none coroutine mode.'); } @@ -233,7 +233,7 @@ function set_coroutine_params(array $params): void Context::$context[$cid] = $params; } foreach (Context::$context as $c => $v) { - if (!Co::exists($c)) { + if (!co::exists($c)) { unset(Context::$context[$c], ZMBuf::$context_class[$c]); } } @@ -252,13 +252,13 @@ function context(): ContextInterface */ function ctx(): ContextInterface { - $cid = Co::getCid(); + $cid = co::getCid(); $c_class = ZMConfig::get('global', 'context_class'); if (isset(Context::$context[$cid])) { return ZMBuf::$context_class[$cid] ?? (ZMBuf::$context_class[$cid] = new $c_class($cid)); } Console::debug("未找到当前协程的上下文({$cid}),正在找父进程的上下文"); - while (($parent_cid = Co::getPcid($cid)) !== -1) { + while (($parent_cid = co::getPcid($cid)) !== -1) { $cid = $parent_cid; if (isset(Context::$context[$cid])) { return ZMBuf::$context_class[$cid] ?? (ZMBuf::$context_class[$cid] = new $c_class($cid)); @@ -320,7 +320,7 @@ function zm_exec(string $command): array */ function zm_cid(): int { - return Co::getCid(); + return co::getCid(); } /** @@ -330,7 +330,7 @@ function zm_cid(): int */ function zm_yield() { - Co::yield(); + co::yield(); } /** @@ -340,7 +340,7 @@ function zm_yield() */ function zm_resume(int $cid) { - Co::resume($cid); + co::resume($cid); } /** From a7324f6c49ad7704702985d471c0ef968b2d6ac0 Mon Sep 17 00:00:00 2001 From: sunxyw Date: Sun, 10 Apr 2022 01:36:46 +0800 Subject: [PATCH 15/22] fix module config dir not exists --- src/ZM/Framework.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ZM/Framework.php b/src/ZM/Framework.php index fd079f22..b8127140 100644 --- a/src/ZM/Framework.php +++ b/src/ZM/Framework.php @@ -109,7 +109,7 @@ class Framework // 确保目录存在 DataProvider::createIfNotExists(app('path.data')); - DataProvider::createIfNotExists(app('path.config')); + DataProvider::createIfNotExists(app('path.module_config')); DataProvider::createIfNotExists(ZMConfig::get('global', 'crash_dir')); // 初始化连接池? @@ -624,6 +624,7 @@ class Framework $this->container->instance('path.source', DataProvider::getSourceRootDir()); $this->container->alias('path.source', 'path.base'); $this->container->instance('path.config', DataProvider::getSourceRootDir() . '/config'); + $this->container->instance('path.module_config', ZMConfig::get('global', 'config_dir')); $this->container->singleton('path.data', function () { return DataProvider::getDataFolder(); }); From 9e08919ce0c1e5b7f4fc08aaab0f1029e6bd6659 Mon Sep 17 00:00:00 2001 From: sunxyw Date: Sun, 10 Apr 2022 01:44:54 +0800 Subject: [PATCH 16/22] fix read config before loaded --- src/ZM/Framework.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/ZM/Framework.php b/src/ZM/Framework.php index b8127140..18e45078 100644 --- a/src/ZM/Framework.php +++ b/src/ZM/Framework.php @@ -97,7 +97,7 @@ class Framework $this->registerBaseBindings(); // 初始化配置 - ZMConfig::setDirectory($this->container->get('path.config')); + ZMConfig::setDirectory(app('path.config')); ZMConfig::setEnv($args['env'] ?? ''); if (ZMConfig::get('global') === false) { echo zm_internal_errcode('E00007') . 'Global config load failed: ' . ZMConfig::$last_error . "\nError path: " . DataProvider::getSourceRootDir() . "\nPlease init first!\nSee: https://github.com/zhamao-robot/zhamao-framework/issues/37\n"; @@ -624,7 +624,9 @@ class Framework $this->container->instance('path.source', DataProvider::getSourceRootDir()); $this->container->alias('path.source', 'path.base'); $this->container->instance('path.config', DataProvider::getSourceRootDir() . '/config'); - $this->container->instance('path.module_config', ZMConfig::get('global', 'config_dir')); + $this->container->singleton('path.module_config', function () { + return ZMConfig::get('global', 'config_dir'); + }); $this->container->singleton('path.data', function () { return DataProvider::getDataFolder(); }); From 420b8e6d675e22335368e0aebbb7f66b94802ab5 Mon Sep 17 00:00:00 2001 From: sunxyw Date: Sun, 10 Apr 2022 21:45:04 +0800 Subject: [PATCH 17/22] add container debug log --- src/ZM/Container/WorkerContainer.php | 96 +++++++++++++++++++++++++++- 1 file changed, 95 insertions(+), 1 deletion(-) diff --git a/src/ZM/Container/WorkerContainer.php b/src/ZM/Container/WorkerContainer.php index 8f54b999..96d8860b 100644 --- a/src/ZM/Container/WorkerContainer.php +++ b/src/ZM/Container/WorkerContainer.php @@ -13,6 +13,7 @@ use ReflectionClass; use ReflectionException; use ReflectionNamedType; use ReflectionParameter; +use ZM\Console\Console; use ZM\Utils\ReflectionUtil; use ZM\Utils\SingletonTrait; @@ -96,6 +97,10 @@ class WorkerContainer implements ContainerInterface } self::$aliases[$alias] = $abstract; + + if ($this->shouldLog()) { + $this->log("[{$abstract}] is aliased as [{$alias}]"); + } } /** @@ -114,12 +119,18 @@ class WorkerContainer implements ContainerInterface $concrete = $abstract; } + $concrete_name = is_string($concrete) ? $concrete : 'Closure'; + // 如果不是闭包,则认为是类名,此时将其包装在一个闭包中,以方便后续处理 if (!$concrete instanceof Closure) { $concrete = $this->getClosure($abstract, $concrete); } self::$bindings[$abstract] = compact('concrete', 'shared'); + + if ($this->shouldLog()) { + $this->log("[{$abstract}] is bound to [{$concrete_name}]" . ($shared ? ' (shared)' : '')); + } } /** @@ -179,6 +190,10 @@ class WorkerContainer implements ContainerInterface self::$instances[$abstract] = $instance; + if ($this->shouldLog()) { + $this->log("[{$abstract}] is bound to [{$instance}] (instance)"); + } + return $instance; } @@ -224,11 +239,21 @@ class WorkerContainer implements ContainerInterface $needs_contextual_build = !empty($parameters); if (isset($this->shared[$abstract])) { + if ($this->shouldLog()) { + $this->log(sprintf( + '[%s] resolved (shared)%s', + $abstract, + ($needs_contextual_build ? ' with ' . implode(', ', $parameters) : '') + )); + } return $this->shared[$abstract]; } // 如果已经存在在实例池中(通常意味着单例绑定),则直接返回该实例 if (isset(self::$instances[$abstract]) && !$needs_contextual_build) { + if ($this->shouldLog()) { + $this->log("[{$abstract}] resolved (instance)"); + } return self::$instances[$abstract]; } @@ -251,11 +276,22 @@ class WorkerContainer implements ContainerInterface // 如果该类被注册为单例,则需要将其存放在实例池中,方便后续取用同一实例 if (!$needs_contextual_build && $this->isShared($abstract)) { $this->shared[$abstract] = $object; + if ($this->shouldLog()) { + $this->log("[{$abstract}] added to shared pool"); + } } // 弹出本次构造的覆盖参数 array_pop($this->with); + if ($this->shouldLog()) { + $this->log(sprintf( + '[%s] resolved%s', + $abstract, + ($needs_contextual_build ? ' with ' . implode(', ', $parameters) : '') + )); + } + return $object; } @@ -318,6 +354,14 @@ class WorkerContainer implements ContainerInterface */ public function call($callback, array $parameters = [], string $default_method = null) { + if ($this->shouldLog()) { + $this->log(sprintf( + '[%s] called%s%s', + $this->getCallableName($callback), + ($default_method ? ' defaulting' . $default_method : ''), + ($parameters ? ' with ' . implode(', ', $parameters) : '') + )); + } return BoundMethod::call($this, $callback, $parameters, $default_method); } @@ -375,6 +419,10 @@ class WorkerContainer implements ContainerInterface } else { self::$extenders[$abstract][] = $closure; } + + if ($this->shouldLog()) { + $this->log("[{$abstract}] extended"); + } } /** @@ -477,9 +525,21 @@ class WorkerContainer implements ContainerInterface } // 如果类名为空,则代表此依赖是基本类型,且无法对其进行依赖解析 - $results[] = is_null(ReflectionUtil::getParameterClassName($dependency)) + $class_name = ReflectionUtil::getParameterClassName($dependency); + $results[] = is_null($class_name) ? $this->resolvePrimitive($dependency) : $this->resolveClass($dependency); + + if ($this->shouldLog()) { + if (is_null($class_name)) { + if ($dependency->hasType()) { + $class_name = $dependency->getType(); + } else { + $class_name = 'Primitive'; + } + } + $this->log("Dependency [{$class_name} {$dependency->name}] resolved"); + } } return $results; @@ -616,4 +676,38 @@ class WorkerContainer implements ContainerInterface || (isset($this->bindings[$abstract]['shared']) && $this->bindings[$abstract]['shared'] === true); } + + /** + * 获取回调的名称 + * + * @param callable|string $callable 回调 + */ + private function getCallableName($callable): string + { + $name = is_string($callable) ? $callable : '{closure}'; + if (is_array($callable)) { + if (is_object($callable[0])) { + $name = get_class($callable[0]) . '@' . $callable[1]; + } else { + $name = $callable[0] . '::' . $callable[1]; + } + } + return $name; + } + + /** + * 判断是否输出日志 + */ + private function shouldLog(): bool + { + return Console::getLevel() >= 4; + } + + /** + * 记录日志 + */ + private function log(string $message): void + { + Console::debug($message); + } } From 0dd61a727002b89ecb24269c8554dce5a972f36b Mon Sep 17 00:00:00 2001 From: sunxyw Date: Sun, 10 Apr 2022 21:46:53 +0800 Subject: [PATCH 18/22] add di support for event handlers --- src/Module/Example/Hello.php | 37 ++++++----------------------- src/ZM/Event/EventDispatcher.php | 8 ++++--- src/ZM/Event/SwooleEvent/OnOpen.php | 4 ++++ 3 files changed, 16 insertions(+), 33 deletions(-) diff --git a/src/Module/Example/Hello.php b/src/Module/Example/Hello.php index 7f21bfad..4766b02d 100644 --- a/src/Module/Example/Hello.php +++ b/src/Module/Example/Hello.php @@ -18,6 +18,7 @@ use ZM\API\TuringAPI; use ZM\Config\ZMConfig; use ZM\ConnectionManager\ConnectionObject; use ZM\Console\Console; +use ZM\Context\Context; use ZM\Event\EventDispatcher; use ZM\Exception\InterruptException; use ZM\Module\QQBot; @@ -69,7 +70,6 @@ class Hello */ public function hello() { - zm_dump(resolve('swoole_server')->worker_id); return '你好啊,我是由炸毛框架构建的机器人!'; } @@ -141,7 +141,6 @@ class Hello */ public function randNum() { - zm_dump(resolve('swoole_server')->worker_id); // 获取第一个数字类型的参数 $num1 = ctx()->getNumArg('请输入第一个数字'); // 获取第二个数字类型的参数 @@ -192,13 +191,7 @@ class Hello */ public function onConnect(ConnectionObject $conn) { - zm_dump(resolve('path.base')); - zm_dump(resolve('path.config')); - zm_dump(resolve('app')::$server->worker_id); Console::info('机器人 ' . $conn->getOption('connect_id') . ' 已连接!'); - container()->bind('cid', function () use ($conn) { - return resolve('app')::$server->worker_id . $conn->getOption('connect_id'); - }); } /** @@ -207,7 +200,6 @@ class Hello */ public function onDisconnect(ConnectionObject $conn) { - zm_dump(resolve('cid')); Console::info('机器人 ' . $conn->getOption('connect_id') . ' 已断开连接!'); } @@ -255,29 +247,14 @@ class Hello } /* - * @OnStart(-1) - */ - #[OnStart(-1)] - public function initContainer() - { - $worker_container = new WorkerContainer(); - $worker_container->bindIf('echo', function ($container, $parameters) { - return 'Hello, ' . $parameters[0]; - }); - container()->bind(ContextInterface::class, function () { - return ctx(); - }); - } - - /** - * 容器呐 + * 欢迎来到容器时代 * - * @CQCommand("container") + * @param Context $context 通过依赖注入实现的 + * + * @CQCommand("容器你好") */ - #[CQCommand('container')] - public function container(): string + public function welcomeToContainerAge(Context $context) { - resolve(ContextInterface::class)->reply('Good'); // ctx()->reply('good') - return resolve('echo', ['hello']); // Hello, hello + $context->reply('欢迎来到容器时代'); } } diff --git a/src/ZM/Event/EventDispatcher.php b/src/ZM/Event/EventDispatcher.php index 72940e4c..6be94696 100644 --- a/src/ZM/Event/EventDispatcher.php +++ b/src/ZM/Event/EventDispatcher.php @@ -188,7 +188,9 @@ class EventDispatcher if (isset(EventManager::$middleware_map[$q_c][$q_f])) { $middlewares = EventManager::$middleware_map[$q_c][$q_f]; if ($this->log) { - Console::verbose("[事件分发{$this->eid}] " . $q_c . '::' . $q_f . ' 方法还绑定了 Middleware:' . implode(', ', array_map(function ($x) { return $x->middleware; }, $middlewares))); + Console::verbose("[事件分发{$this->eid}] " . $q_c . '::' . $q_f . ' 方法还绑定了 Middleware:' . implode(', ', array_map(function ($x) { + return $x->middleware; + }, $middlewares))); } $before_result = true; $r = []; @@ -231,7 +233,7 @@ class EventDispatcher if ($this->log) { Console::verbose("[事件分发{$this->eid}] 正在执行方法 " . $q_c . '::' . $q_f . ' ...'); } - $this->store = $q_o->{$q_f}(...$params); + $this->store = container()->call([$q_o, $q_f], $params); } catch (Exception $e) { if ($e instanceof InterruptException) { if ($this->log) { @@ -283,7 +285,7 @@ class EventDispatcher if ($this->log) { Console::verbose("[事件分发{$this->eid}] 正在执行方法 " . $q_c . '::' . $q_f . ' ...'); } - $this->store = $q_o->{$q_f}(...$params); + $this->store = container()->call([$q_o, $q_f], $params); $this->status = self::STATUS_NORMAL; return true; } diff --git a/src/ZM/Event/SwooleEvent/OnOpen.php b/src/ZM/Event/SwooleEvent/OnOpen.php index a87fce63..581a4a18 100644 --- a/src/ZM/Event/SwooleEvent/OnOpen.php +++ b/src/ZM/Event/SwooleEvent/OnOpen.php @@ -13,6 +13,7 @@ use ZM\Annotation\Swoole\OnOpenEvent; use ZM\Annotation\Swoole\OnSwooleEvent; use ZM\Annotation\Swoole\SwooleHandler; use ZM\Config\ZMConfig; +use ZM\ConnectionManager\ConnectionObject; use ZM\ConnectionManager\ManagerGM; use ZM\Console\Console; use ZM\Context\Context; @@ -30,6 +31,7 @@ class OnOpen implements SwooleEvent { Console::debug('Calling Swoole "open" event from fd=' . $request->fd); unset(Context::$context[Coroutine::getCid()]); + $type = strtolower($request->header['x-client-role'] ?? $request->get['type'] ?? ''); $access_token = explode(' ', $request->header['authorization'] ?? '')[1] ?? $request->get['token'] ?? ''; $token = ZMConfig::get('global', 'access_token'); @@ -52,6 +54,8 @@ class OnOpen implements SwooleEvent set_coroutine_params(['server' => $server, 'request' => $request, 'connection' => $conn, 'fd' => $request->fd]); $conn->setOption('connect_id', strval($request->header['x-self-id'] ?? '')); + container()->instance(ConnectionObject::class, $conn); + $dispatcher1 = new EventDispatcher(OnOpenEvent::class); $dispatcher1->setRuleFunction(function ($v) { return ctx()->getConnection()->getName() == $v->connect_type && eval('return ' . $v->getRule() . ';'); From f2aff5882d3ae81ec66fc4d518df70dd66659aa6 Mon Sep 17 00:00:00 2001 From: sunxyw Date: Mon, 11 Apr 2022 22:35:10 +0800 Subject: [PATCH 19/22] use accurate property name --- src/ZM/Framework.php | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/ZM/Framework.php b/src/ZM/Framework.php index 18e45078..efef8004 100644 --- a/src/ZM/Framework.php +++ b/src/ZM/Framework.php @@ -61,11 +61,11 @@ class Framework public static $instant_mode = false; /** - * Swoole 配置 + * Swoole 服务端配置 * * @var null|array */ - private $swoole_config; + private $swoole_server_config; /** * @var array @@ -148,8 +148,8 @@ class Framework date_default_timezone_set($timezone); // 读取 Swoole 配置 - $this->swoole_config = ZMConfig::get('global', 'swoole'); - $this->swoole_config['log_level'] = SWOOLE_LOG_DEBUG; + $this->swoole_server_config = ZMConfig::get('global', 'swoole'); + $this->swoole_server_config['log_level'] = SWOOLE_LOG_DEBUG; // 是否启用远程终端 $add_port = ZMConfig::get('global', 'remote_terminal')['status'] ?? false; @@ -163,11 +163,11 @@ class Framework $this->parseCliArgs(self::$argv, $add_port); // 设置默认最长等待时间 - if (!isset($this->swoole_config['max_wait_time'])) { - $this->swoole_config['max_wait_time'] = 5; + if (!isset($this->swoole_server_config['max_wait_time'])) { + $this->swoole_server_config['max_wait_time'] = 5; } // 设置最大 worker 进程数 - $worker = $this->swoole_config['worker_num'] ?? swoole_cpu_num(); + $worker = $this->swoole_server_config['worker_num'] ?? swoole_cpu_num(); define('ZM_WORKER_NUM', $worker); // 初始化原子计数器 @@ -177,14 +177,14 @@ class Framework if (!self::$argv['private-mode']) { $out['working_dir'] = DataProvider::getWorkingDir(); $out['listen'] = ZMConfig::get('global', 'host') . ':' . ZMConfig::get('global', 'port'); - if (!isset($this->swoole_config['worker_num'])) { + if (!isset($this->swoole_server_config['worker_num'])) { if ((ZMConfig::get('global', 'runtime')['swoole_server_mode'] ?? SWOOLE_PROCESS) === SWOOLE_PROCESS) { $out['worker'] = swoole_cpu_num() . ' (auto)'; } else { $out['single_proc_mode'] = 'true'; } } else { - $out['worker'] = $this->swoole_config['worker_num']; + $out['worker'] = $this->swoole_server_config['worker_num']; } $out['environment'] = ($args['env'] ?? null) === null ? 'default' : $args['env']; $out['log_level'] = Console::getLevel(); @@ -193,8 +193,8 @@ class Framework if (APP_VERSION !== 'unknown') { $out['app_version'] = APP_VERSION; } - if (isset($this->swoole_config['task_worker_num'])) { - $out['task_worker'] = $this->swoole_config['task_worker_num']; + if (isset($this->swoole_server_config['task_worker_num'])) { + $out['task_worker'] = $this->swoole_server_config['task_worker_num']; } if ((ZMConfig::get('global', 'sql_config')['sql_host'] ?? '') !== '') { $conf = ZMConfig::get('global', 'sql_config'); @@ -315,7 +315,7 @@ class Framework } // 设置服务器配置 - self::$server->set($this->swoole_config); + self::$server->set($this->swoole_server_config); Console::setServer(self::$server); // 非静默模式下,打印欢迎信息 @@ -734,15 +734,15 @@ class Framework switch ($x) { case 'worker-num': if (intval($y) >= 1 && intval($y) <= 1024) { - $this->swoole_config['worker_num'] = intval($y); + $this->swoole_server_config['worker_num'] = intval($y); } else { - Console::warning(zm_internal_errcode('E00013') . 'Invalid worker num! Turn to default value (' . ($this->swoole_config['worker_num'] ?? swoole_cpu_num()) . ')'); + Console::warning(zm_internal_errcode('E00013') . 'Invalid worker num! Turn to default value (' . ($this->swoole_server_config['worker_num'] ?? swoole_cpu_num()) . ')'); } break; case 'task-worker-num': if (intval($y) >= 1 && intval($y) <= 1024) { - $this->swoole_config['task_worker_num'] = intval($y); - $this->swoole_config['task_enable_coroutine'] = true; + $this->swoole_server_config['task_worker_num'] = intval($y); + $this->swoole_server_config['task_enable_coroutine'] = true; } else { Console::warning(zm_internal_errcode('E00013') . 'Invalid worker num! Turn to default value (0)'); } @@ -758,9 +758,9 @@ class Framework echo "* You are in debug mode, do not use in production!\n"; break; case 'daemon': - $this->swoole_config['daemonize'] = 1; + $this->swoole_server_config['daemonize'] = 1; Console::$theme = 'no-color'; - Console::log('已启用守护进程,输出重定向到 ' . $this->swoole_config['log_file']); + Console::log('已启用守护进程,输出重定向到 ' . $this->swoole_server_config['log_file']); $terminal_id = null; break; case 'disable-console-input': From 7253a309c7073e9a9d77d4ad2e3390152dffde64 Mon Sep 17 00:00:00 2001 From: sunxyw Date: Mon, 11 Apr 2022 23:31:49 +0800 Subject: [PATCH 20/22] refactor container injection --- src/ZM/Container/Container.php | 1 + src/ZM/Container/WorkerContainer.php | 98 ++++++++++++++-------- src/ZM/Event/SwooleEvent/OnMessage.php | 28 ++++--- src/ZM/Event/SwooleEvent/OnWorkerStart.php | 26 ++++-- src/ZM/Event/SwooleEvent/OnWorkerStop.php | 3 - src/ZM/Framework.php | 49 +---------- src/ZM/Utils/ReflectionUtil.php | 36 ++++++++ src/ZM/global_functions.php | 1 - 8 files changed, 141 insertions(+), 101 deletions(-) diff --git a/src/ZM/Container/Container.php b/src/ZM/Container/Container.php index a235aea0..3ab833ca 100644 --- a/src/ZM/Container/Container.php +++ b/src/ZM/Container/Container.php @@ -15,6 +15,7 @@ class Container extends WorkerContainer public function __construct() { + parent::__construct(); $this->parent = WorkerContainer::getInstance(); } diff --git a/src/ZM/Container/WorkerContainer.php b/src/ZM/Container/WorkerContainer.php index 96d8860b..e0d48f2e 100644 --- a/src/ZM/Container/WorkerContainer.php +++ b/src/ZM/Container/WorkerContainer.php @@ -29,13 +29,20 @@ class WorkerContainer implements ContainerInterface /** * @var array[] */ - protected $buildStack = []; + protected $build_stack = []; /** * @var array[] */ protected $with = []; + /** + * 日志前缀 + * + * @var string + */ + protected $log_prefix; + /** * @var array[] */ @@ -56,6 +63,13 @@ class WorkerContainer implements ContainerInterface */ private static $extenders = []; + public function __construct() + { + if ($this->shouldLog()) { + $this->log('Container created'); + } + } + /** * 判断对应的类或接口是否已经注册 * @@ -119,7 +133,10 @@ class WorkerContainer implements ContainerInterface $concrete = $abstract; } - $concrete_name = is_string($concrete) ? $concrete : 'Closure'; + $concrete_name = ''; + if ($this->shouldLog()) { + $concrete_name = ReflectionUtil::variableToString($concrete); + } // 如果不是闭包,则认为是类名,此时将其包装在一个闭包中,以方便后续处理 if (!$concrete instanceof Closure) { @@ -191,7 +208,8 @@ class WorkerContainer implements ContainerInterface self::$instances[$abstract] = $instance; if ($this->shouldLog()) { - $this->log("[{$abstract}] is bound to [{$instance}] (instance)"); + $class_name = ReflectionUtil::variableToString($instance); + $this->log("[{$abstract}] is bound to [{$class_name}] (instance)"); } return $instance; @@ -219,8 +237,12 @@ class WorkerContainer implements ContainerInterface self::$instances = []; $this->shared = []; - $this->buildStack = []; + $this->build_stack = []; $this->with = []; + + if ($this->shouldLog()) { + $this->log('Container flushed'); + } } /** @@ -319,13 +341,13 @@ class WorkerContainer implements ContainerInterface $this->notInstantiable($concrete); } - $this->buildStack[] = $concrete; + $this->build_stack[] = $concrete; $constructor = $reflection->getConstructor(); // 如果不存在构造函数,则代表不需要进一步解析,直接实例化即可 if (is_null($constructor)) { - array_pop($this->buildStack); + array_pop($this->build_stack); return new $concrete(); } @@ -335,11 +357,11 @@ class WorkerContainer implements ContainerInterface try { $instances = $this->resolveDependencies($dependencies); } catch (EntryResolutionException $e) { - array_pop($this->buildStack); + array_pop($this->build_stack); throw $e; } - array_pop($this->buildStack); + array_pop($this->build_stack); return $reflection->newInstanceArgs($instances); } @@ -355,11 +377,17 @@ class WorkerContainer implements ContainerInterface public function call($callback, array $parameters = [], string $default_method = null) { if ($this->shouldLog()) { + if (count($parameters)) { + $str_parameters = array_map([ReflectionUtil::class, 'variableToString'], $parameters); + $str_parameters = implode(', ', $str_parameters); + } else { + $str_parameters = ''; + } $this->log(sprintf( - '[%s] called%s%s', - $this->getCallableName($callback), - ($default_method ? ' defaulting' . $default_method : ''), - ($parameters ? ' with ' . implode(', ', $parameters) : '') + 'Called %s%s(%s)', + ReflectionUtil::variableToString($callback), + ($default_method ? '@' . $default_method : ''), + $str_parameters )); } return BoundMethod::call($this, $callback, $parameters, $default_method); @@ -425,6 +453,22 @@ class WorkerContainer implements ContainerInterface } } + /** + * 获取日志前缀 + */ + public function getLogPrefix(): string + { + return ($this->log_prefix ?: '[WorkerContainer(U)]') . ' '; + } + + /** + * 设置日志前缀 + */ + public function setLogPrefix(string $prefix): void + { + $this->log_prefix = $prefix; + } + /** * 获取对应类型的所有扩展器 * @@ -490,8 +534,8 @@ class WorkerContainer implements ContainerInterface */ protected function notInstantiable(string $concrete, string $reason = ''): void { - if (!empty($this->buildStack)) { - $previous = implode(', ', $this->buildStack); + if (!empty($this->build_stack)) { + $previous = implode(', ', $this->build_stack); $message = "类 {$concrete} 无法实例化,其被 {$previous} 依赖"; } else { $message = "类 {$concrete} 无法实例化"; @@ -677,37 +721,19 @@ class WorkerContainer implements ContainerInterface && $this->bindings[$abstract]['shared'] === true); } - /** - * 获取回调的名称 - * - * @param callable|string $callable 回调 - */ - private function getCallableName($callable): string - { - $name = is_string($callable) ? $callable : '{closure}'; - if (is_array($callable)) { - if (is_object($callable[0])) { - $name = get_class($callable[0]) . '@' . $callable[1]; - } else { - $name = $callable[0] . '::' . $callable[1]; - } - } - return $name; - } - /** * 判断是否输出日志 */ - private function shouldLog(): bool + protected function shouldLog(): bool { return Console::getLevel() >= 4; } /** - * 记录日志 + * 记录日志(自动附加容器日志前缀) */ - private function log(string $message): void + protected function log(string $message): void { - Console::debug($message); + Console::debug($this->getLogPrefix() . $message); } } diff --git a/src/ZM/Event/SwooleEvent/OnMessage.php b/src/ZM/Event/SwooleEvent/OnMessage.php index b7ddbdfa..2a38acdf 100644 --- a/src/ZM/Event/SwooleEvent/OnMessage.php +++ b/src/ZM/Event/SwooleEvent/OnMessage.php @@ -5,7 +5,6 @@ declare(strict_types=1); namespace ZM\Event\SwooleEvent; use Swoole\Coroutine; -use Swoole\Http\Server; use Swoole\WebSocket\Frame; use Throwable; use ZM\Annotation\Swoole\OnMessageEvent; @@ -15,6 +14,7 @@ use ZM\ConnectionManager\ConnectionObject; use ZM\ConnectionManager\ManagerGM; use ZM\Console\Console; use ZM\Console\TermColor; +use ZM\Container\Container; use ZM\Context\Context; use ZM\Context\ContextInterface; use ZM\Event\EventDispatcher; @@ -33,13 +33,7 @@ class OnMessage implements SwooleEvent $conn = ManagerGM::get($frame->fd); set_coroutine_params(['server' => $server, 'frame' => $frame, 'connection' => $conn]); - container()->instance(Server::class, $server); - container()->instance(Frame::class, $frame); - container()->instance(ConnectionObject::class, $conn); - container()->bind(ContextInterface::class, function () { - return ctx(); - }); - container()->alias(ContextInterface::class, Context::class); + $this->registerRequestContainerBindings($frame, $conn); $dispatcher1 = new EventDispatcher(OnMessageEvent::class); $dispatcher1->setRuleFunction(function ($v) use ($conn) { @@ -70,9 +64,21 @@ class OnMessage implements SwooleEvent Console::trace(); } finally { container()->flush(); - if (Console::getLevel() >= 4) { - Console::debug(sprintf('Request container [fd=%d, cid=%d] flushed.', $frame->fd, Coroutine::getCid())); - } } } + + /** + * 注册请求容器绑定 + */ + private function registerRequestContainerBindings(Frame $frame, ?ConnectionObject $conn): void + { + $container = Container::getInstance(); + $container->setLogPrefix("[Container#{$frame->fd}]"); + $container->instance(Frame::class, $frame); + $container->instance(ConnectionObject::class, $conn); + $container->bind(ContextInterface::class, function () { + return ctx(); + }); + $container->alias(ContextInterface::class, Context::class); + } } diff --git a/src/ZM/Event/SwooleEvent/OnWorkerStart.php b/src/ZM/Event/SwooleEvent/OnWorkerStart.php index 893d4b8f..d3bc0338 100644 --- a/src/ZM/Event/SwooleEvent/OnWorkerStart.php +++ b/src/ZM/Event/SwooleEvent/OnWorkerStart.php @@ -54,11 +54,7 @@ class OnWorkerStart implements SwooleEvent } unset(Context::$context[Coroutine::getCid()]); - /* @noinspection PhpExpressionResultUnusedInspection */ - new WorkerContainer(); - if (Console::getLevel() >= 4) { - Console::debug(sprintf('Worker container [id=%d,cid=%d] booted', $worker_id, Coroutine::getCid())); - } + $this->registerWorkerContainerBindings($server); if ($server->taskworker === false) { Framework::saveProcessState(ZM_PROCESS_WORKER, $server->worker_pid, ['worker_id' => $worker_id]); @@ -292,4 +288,24 @@ class OnWorkerStart implements SwooleEvent DB::initTableList($real_conf['dbname']); } } + + /** + * 注册进程容器绑定 + */ + private function registerWorkerContainerBindings(Server $server): void + { + $container = WorkerContainer::getInstance(); + $container->setLogPrefix("[WorkerContainer#{$server->worker_id}]"); + // 路径 + $container->instance('path.working', DataProvider::getWorkingDir()); + $container->instance('path.source', DataProvider::getSourceRootDir()); + $container->alias('path.source', 'path.base'); + $container->instance('path.config', DataProvider::getSourceRootDir() . '/config'); + $container->instance('path.module_config', ZMConfig::get('global', 'config_dir')); + $container->instance('path.data', DataProvider::getDataFolder()); + $container->instance('path.framework', DataProvider::getFrameworkRootDir()); + // 基础 + $container->instance('server', $server); + $container->instance('worker_id', $server->worker_id); + } } diff --git a/src/ZM/Event/SwooleEvent/OnWorkerStop.php b/src/ZM/Event/SwooleEvent/OnWorkerStop.php index 919916ed..507bee74 100644 --- a/src/ZM/Event/SwooleEvent/OnWorkerStop.php +++ b/src/ZM/Event/SwooleEvent/OnWorkerStop.php @@ -22,9 +22,6 @@ class OnWorkerStop implements SwooleEvent public function onCall(Server $server, $worker_id) { WorkerContainer::getInstance()->flush(); - if (Console::getLevel() >= 4) { - Console::debug(sprintf('Worker container [id=%d] flushed', $worker_id)); - } if ($worker_id == (ZMConfig::get('worker_cache')['worker'] ?? 0)) { LightCache::savePersistence(); diff --git a/src/ZM/Framework.php b/src/ZM/Framework.php index efef8004..f970b0a5 100644 --- a/src/ZM/Framework.php +++ b/src/ZM/Framework.php @@ -19,8 +19,6 @@ use ZM\Config\ZMConfig; use ZM\ConnectionManager\ManagerGM; use ZM\Console\Console; use ZM\Console\TermColor; -use ZM\Container\Container; -use ZM\Container\ContainerInterface; use ZM\Exception\ZMKnownException; use ZM\Store\LightCache; use ZM\Store\LightCacheInside; @@ -72,13 +70,6 @@ class Framework */ private $setup_events = []; - /** - * 容器 - * - * @var ContainerInterface - */ - private $container; - /** * 创建一个新的框架实例 * @@ -91,13 +82,8 @@ class Framework self::$instant_mode = $instant_mode; self::$argv = $args; - // 初始化全局容器 - $this->container = new Container(); - $this->bindPathsInContainer(); - $this->registerBaseBindings(); - // 初始化配置 - ZMConfig::setDirectory(app('path.config')); + ZMConfig::setDirectory(DataProvider::getSourceRootDir() . '/config'); ZMConfig::setEnv($args['env'] ?? ''); if (ZMConfig::get('global') === false) { echo zm_internal_errcode('E00007') . 'Global config load failed: ' . ZMConfig::$last_error . "\nError path: " . DataProvider::getSourceRootDir() . "\nPlease init first!\nSee: https://github.com/zhamao-robot/zhamao-framework/issues/37\n"; @@ -108,8 +94,8 @@ class Framework require_once 'global_defines.php'; // 确保目录存在 - DataProvider::createIfNotExists(app('path.data')); - DataProvider::createIfNotExists(app('path.module_config')); + DataProvider::createIfNotExists(ZMConfig::get('global', 'zm_data')); + DataProvider::createIfNotExists(ZMConfig::get('global', 'config_dir')); DataProvider::createIfNotExists(ZMConfig::get('global', 'crash_dir')); // 初始化连接池? @@ -589,7 +575,7 @@ class Framework echo str_pad('', $max_border, '=') . PHP_EOL; } - public static function getTtyWidth(): int + public function getTtyWidth(): int { $size = exec('stty size'); if (empty($size)) { @@ -615,33 +601,6 @@ class Framework return file_put_contents(DataProvider::getDataFolder() . '.state.json', json_encode($data, 64 | 128 | 256)); } - /** - * 注册所有应用路径到容器 - */ - protected function bindPathsInContainer(): void - { - $this->container->instance('path.working', DataProvider::getWorkingDir()); - $this->container->instance('path.source', DataProvider::getSourceRootDir()); - $this->container->alias('path.source', 'path.base'); - $this->container->instance('path.config', DataProvider::getSourceRootDir() . '/config'); - $this->container->singleton('path.module_config', function () { - return ZMConfig::get('global', 'config_dir'); - }); - $this->container->singleton('path.data', function () { - return DataProvider::getDataFolder(); - }); - $this->container->instance('path.framework', DataProvider::getFrameworkRootDir()); - } - - /** - * 注册基础绑定到容器 - */ - protected function registerBaseBindings(): void - { - $this->container->instance('framework', $this); - $this->container->alias('framework', 'app'); - } - private static function printMotd($tty_width) { if (file_exists(DataProvider::getSourceRootDir() . '/config/motd.txt')) { diff --git a/src/ZM/Utils/ReflectionUtil.php b/src/ZM/Utils/ReflectionUtil.php index faea361a..e6710d81 100644 --- a/src/ZM/Utils/ReflectionUtil.php +++ b/src/ZM/Utils/ReflectionUtil.php @@ -40,4 +40,40 @@ class ReflectionUtil return $class_name; } + + /** + * 将传入变量转换为字符串 + * + * @param mixed $var + */ + public static function variableToString($var): string + { + switch (true) { + case is_callable($var): + if (is_array($var)) { + if (is_object($var[0])) { + return get_class($var[0]) . '@' . $var[1]; + } + return $var[0] . '::' . $var[1]; + } + return 'closure'; + case is_string($var): + return $var; + case is_array($var): + return 'array' . json_encode($var); + case is_object($var): + return get_class($var); + case is_resource($var): + return 'resource' . get_resource_type($var); + case is_null($var): + return 'null'; + case is_bool($var): + return $var ? 'true' : 'false'; + case is_float($var): + case is_int($var): + return (string) $var; + default: + return 'unknown'; + } + } } diff --git a/src/ZM/global_functions.php b/src/ZM/global_functions.php index bf6c05c2..81c5d7e7 100644 --- a/src/ZM/global_functions.php +++ b/src/ZM/global_functions.php @@ -628,7 +628,6 @@ function zm_internal_errcode($code): string } /** -<<<<<<< HEAD * 将可能为数组的参数转换为字符串 * * 如传入字符串则为原样返回 From 33f517333f9161c9f994adb6b2e06faebf3286d6 Mon Sep 17 00:00:00 2001 From: sunxyw Date: Tue, 12 Apr 2022 17:13:17 +0800 Subject: [PATCH 21/22] fix unhandled exception --- src/ZM/Container/WorkerContainer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ZM/Container/WorkerContainer.php b/src/ZM/Container/WorkerContainer.php index e0d48f2e..6a37b447 100644 --- a/src/ZM/Container/WorkerContainer.php +++ b/src/ZM/Container/WorkerContainer.php @@ -334,7 +334,7 @@ class WorkerContainer implements ContainerInterface try { $reflection = new ReflectionClass($concrete); } catch (ReflectionException $e) { - throw new EntryResolutionException("指定的类 {$concrete} 不存在"); + throw new EntryResolutionException("指定的类 {$concrete} 不存在", 0, $e); } if (!$reflection->isInstantiable()) { From 48f6cc644d77e39adb56bfa02f98c949ab720e57 Mon Sep 17 00:00:00 2001 From: sunxyw Date: Tue, 12 Apr 2022 17:52:51 +0800 Subject: [PATCH 22/22] abstract reflection utils --- src/ZM/Container/BoundMethod.php | 43 ++----------------------------- src/ZM/Utils/ReflectionUtil.php | 44 ++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 41 deletions(-) diff --git a/src/ZM/Container/BoundMethod.php b/src/ZM/Container/BoundMethod.php index e2a6ae40..fc9f261f 100644 --- a/src/ZM/Container/BoundMethod.php +++ b/src/ZM/Container/BoundMethod.php @@ -4,12 +4,8 @@ declare(strict_types=1); namespace ZM\Container; -use Closure; use InvalidArgumentException; use ReflectionException; -use ReflectionFunction; -use ReflectionFunctionAbstract; -use ReflectionMethod; use ReflectionParameter; use ZM\Utils\ReflectionUtil; @@ -34,7 +30,7 @@ class BoundMethod $callback = [$callback, $default_method]; } - if (self::isCallingNonStaticMethod($callback)) { + if (ReflectionUtil::isNonStaticMethod($callback)) { $callback[0] = $container->make($callback[0]); } @@ -45,21 +41,6 @@ class BoundMethod return call_user_func_array($callback, self::getMethodDependencies($container, $callback, $parameters)); } - /** - * 判断调用的是否为非静态方法 - * - * @param array|string $callback - * @throws ReflectionException - */ - protected static function isCallingNonStaticMethod($callback): bool - { - if (is_array($callback) && is_string($callback[0])) { - $reflection = new ReflectionMethod($callback[0], $callback[1]); - return !$reflection->isStatic(); - } - return false; - } - /** * Get all dependencies for a given method. * @@ -70,33 +51,13 @@ class BoundMethod { $dependencies = []; - foreach (static::getCallReflector($callback)->getParameters() as $parameter) { + foreach (ReflectionUtil::getCallReflector($callback)->getParameters() as $parameter) { static::addDependencyForCallParameter($container, $parameter, $parameters, $dependencies); } return array_merge($dependencies, array_values($parameters)); } - /** - * Get the proper reflection instance for the given callback. - * - * @param callable|string $callback - * @throws \ReflectionException - * @return ReflectionFunctionAbstract - */ - protected static function getCallReflector($callback) - { - if (is_string($callback) && str_contains($callback, '::')) { - $callback = explode('::', $callback); - } elseif (is_object($callback) && !$callback instanceof Closure) { - $callback = [$callback, '__invoke']; - } - - return is_array($callback) - ? new ReflectionMethod($callback[0], $callback[1]) - : new ReflectionFunction($callback); - } - /** * Get the dependency for the given call parameter. * diff --git a/src/ZM/Utils/ReflectionUtil.php b/src/ZM/Utils/ReflectionUtil.php index e6710d81..d25387d6 100644 --- a/src/ZM/Utils/ReflectionUtil.php +++ b/src/ZM/Utils/ReflectionUtil.php @@ -4,6 +4,11 @@ declare(strict_types=1); namespace ZM\Utils; +use Closure; +use ReflectionException; +use ReflectionFunction; +use ReflectionFunctionAbstract; +use ReflectionMethod; use ReflectionNamedType; use ReflectionParameter; @@ -76,4 +81,43 @@ class ReflectionUtil return 'unknown'; } } + + /** + * 判断传入的回调是否为任意类的非静态方法 + * + * @param callable|string $callback 回调 + * @throws ReflectionException + */ + public static function isNonStaticMethod($callback): bool + { + if (is_array($callback) && is_string($callback[0])) { + $reflection = new ReflectionMethod($callback[0], $callback[1]); + return !$reflection->isStatic(); + } + return false; + } + + /** + * 获取传入的回调的反射实例 + * + * 如果传入的是类方法,则会返回 {@link ReflectionMethod} 实例 + * 否则将返回 {@link ReflectionFunction} 实例 + * + * 可传入实现了 __invoke 的类 + * + * @param callable|string $callback 回调 + * @throws ReflectionException + */ + public static function getCallReflector($callback): ReflectionFunctionAbstract + { + if (is_string($callback) && str_contains($callback, '::')) { + $callback = explode('::', $callback); + } elseif (is_object($callback) && !$callback instanceof Closure) { + $callback = [$callback, '__invoke']; + } + + return is_array($callback) + ? new ReflectionMethod($callback[0], $callback[1]) + : new ReflectionFunction($callback); + } }