= 4.5.0 !'); } if (version_compare(PHP_VERSION, '7.2') === -1) { exit(zm_internal_errcode('E00003') . 'PHP >= 7.2 required.'); } if (version_compare(SWOOLE_VERSION, '4.6.7') < 0 && !extension_loaded('pcntl')) { Console::error(zm_internal_errcode('E00004') . 'Swoole 版本必须不低于 4.6.7 或 PHP 安装加载了 pcntl 扩展!'); exit(); } } /** * 分割消息字符串 * * @param array $includes 需要进行切割的字符串,默认包含空格及制表符(\t) */ function explode_msg(string $msg, array $includes = [' ', "\t"]): array { $msg = trim($msg); foreach ($includes as $v) { $msg = str_replace($v, "\n", $msg); } $msg_seg = explode("\n", $msg); $ls = []; foreach ($msg_seg as $v) { if (empty(trim($v))) { continue; } $ls[] = trim($v); } return $ls; } /** * 解码Unicode字符串 */ function unicode_decode(string $str): ?string { return preg_replace_callback('/\\\\u([0-9a-f]{4})/i', static function ($matches) { return mb_convert_encoding(pack('H*', $matches[1]), 'UTF-8', 'UCS-2BE'); }, $str); } /** * 格式匹配 */ function match_pattern(string $pattern, string $subject): bool { $pattern = str_replace(['\*', '\\\\.*'], ['.*', '\*'], preg_quote($pattern, '/')); $pattern = '/^' . $pattern . '$/i'; return preg_match($pattern, $subject) === 1; } /** * @requires symfony/polyfill-ctype * @return array|string[] */ function split_explode(string $del, string $string, bool $divide_en = false): array { $str = explode($del, $string); for ($i = 0, $i_max = mb_strlen($str[0]); $i < $i_max; ++$i) { if ( is_numeric(mb_substr($str[0], $i, 1)) && ( !is_numeric(mb_substr($str[0], $i - 1, 1)) && mb_substr($str[0], $i - 1, 1) !== ' ' && ctype_alpha(mb_substr($str[0], $i - 1, 1)) === false ) ) { $str[0] = mb_substr($str[0], 0, $i) . ' ' . mb_substr($str[0], $i); } elseif ( $divide_en && mb_substr($str[0], $i - 1, 1) !== ' ' && ctype_alnum(mb_substr($str[0], $i, 1)) && !ctype_alnum(mb_substr($str[0], $i - 1, 1)) ) { $str[0] = mb_substr($str[0], 0, $i) . ' ' . mb_substr($str[0], $i); } } $str = implode($del, $str); $ls = []; foreach (explode($del, $str) as $v) { if (empty(trim($v))) { continue; } $ls[] = $v; } return $ls ?: ['']; } /** * 匹配参数 * * @return array|false 成功时返回匹配到的参数数组,失败时返回false */ function match_args(string $pattern, string $subject) { $result = []; if (match_pattern($pattern, $subject)) { if (mb_strpos($pattern, '*') === false) { return []; } $exp = explode('*', $pattern); $i = 0; foreach ($exp as $k => $v) { if (empty($v) && $k === 0) { continue; } if (empty($v) && $k === count($exp) - 1) { $subject .= '^EOL'; $v = '^EOL'; } $cur_var = ''; $ori = $i; while (($a = mb_substr($subject, $i, mb_strlen($v))) !== $v && !empty($a)) { $cur_var .= mb_substr($subject, $i, 1); ++$i; } if ($i !== $ori || $k === 1 || $k === count($exp) - 1) { $result[] = $cur_var; } $i += mb_strlen($v); } return $result; } return false; } /** * 判断当前连接类型是否为传入的$type * * @param string $type 连接类型 */ function current_connection_is(string $type): bool { return ctx()->getConnection()->getName() === $type; } /** * 获取触发当前方法的注解 */ function get_annotations(): array { $s = debug_backtrace()[1]; $list = []; foreach (EventManager::$events as $v) { foreach ($v as $vs) { if ($vs->class === $s['class'] && $vs->method === $s['function']) { $list[get_class($vs)][] = $vs; } } } return $list; } /** * 设置协程参数 */ function set_coroutine_params(array $params): void { $cid = co::getCid(); if ($cid === -1) { exit(zm_internal_errcode('E00061') . 'Cannot set coroutine params at none coroutine mode.'); } if (isset(Context::$context[$cid])) { Context::$context[$cid] = array_merge(Context::$context[$cid], $params); } else { Context::$context[$cid] = $params; } foreach (Context::$context as $c => $v) { if (!co::exists($c)) { unset(Context::$context[$c], ZMBuf::$context_class[$c]); } } } /** * 获取当前上下文 */ function context(): ContextInterface { return ctx(); } /** * 获取当前上下文 */ function ctx(): ContextInterface { $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) { $cid = $parent_cid; if (isset(Context::$context[$cid])) { return ZMBuf::$context_class[$cid] ?? (ZMBuf::$context_class[$cid] = new $c_class($cid)); } } Console::warning('当前环境不是协程环境,将返回独立的非协程的容器'); return ZMBuf::$context_class[$cid] ?? (ZMBuf::$context_class[$cid] = new $c_class($cid)); } /** * 根据消息类型获取对应的OneBot目标名称 * * @return string 如传入的消息类型不被支持,将默认返回`user_id` */ function get_onebot_target_id_name(string $message_type): string { switch ($message_type) { case 'group': return 'group_id'; case 'discuss': return 'discuss_id'; case 'private': default: return 'user_id'; } } /** * 协程休眠 * * 与 {@link sleep()} 一致,只是增加了协程支持 * * @since 2.7.3 此函数不再返回 true */ function zm_sleep(int $seconds = 1): void { if (Coroutine::getCid() !== -1) { System::sleep($seconds); } else { usleep($seconds * 1000 * 1000); } } /** * 协程执行命令 * * 与 {@link exec()} 一致,只是增加了协程支持 * * @return array{code: int, signal: int, output: string} */ function zm_exec(string $command): array { return System::exec($command); } /** * 获取当前协程ID * * 与 {@link Co::getCid()} 一致 */ function zm_cid(): int { return co::getCid(); } /** * 挂起当前协程 * * 与 {@link Co::yield()} 一致 */ function zm_yield() { co::yield(); } /** * 恢复并继续执行指定协程 * * 与 {@link Co::resume()} 一致 */ function zm_resume(int $cid) { co::resume($cid); } /** * 指定延迟后执行函数 * * @param int $delay 延迟时间,单位毫秒ms */ function zm_timer_after(int $delay, callable $runnable) { Swoole\Timer::after($delay, static function () use ($runnable) { call_with_catch($runnable); }); } /** * 重复在指定时间间隔后执行函数 * * @param int $interval 间隔时间,单位毫秒ms * @return false|int 定时器ID,失败返回false */ function zm_timer_tick(int $interval, callable $runnable) { if (zm_cid() === -1) { return go(static function () use ($interval, $runnable) { Console::debug('Adding extra timer tick of ' . $interval . ' ms'); Swoole\Timer::tick($interval, static function () use ($runnable) { call_with_catch($runnable); }); }); } return Swoole\Timer::tick($interval, static function () use ($runnable) { call_with_catch($runnable); }); } /** * 执行函数并记录异常 */ function call_with_catch(callable $callable): void { try { $callable(); } catch (Exception $e) { $error_msg = $e->getMessage() . ' at ' . $e->getFile() . '(' . $e->getLine() . ')'; Console::error(zm_internal_errcode('E00033') . 'Uncaught exception ' . get_class($e) . ': ' . $error_msg); Console::trace(); } catch (Error $e) { $error_msg = $e->getMessage() . ' at ' . $e->getFile() . '(' . $e->getLine() . ')'; Console::error(zm_internal_errcode('E00033') . 'Uncaught ' . get_class($e) . ': ' . $error_msg); Console::trace(); } } /** * 生成消息的哈希值 */ function hash_message(array $message): string { return md5($message['user_id'] . '^' . $message['self_id'] . '^' . $message['message_type'] . '^' . ($message[$message['message_type'] . '_id'] ?? $message['user_id'])); } /** * 获取 Swoole Server 实例 * * 与 {@link Framework::$server} 一致 */ function server(): Server { return Framework::$server; } /** * 获取缓存当前框架pid的临时目录 * * @internal */ function _zm_pid_dir(): string { global $_ZM_HASH; if (!isset($_ZM_HASH)) { $_ZM_HASH = md5(DataProvider::getWorkingDir()); } return '/tmp/.zm_' . $_ZM_HASH; } /** * 获取 ZMRobot 实例 * * 随机返回一个 ZMRobot 实例,效果等同于 {@link ZMRobot::getRandom()}。 * * 在单机器人模式下,会直接返回该机器人实例。 * * @throws RobotNotFoundException */ function bot(): ZMRobot { if (($conn = LightCacheInside::get('connect', 'conn_fd')) === -2) { return OneBotV11::getRandom(); } if ($conn !== -1) { if (($obj = ManagerGM::get($conn)) !== null) { return new ZMRobot($obj); } throw new RobotNotFoundException('单机器人连接模式可能连接了多个机器人!'); } throw new RobotNotFoundException('没有任何机器人连接到框架!'); } /** * 获取指定连接类型的文件描述符ID */ function get_all_fd_of_type(string $type = 'default'): array { $fds = []; foreach (ManagerGM::getAllByName($type) as $obj) { $fds[] = $obj->getFd(); } return $fds; } /** * 获取原子计数器 * * 与 {@link ZMAtomic::get()} 一致 */ function zm_atomic(string $name): ?Atomic { return ZMAtomic::get($name); } /** * 生成 UUID */ function uuidgen(bool $uppercase = false): string { try { $data = random_bytes(16); } catch (Exception $e) { return ''; } $data[6] = chr(ord($data[6]) & 0x0F | 0x40); $data[8] = chr(ord($data[8]) & 0x3F | 0x80); return $uppercase ? strtoupper(vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4))) : vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4)); } /** * 获取框架运行的工作目录 * * @example 例如你是从 /root/framework-starter/ 目录启动的框架,vendor/bin/start server,那么 working_dir() 返回的就是 /root/framework-starter。 */ function working_dir(): string { return WORKING_DIR; } /** * 更漂亮地输出变量值 * * 可替代 {@link var_dump()} * * @param mixed $var * @return array|mixed 返回传入的变量,传入多个变量则会返回数组 */ function zm_dump($var, ...$moreVars) { VarDumper::dump($var); foreach ($moreVars as $v) { VarDumper::dump($v); } if (func_num_args() > 1) { return func_get_args(); } return $var; } /** * 输出info日志 * * 与 {@link Console::info()} 一致 * * @param mixed $obj */ function zm_info($obj): void { Console::info($obj); } /** * 输出warning日志 * * 与 {@link Console::warning()} 一致 * * @param mixed $obj */ function zm_warning($obj): void { Console::warning($obj); } /** * 输出success日志 * * 与 {@link Console::success()} 一致 * * @param mixed $obj */ function zm_success($obj): void { Console::success($obj); } /** * 输出debug日志 * * 与 {@link Console::debug()} 一致 * * @param mixed $obj */ function zm_debug($obj): void { Console::debug($obj); } /** * 输出verbose日志 * * 与 {@link Console::verbose()} 一致 * * @param mixed $obj */ function zm_verbose($obj): void { Console::verbose($obj); } /** * 输出error日志 * * 与 {@link Console::error()} 一致 * * @param mixed $obj */ function zm_error($obj): void { Console::error($obj); } /** * 获取配置项 * * 与 {@link ZMConfig::get()} 一致 * * @return mixed */ function zm_config(string $name, ?string $key = null) { return ZMConfig::get($name, $key); } /** * 生成快速回复闭包 * * @param mixed $reply */ function quick_reply_closure($reply): Closure { return static function () use ($reply) { return $reply; }; } /** * 获取内部错误码 * * @param int|string $code */ function zm_internal_errcode($code): string { return "[ErrCode:{$code}] "; } if (!function_exists('chain')) { define('CARRY', '{carry}'); /** * 链式调用对象方法 * * 如需使用上一步的返回值作为参数,请使用 CARRY 常量替换对应的参数值 * * @param object $object 需要进行链式调用的对象 * @return object 链式操作对象,可当成传入的对象使用 */ function chain(object $object): object { return new class($object) { /** * 最后一次执行的方法的返回值 * * @var mixed */ protected $return; /** * 需要进行链式调用的对象 * * @var object */ protected $wrapped; /** * 构造链式调用匿名类 */ public function __construct(object $object) { $this->wrapped = $object; } /** * 代理所有调用 */ public function __call(string $name, array $arguments) { if (($index = array_search(CARRY, $arguments, true)) !== false) { $arguments[$index] = $this->return; } $this->return = $this->wrapped->{$name}(...$arguments); return $this; } /** * 返回最后执行结果 */ public function __toString() { return (string) $this->return; } /** * 允许调用最后返回结果 */ public function __invoke() { return $this->return; } /** * 使用链式操作对象作为参数,执行回调 */ public function tap(callable $callback): self { $callback($this->wrapped); return $this; } }; } } if (!function_exists('stopwatch')) { /** * 执行回调函数并返回平均执行耗时 * * @param int $times 执行次数 * @return float 平均执行耗时 */ function stopwatch(callable $callback, int $times = 1): float { $total = 0; for ($i = 0; $i < $times; ++$i) { $start = microtime(true); $callback(); $total += microtime(true) - $start; } return $total / $times; } } /** * 将可能为数组的参数转换为字符串 * * 如传入字符串则为原样返回 * * @param array|string $string_or_array */ 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); } /** * 以下为废弃的函数,将于未来移除 */ /** * @deprecated 已废弃,请使用 {@link get_all_fd_of_type()} */ function getAllFdByConnectType(string $type = 'default'): array { return get_all_fd_of_type($type); } /** * @param mixed $class_name * @deprecated 已废弃,请使用 {@link get_class_path()} */ function getClassPath($class_name): ?string { return get_class_path($class_name); } /** * @param mixed $msg * @param mixed $ban_comma * @deprecated 已废弃,请使用 {@link explode_msg()},参数有变 */ function explodeMsg($msg, $ban_comma = false): array { if ($ban_comma) { return explode_msg($msg, [' ']); } return explode_msg($msg); } /** * @deprecated 已废弃,请使用 {@link current_connection_is()} */ function connectIsQQ(): bool { return current_connection_is('qq'); } /** * @deprecated 已废弃,请使用 {@link current_connection_is()} */ function connectIsDefault(): bool { return current_connection_is('default'); } /** * @param mixed $type * @deprecated 已废弃,请使用 {@link current_connection_is()} */ function connectIs($type): bool { return current_connection_is($type); } /** * @deprecated 已废弃,请使用 {@link get_annotations()} */ function getAnnotations(): array { return get_annotations(); } /** * @param mixed $pattern * @param mixed $context * @deprecated 已废弃,请使用 {@link match_args()} */ function matchArgs($pattern, $context) { return match_args($pattern, $context); } /** * @param mixed $pattern * @param mixed $context * @deprecated 已废弃,请使用 {@link match_pattern()} */ function matchPattern($pattern, $context): bool { return match_pattern($pattern, $context); } /** * @param string $message_type 消息类型 * @deprecated 已废弃,请使用 {@link get_onebot_target_id_name()} */ function onebot_target_id_name(string $message_type): string { return get_onebot_target_id_name($message_type); } /** * @deprecated 已废弃,请直接使用 {@link call_with_catch()} */ function zm_go(callable $callable) { call_with_catch($callable); } /** * @param mixed $v * @deprecated 已废弃,请使用 {@link hash_message()} */ function zm_data_hash($v): string { return hash_message($v); }