Merge pull request #97 from zhamao-robot/add-di

增加 IoC 容器 & 依赖注入支持
This commit is contained in:
sunxyw 2022-04-15 23:07:52 +08:00 committed by GitHub
commit f4d75e211f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 1765 additions and 134 deletions

View File

@ -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",

View File

@ -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

View File

@ -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('欢迎来到容器时代');
}
}

View File

@ -0,0 +1,98 @@
<?php
declare(strict_types=1);
namespace ZM\Container;
use InvalidArgumentException;
use ReflectionException;
use ReflectionParameter;
use ZM\Utils\ReflectionUtil;
class BoundMethod
{
/**
* 调用指定闭包、类方法并注入依赖
*
* @param Container $container
* @param callable|string $callback
* @throws InvalidArgumentException
* @throws ReflectionException
* @return mixed
*/
public static function call(ContainerInterface $container, $callback, array $parameters = [], string $default_method = null)
{
if (is_string($callback) && !$default_method && method_exists($callback, '__invoke')) {
$default_method = '__invoke';
}
if (is_string($callback) && $default_method) {
$callback = [$callback, $default_method];
}
if (ReflectionUtil::isNonStaticMethod($callback)) {
$callback[0] = $container->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);
}
}
}

View File

@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace ZM\Container;
class Container extends WorkerContainer
{
/**
* 父容器
*
* @var ContainerInterface
*/
protected $parent;
public function __construct()
{
parent::__construct();
$this->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<T> $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();
}
}

View File

@ -0,0 +1,110 @@
<?php
declare(strict_types=1);
namespace ZM\Container;
use Closure;
use Psr\Container\ContainerInterface as PsrContainerInterface;
/**
* Interface ContainerInterface
*
* Illuminate WorkerContainer 简化而来,兼容 PSR-11
*/
interface ContainerInterface extends PsrContainerInterface
{
/**
* 判断对应的类或接口是否已经注册
*
* @param string $abstract 类或接口名
*/
public function bound(string $abstract): bool;
/**
* 注册一个类别名
*
* @param string $abstract 类或接口名
* @param string $alias 别名
*/
public function alias(string $abstract, string $alias): void;
/**
* 注册绑定
*
* @param string $abstract 类或接口名
* @param null|Closure|string $concrete 返回类实例的闭包,或是类名
* @param bool $shared 是否共享
*/
public function bind(string $abstract, $concrete = null, bool $shared = false): void;
/**
* 注册绑定
*
* 在已经绑定时不会重复注册
*
* @param string $abstract 类或接口名
* @param null|Closure|string $concrete 返回类实例的闭包,或是类名
* @param bool $shared 是否共享
*/
public function bindIf(string $abstract, $concrete = null, bool $shared = false): void;
/**
* 注册一个单例绑定
*
* @param string $abstract 类或接口名
* @param null|Closure|string $concrete 返回类实例的闭包,或是类名
*/
public function singleton(string $abstract, $concrete = null): void;
/**
* 注册一个单例绑定
*
* 在已经绑定时不会重复注册
*
* @param string $abstract 类或接口名
* @param null|Closure|string $concrete 返回类实例的闭包,或是类名
*/
public function singletonIf(string $abstract, $concrete = null): void;
/**
* 注册一个已有的实例,效果等同于单例绑定
*
* @param string $abstract 类或接口名
* @param mixed $instance 实例
* @return mixed
*/
public function instance(string $abstract, $instance);
/**
* 获取一个解析对应类实例的闭包
*
* @param string $abstract 类或接口名
*/
public function factory(string $abstract): Closure;
/**
* 清除所有绑定和实例
*/
public function flush(): void;
/**
* 获取一个绑定的实例
*
* @template T
* @param class-string<T> $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);
}

View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace ZM\Container;
use Exception;
use Psr\Container\NotFoundExceptionInterface;
class EntryNotFoundException extends Exception implements NotFoundExceptionInterface
{
}

View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace ZM\Container;
use Exception;
use Psr\Container\ContainerExceptionInterface;
class EntryResolutionException extends Exception implements ContainerExceptionInterface
{
}

View File

@ -0,0 +1,739 @@
<?php
declare(strict_types=1);
namespace ZM\Container;
use Closure;
use Exception;
use InvalidArgumentException;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use ReflectionClass;
use ReflectionException;
use ReflectionNamedType;
use ReflectionParameter;
use ZM\Console\Console;
use ZM\Utils\ReflectionUtil;
use ZM\Utils\SingletonTrait;
class WorkerContainer implements ContainerInterface
{
use SingletonTrait;
/**
* @var array
*/
protected $shared = [];
/**
* @var array[]
*/
protected $build_stack = [];
/**
* @var array[]
*/
protected $with = [];
/**
* 日志前缀
*
* @var string
*/
protected $log_prefix;
/**
* @var array[]
*/
private static $bindings = [];
/**
* @var object[]
*/
private static $instances = [];
/**
* @var string[]
*/
private static $aliases = [];
/**
* @var Closure[][]
*/
private static $extenders = [];
public function __construct()
{
if ($this->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<T> $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);
}
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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() . ';');

View File

@ -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);
}
}

View File

@ -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();
}

View File

@ -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':

View File

@ -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<int, ContextInterface>
*/
public static $context_class = [];
/**
* 终端输入流?
*
* 目前等用于 STDIN
*
* @var resource
*/
public static $terminal;
}

View File

@ -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));
}
}
}

View File

@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
namespace ZM\Utils;
use Closure;
use ReflectionException;
use ReflectionFunction;
use ReflectionFunctionAbstract;
use ReflectionMethod;
use ReflectionNamedType;
use ReflectionParameter;
class ReflectionUtil
{
/**
* 获取参数的类名(如有)
*
* @param ReflectionParameter $parameter 参数
* @return null|string 类名,如果参数不是类,返回 null
*/
public static function getParameterClassName(ReflectionParameter $parameter): ?string
{
// 获取参数类型
$type = $parameter->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);
}
}

View File

@ -1,30 +1,34 @@
<?php
/** @noinspection PhpUnused */
declare(strict_types=1);
namespace ZM\Utils;
trait SingletonTrait
{
/**
* @deprecated 将会于未来版本移除
*
* @var array
*/
protected static $cached = [];
/**
* @var self
* @var null|static
*/
private static $instance;
protected static $instance;
/**
* @return self
* @noinspection PhpMissingReturnTypeInspection
* 获取类实例
*
* @return static
*/
public static function getInstance()
{
if (self::$instance === null) {
self::$instance = new self();
if (is_null(static::$instance)) {
static::$instance = new static();
}
return self::$instance;
return static::$instance;
}
}

View File

@ -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()
@ -220,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.');
}
@ -230,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]);
}
}
@ -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<T> $abstract
* @return Closure|mixed|T
*/
function resolve(string $abstract, array $parameters = [])
{
return Container::getInstance()->make($abstract, $parameters);
}
/**
* 获取容器实例
*
* @template T
* @param null|class-string<T> $abstract
* @return Closure|ContainerInterface|mixed|T
*/
function app(string $abstract = null, array $parameters = [])
{
if (is_null($abstract)) {
return container();
}
return resolve($abstract, $parameters);
}
/**
* 以下为废弃的函数,将于未来移除
*/

View File

@ -0,0 +1,130 @@
<?php
declare(strict_types=1);
namespace Tests\ZM\Container;
use PHPUnit\Framework\TestCase;
use ZM\Container\Container;
/**
* @internal
*/
class ContainerCallTest extends TestCase
{
public function testCallInvokableClass(): void
{
$container = new Container();
$this->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();
}
}

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Tests\ZM\Container;
use PHPUnit\Framework\TestCase;
use ZM\Container\Container;
use ZM\Container\WorkerContainer;
/**
* @internal
*/
class ContainerTest extends TestCase
{
public function testInherit(): void
{
$worker_container = new WorkerContainer();
$worker_container->instance('foo', 'bar');
$container = new Container($worker_container);
$container->instance('baz', 'qux');
// 获取父容器的实例
$this->assertEquals('bar', $container->get('foo'));
// 获取自身容器的实例
$this->assertEquals('qux', $container->get('baz'));
}
}

View File

@ -0,0 +1,120 @@
<?php
declare(strict_types=1);
namespace Tests\ZM\Container;
use PHPUnit\Framework\TestCase;
use ZM\Container\EntryResolutionException;
use ZM\Container\WorkerContainer;
use ZM\Utils\MessageUtil;
/**
* @internal
*/
class WorkerContainerTest extends TestCase
{
private $container;
protected function setUp(): void
{
$this->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));
}
}