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..b20bc6c3 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -6,5 +6,7 @@ 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#' + - '#Unsafe usage of new static#' dynamicConstantNames: - SWOOLE_VERSION diff --git a/src/Module/Example/Hello.php b/src/Module/Example/Hello.php index 8dba02f8..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; @@ -244,4 +245,16 @@ class Hello { bot()->all()->allGroups()->sendGroupMsg(0, ctx()->getMessage()); } + + /* + * 欢迎来到容器时代 + * + * @param Context $context 通过依赖注入实现的 + * + * @CQCommand("容器你好") + */ + public function welcomeToContainerAge(Context $context) + { + $context->reply('欢迎来到容器时代'); + } } diff --git a/src/ZM/Container/BoundMethod.php b/src/ZM/Container/BoundMethod.php new file mode 100644 index 00000000..fc9f261f --- /dev/null +++ b/src/ZM/Container/BoundMethod.php @@ -0,0 +1,98 @@ +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)); + } + + /** + * 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 (ReflectionUtil::getCallReflector($callback)->getParameters() as $parameter) { + static::addDependencyForCallParameter($container, $parameter, $parameters, $dependencies); + } + + return array_merge($dependencies, array_values($parameters)); + } + + /** + * 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/Container.php b/src/ZM/Container/Container.php new file mode 100644 index 00000000..3ab833ca --- /dev/null +++ b/src/ZM/Container/Container.php @@ -0,0 +1,75 @@ +parent = WorkerContainer::getInstance(); + } + + /** + * 获取父容器 + */ + 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); + } + + /** + * 获取一个绑定的实例 + * + * @template T + * @param class-string $abstract 类或接口名 + * @param array $parameters 参数 + * @throws EntryResolutionException + * @return Closure|mixed|T 实例 + */ + 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/src/ZM/Container/ContainerInterface.php b/src/ZM/Container/ContainerInterface.php new file mode 100644 index 00000000..26616c66 --- /dev/null +++ b/src/ZM/Container/ContainerInterface.php @@ -0,0 +1,110 @@ + $abstract 类或接口名 + * @param array $parameters 参数 + * @return Closure|mixed|T 实例 + */ + public function make(string $abstract, array $parameters = []); + + /** + * 调用对应的方法,并自动注入依赖 + * + * @param callable $callback 对应的方法 + * @param array $parameters 参数 + * @param null|string $default_method 默认方法 + * @return mixed + */ + public function call(callable $callback, array $parameters = [], string $default_method = null); +} diff --git a/src/ZM/Container/EntryNotFoundException.php b/src/ZM/Container/EntryNotFoundException.php new file mode 100644 index 00000000..03332d17 --- /dev/null +++ b/src/ZM/Container/EntryNotFoundException.php @@ -0,0 +1,12 @@ +shouldLog()) { + $this->log('Container created'); + } + } + + /** + * 判断对应的类或接口是否已经注册 + * + * @param string $abstract 类或接口名 + */ + public function bound(string $abstract): bool + { + return array_key_exists($abstract, self::$bindings) + || array_key_exists($abstract, self::$instances) + || array_key_exists($abstract, $this->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; + + if ($this->shouldLog()) { + $this->log("[{$abstract}] is aliased as [{$alias}]"); + } + } + + /** + * 注册绑定 + * + * @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; + } + + $concrete_name = ''; + if ($this->shouldLog()) { + $concrete_name = ReflectionUtil::variableToString($concrete); + } + + // 如果不是闭包,则认为是类名,此时将其包装在一个闭包中,以方便后续处理 + 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)' : '')); + } + } + + /** + * 注册绑定 + * + * 在已经绑定时不会重复注册 + * + * @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; + + if ($this->shouldLog()) { + $class_name = ReflectionUtil::variableToString($instance); + $this->log("[{$abstract}] is bound to [{$class_name}] (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->build_stack = []; + $this->with = []; + + if ($this->shouldLog()) { + $this->log('Container flushed'); + } + } + + /** + * 获取一个绑定的实例 + * + * @template T + * @param class-string $abstract 类或接口名 + * @param array $parameters 参数 + * @throws EntryResolutionException + * @return Closure|mixed|T 实例 + */ + public function make(string $abstract, array $parameters = []) + { + $abstract = $this->getAlias($abstract); + + $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]; + } + + $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; + 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; + } + + /** + * 实例化具体的类实例 + * + * @param Closure|string $concrete 类名或对应的闭包 + * @throws EntryResolutionException + * @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} 不存在", 0, $e); + } + + if (!$reflection->isInstantiable()) { + $this->notInstantiable($concrete); + } + + $this->build_stack[] = $concrete; + + $constructor = $reflection->getConstructor(); + + // 如果不存在构造函数,则代表不需要进一步解析,直接实例化即可 + if (is_null($constructor)) { + array_pop($this->build_stack); + return new $concrete(); + } + + $dependencies = $constructor->getParameters(); + + // 获取所有依赖的实例 + try { + $instances = $this->resolveDependencies($dependencies); + } catch (EntryResolutionException $e) { + array_pop($this->build_stack); + throw $e; + } + + array_pop($this->build_stack); + + return $reflection->newInstanceArgs($instances); + } + + /** + * 调用对应的方法,并自动注入依赖 + * + * @param callable|string $callback 对应的方法 + * @param array $parameters 参数 + * @param null|string $default_method 默认方法 + * @return mixed + */ + 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( + 'Called %s%s(%s)', + ReflectionUtil::variableToString($callback), + ($default_method ? '@' . $default_method : ''), + $str_parameters + )); + } + return BoundMethod::call($this, $callback, $parameters, $default_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; + } + + if ($this->shouldLog()) { + $this->log("[{$abstract}] extended"); + } + } + + /** + * 获取日志前缀 + */ + public function getLogPrefix(): string + { + return ($this->log_prefix ?: '[WorkerContainer(U)]') . ' '; + } + + /** + * 设置日志前缀 + */ + public function setLogPrefix(string $prefix): void + { + $this->log_prefix = $prefix; + } + + /** + * 获取对应类型的所有扩展器 + * + * @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->build_stack)) { + $previous = implode(', ', $this->build_stack); + $message = "类 {$concrete} 无法实例化,其被 {$previous} 依赖"; + } else { + $message = "类 {$concrete} 无法实例化"; + } + + throw new EntryResolutionException("{$message}:{$reason}"); + } + + /** + * 解析依赖 + * + * @param ReflectionParameter[] $dependencies + * @throws EntryResolutionException + */ + 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; + } + + // 如果类名为空,则代表此依赖是基本类型,且无法对其进行依赖解析 + $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; + } + + /** + * 判断传入的参数是否存在覆盖参数 + */ + 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 如果无法解析类,则抛出异常 + * @return mixed + */ + protected function resolveClass(ReflectionParameter $parameter) + { + try { + // 尝试解析 + return $this->make($parameter->getClass()->name); + } catch (EntryResolutionException $e) { + // 如果参数是可选的,则返回默认值 + if ($parameter->isDefaultValueAvailable()) { + array_pop($this->with); + return $parameter->getDefaultValue(); + } + + if ($parameter->isVariadic()) { + array_pop($this->with); + return []; + } + + 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); + } + + /** + * 判断是否输出日志 + */ + protected function shouldLog(): bool + { + return Console::getLevel() >= 4; + } + + /** + * 记录日志(自动附加容器日志前缀) + */ + protected function log(string $message): void + { + Console::debug($this->getLogPrefix() . $message); + } +} 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/OnMessage.php b/src/ZM/Event/SwooleEvent/OnMessage.php index a365b66c..2a38acdf 100644 --- a/src/ZM/Event/SwooleEvent/OnMessage.php +++ b/src/ZM/Event/SwooleEvent/OnMessage.php @@ -10,10 +10,13 @@ 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\Container\Container; use ZM\Context\Context; +use ZM\Context\ContextInterface; use ZM\Event\EventDispatcher; use ZM\Event\SwooleEvent; @@ -29,6 +32,9 @@ class OnMessage implements SwooleEvent unset(Context::$context[Coroutine::getCid()]); $conn = ManagerGM::get($frame->fd); set_coroutine_params(['server' => $server, 'frame' => $frame, 'connection' => $conn]); + + $this->registerRequestContainerBindings($frame, $conn); + $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 +62,23 @@ 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(); } } + + /** + * 注册请求容器绑定 + */ + 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/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() . ';'); diff --git a/src/ZM/Event/SwooleEvent/OnWorkerStart.php b/src/ZM/Event/SwooleEvent/OnWorkerStart.php index 18cdc98c..d3bc0338 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,9 @@ class OnWorkerStart implements SwooleEvent SignalListener::signalWorker($server, $worker_id); } unset(Context::$context[Coroutine::getCid()]); + + $this->registerWorkerContainerBindings($server); + 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 +278,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']) @@ -284,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 17c89976..507bee74 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,8 @@ class OnWorkerStop implements SwooleEvent { public function onCall(Server $server, $worker_id) { + WorkerContainer::getInstance()->flush(); + 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..f970b0a5 100644 --- a/src/ZM/Framework.php +++ b/src/ZM/Framework.php @@ -27,69 +27,62 @@ 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_server_config; /** * @var array */ private $setup_events = []; - public function __construct($args = [], $instant_mode = false) + /** + * 创建一个新的框架实例 + * + * @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'); ZMConfig::setEnv($args['env'] ?? ''); if (ZMConfig::get('global') === false) { @@ -98,19 +91,15 @@ class Framework } // 定义常量 - include_once 'global_defines.php'; + require_once 'global_defines.php'; + // 确保目录存在 + DataProvider::createIfNotExists(ZMConfig::get('global', 'zm_data')); + DataProvider::createIfNotExists(ZMConfig::get('global', 'config_dir')); + 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 +115,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_server_config = ZMConfig::get('global', 'swoole'); + $this->swoole_server_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_server_config['max_wait_time'])) { + $this->swoole_server_config['max_wait_time'] = 5; } - $worker = $this->server_set['worker_num'] ?? swoole_cpu_num(); + // 设置最大 worker 进程数 + $worker = $this->swoole_server_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_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_server_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_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'); + $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 +235,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 +300,22 @@ class Framework }); } - self::$server->set($this->server_set); + // 设置服务器配置 + self::$server->set($this->swoole_server_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 +325,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 +359,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 +375,7 @@ class Framework /** * 将各进程的pid写入文件,以备后续崩溃及僵尸进程处理使用 + * * @param int|string $pid * @internal */ @@ -394,6 +412,7 @@ class Framework /** * 用于框架内部获取多进程运行状态的函数 + * * @param mixed $id_or_name * @throws ZMKnownException * @return false|int|mixed @@ -510,8 +529,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 +545,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; @@ -556,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)) { @@ -623,6 +642,7 @@ class Framework /** * 从全局配置文件里读取注入系统事件的类 + * * @throws ReflectionException * @throws ReflectionException */ @@ -659,6 +679,7 @@ class Framework /** * 解析命令行的 $argv 参数们 + * * @param array $args 命令行参数 * @param bool|string $add_port 是否添加端口号 */ @@ -672,15 +693,15 @@ class Framework switch ($x) { case 'worker-num': if (intval($y) >= 1 && intval($y) <= 1024) { - $this->server_set['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->server_set['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->server_set['task_worker_num'] = intval($y); - $this->server_set['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)'); } @@ -696,9 +717,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_server_config['daemonize'] = 1; Console::$theme = 'no-color'; - Console::log('已启用守护进程,输出重定向到 ' . $this->server_set['log_file']); + Console::log('已启用守护进程,输出重定向到 ' . $this->swoole_server_config['log_file']); $terminal_id = null; break; case 'disable-console-input': 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; } 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/Utils/ReflectionUtil.php b/src/ZM/Utils/ReflectionUtil.php new file mode 100644 index 00000000..d25387d6 --- /dev/null +++ b/src/ZM/Utils/ReflectionUtil.php @@ -0,0 +1,123 @@ +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; + } + + /** + * 将传入变量转换为字符串 + * + * @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'; + } + } + + /** + * 判断传入的回调是否为任意类的非静态方法 + * + * @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); + } +} 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 @@ $v) { - if (!Co::exists($c)) { + if (!co::exists($c)) { unset(Context::$context[$c], ZMBuf::$context_class[$c]); } } @@ -238,8 +241,6 @@ function set_coroutine_params(array $params): void /** * 获取当前上下文 - * - * @throws ZMKnownException */ function context(): ContextInterface { @@ -248,18 +249,16 @@ function context(): ContextInterface /** * 获取当前上下文 - * - * @throws ZMKnownException */ 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)); @@ -321,7 +320,7 @@ function zm_exec(string $command): array */ function zm_cid(): int { - return Co::getCid(); + return co::getCid(); } /** @@ -331,7 +330,7 @@ function zm_cid(): int */ function zm_yield() { - Co::yield(); + co::yield(); } /** @@ -341,7 +340,7 @@ function zm_yield() */ function zm_resume(int $cid) { - Co::resume($cid); + co::resume($cid); } /** @@ -416,6 +415,7 @@ function server(): Server /** * 获取缓存当前框架pid的临时目录 + * * @internal */ function _zm_pid_dir(): string @@ -433,6 +433,7 @@ function _zm_pid_dir(): string * 随机返回一个 ZMRobot 实例,效果等同于 {@link ZMRobot::getRandom()}。 * * 在单机器人模式下,会直接返回该机器人实例。 + * * @throws RobotNotFoundException */ function bot(): ZMRobot @@ -638,6 +639,42 @@ 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(); +} + +/** + * 解析类实例(使用容器) + * + * @template T + * @param class-string $abstract + * @return Closure|mixed|T + */ +function resolve(string $abstract, array $parameters = []) +{ + return Container::getInstance()->make($abstract, $parameters); +} + +/** + * 获取容器实例 + * + * @template T + * @param null|class-string $abstract + * @return Closure|ContainerInterface|mixed|T + */ +function app(string $abstract = null, array $parameters = []) +{ + if (is_null($abstract)) { + return container(); + } + + return resolve($abstract, $parameters); +} + /** * 以下为废弃的函数,将于未来移除 */ 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(); + } +} 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')); + } +} 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)); + } +}