Anyon e634118a22 refactor(plugin): 迁移 v8 插件化组件体系
将 v6 中直接放在本地 app 的后台与微信能力迁移为 v8 插件组件,并把运行时基础能力沉淀到独立插件包。

主要内容:

- 新增 think-library、system、worker、static、install 等基础插件包。

- 新增 account、payment、wechat-client、wechat-service、wemall、wuma 等业务插件包。

- 移除 v6 的 app/admin 与 app/wechat 本地应用实现,改由插件分发接管。

- 将 Helper 能力彻底并入 System,统一为 plugin\system\helper\* 命名空间。

- 同步插件迁移发布清单与根 route 占位,保证安装发布流程可复现。
2026-05-08 15:30:46 +08:00

1037 lines
33 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
declare(strict_types=1);
/**
* +----------------------------------------------------------------------
* | ThinkAdmin Plugin for ThinkAdminDeveloper
* +----------------------------------------------------------------------
* | Copyright (c) 2014~2026 ThinkAdmin [ thinkadmin.top ]
* +----------------------------------------------------------------------
* | Official Website: https://thinkadmin.top
* +----------------------------------------------------------------------
* | Licensed: https://mit-license.org
* | Disclaimer: https://thinkadmin.top/disclaimer
* | Vip Rights: https://thinkadmin.top/vip-introduce
* +----------------------------------------------------------------------
* | Gitee Repository: https://gitee.com/zoujingli/ThinkAdmin
* | Github Repository: https://github.com/zoujingli/ThinkAdmin
* +----------------------------------------------------------------------
*/
namespace think\admin\service;
use think\admin\Library;
use think\admin\model\QueryFactory;
use think\admin\Plugin;
use think\admin\runtime\RequestContext;
use think\admin\Service;
use think\db\Query;
use think\Model;
use think\Request;
/**
* 应用注册与插件管理中心服务
* @class AppService
*/
final class AppService extends Service
{
/**
* 应用缓存键名.
*/
private const CACHE_APPS = 'think.admin.apps';
/**
* 本地应用缓存键名.
*/
private const CACHE_LOCALS = 'think.admin.apps.locals';
/**
* 插件缓存键名。
*/
private const CACHE_PLUGINS = 'think.admin.plugins';
/**
* 插件别名缓存键名。
*/
private const CACHE_ALIASES = 'think.admin.plugins.aliases';
/**
* 插件前缀绑定缓存键名。
*/
private const CACHE_BINDINGS = 'think.admin.plugins.bindings';
/**
* API 入口缓存键名。
*/
private const CACHE_ENTRY = 'think.admin.plugins.entry';
/**
* app 根目录下的共享目录,不参与本地多应用扫描.
*/
private const IGNORE_LOCAL_APPS = ['common', 'config', 'controller', 'lang', 'middleware', 'model', 'route', 'view'];
/**
* 清理应用缓存.
*/
public static function clear(): void
{
sysvar(self::CACHE_APPS, false);
sysvar(self::CACHE_LOCALS, false);
sysvar(self::CACHE_PLUGINS, false);
sysvar(self::CACHE_ALIASES, false);
sysvar(self::CACHE_BINDINGS, false);
sysvar(self::CACHE_ENTRY, false);
}
/**
* 获取本地模块列表。
* @param array $data 额外数据
*/
public static function getModules(array $data = []): array
{
return array_values(array_unique(array_merge($data, array_keys(self::local()))));
}
/**
* 获取全部应用列表。
* @param array $data 额外数据
*/
public static function getApps(array $data = []): array
{
return array_values(array_unique(array_merge($data, array_keys(self::all()))));
}
/**
* 判断应用是否存在.
*/
public static function exists(string $code, bool $force = false): bool
{
return isset(self::all($force)[$code]);
}
/**
* 获取全部应用定义.
*
* @param bool $force 强制刷新
* @return array<string, array<string, string>>
*/
public static function all(bool $force = false): array
{
if (!$force && is_array($apps = sysvar(self::CACHE_APPS))) {
return $apps;
}
$apps = array_merge(self::local($force), self::allPlugins($force));
ksort($apps);
return sysvar(self::CACHE_APPS, $apps);
}
/**
* 获取本地 app/* 应用定义.
*
* @return array<string, array<string, string>>
*/
public static function local(bool $force = false): array
{
if (!$force && is_array($apps = sysvar(self::CACHE_LOCALS))) {
return $apps;
}
$apps = self::discoverLocalApps();
ksort($apps);
return sysvar(self::CACHE_LOCALS, $apps);
}
/**
* 获取插件应用定义.
*
* @return array<string, array<string, string>>
*/
public static function plugins(bool $force = false): array
{
return self::allPlugins(false, $force);
}
/**
* 获取全部应用编号.
*
* @return string[]
*/
public static function codes(bool $force = false): array
{
return array_keys(self::all($force));
}
/**
* 获取默认本地应用编号.
*/
public static function defaultAppCode(): string
{
$apps = self::local();
$code = strval(Library::$sapp->config->get('route.default_app') ?: Library::$sapp->config->get('app.single_app') ?: '');
if ($code !== '' && !in_array($code, self::IGNORE_LOCAL_APPS, true) && isset($apps[$code])) {
return $code;
}
if (isset($apps['index'])) {
return 'index';
}
return strval(array_key_first($apps) ?: 'index');
}
/**
* 获取默认本地应用编号.
*/
public static function singleCode(): string
{
return self::defaultAppCode();
}
/**
* 获取指定应用定义.
*
* @param ?string $code 应用编号
*/
public static function get(?string $code = null, bool $force = false): ?array
{
$apps = self::all($force);
return is_null($code) ? $apps : ($apps[$code] ?? null);
}
/**
* 按首段路径命中本地应用.
*
* @return null|array<string, mixed>
*/
public static function matchPath(string $pathinfo, bool $force = false): ?array
{
$pathinfo = trim($pathinfo, '\/');
if ($pathinfo === '') {
return null;
}
[$prefix, $suffix] = array_pad(explode('/', $pathinfo, 2), 2, '');
if (strpos($prefix, '.')) {
$prefix = strstr($prefix, '.', true) ?: $prefix;
}
if ($prefix === '' || !($app = self::localApp($prefix, $force))) {
return null;
}
$app['matched_prefix'] = $prefix;
$app['pathinfo'] = $suffix;
return $app;
}
/**
* 获取指定本地应用定义.
*/
public static function localApp(?string $code = null, bool $force = false): ?array
{
$apps = self::local($force);
return is_null($code) ? null : ($apps[$code] ?? null);
}
/**
* 判断插件是否存在。
* @param string $code 插件编码或别名
* @param bool $force 强制刷新
*/
public static function pluginExists(string $code, bool $force = false): bool
{
return self::resolvePlugin($code, false, $force) !== null;
}
/**
* 解析插件编码或别名。
* @param ?string $name 插件编码或别名
* @param bool $append 关联安装信息
* @param bool $force 强制刷新
*/
public static function resolvePlugin(?string $name, bool $append = false, bool $force = false): ?array
{
if ($name === null || $name === '') {
return null;
}
$plugins = self::allPlugins($append, $force);
if (isset($plugins[$name])) {
return $plugins[$name];
}
$bind = self::pluginBindings($force)[$name] ?? null;
if ($bind && isset($plugins[$bind])) {
return $plugins[$bind];
}
$code = self::pluginAliases($force)[$name] ?? null;
return $code ? ($plugins[$code] ?? null) : null;
}
/**
* 获取全部插件定义。
* @param bool $append 关联安装信息
* @param bool $force 强制刷新
*/
public static function allPlugins(bool $append = false, bool $force = false): array
{
if (!$append && !$force && is_array($plugins = sysvar(self::CACHE_PLUGINS))) {
return $plugins;
}
$plugins = [];
foreach ((array)Plugin::get() as $code => $plugin) {
$plugins[$code] = self::normalizePlugin($code, $plugin);
}
ksort($plugins);
if (!$append) {
sysvar(self::CACHE_PLUGINS, $plugins);
return $plugins;
}
return array_map(static function (array $plugin): array {
return self::appendPluginInstall($plugin);
}, $plugins);
}
/**
* 获取指定插件定义。
* @param ?string $code 插件编码
* @param bool $append 关联安装信息
* @param bool $force 强制刷新
*/
public static function getPlugin(?string $code = null, bool $append = false, bool $force = false): ?array
{
$plugins = self::allPlugins($append, $force);
return is_null($code) ? $plugins : ($plugins[$code] ?? null);
}
/**
* 获取插件前缀绑定关系。
* @param bool $force 强制刷新
* @return array<string, string>
*/
public static function pluginBindings(bool $force = false): array
{
if (!$force && is_array($bindings = sysvar(self::CACHE_BINDINGS))) {
return $bindings;
}
$bindings = [];
foreach (self::allPlugins(false, $force) as $code => $plugin) {
foreach ($plugin['prefixes'] ?? [] as $prefix) {
if ($prefix === '') {
continue;
}
if (isset($bindings[$prefix]) && $bindings[$prefix] !== $code) {
throw new \RuntimeException("Plugin prefix conflict [{$prefix}] between [{$bindings[$prefix]}] and [{$code}]");
}
$bindings[$prefix] = $code;
}
}
ksort($bindings);
return sysvar(self::CACHE_BINDINGS, $bindings);
}
/**
* 获取插件别名映射。
* @param bool $force 强制刷新
* @return array<string, string>
*/
public static function pluginAliases(bool $force = false): array
{
if (!$force && is_array($aliases = sysvar(self::CACHE_ALIASES))) {
return $aliases;
}
$aliases = [];
foreach (self::allPlugins(false, $force) as $code => $plugin) {
if (!empty($plugin['alias'])) {
$aliases[$plugin['alias']] = $code;
}
}
ksort($aliases);
return sysvar(self::CACHE_ALIASES, $aliases);
}
/**
* 获取插件主访问前缀。
* @param string $code 插件编码
* @param bool $force 强制刷新
*/
public static function pluginPrefix(string $code, bool $force = false): string
{
return self::activePluginPrefix($code, $force) ?: (self::pluginPrefixes($code, $force)[0] ?? '');
}
/**
* 获取当前激活前缀。
* @param ?string $code 插件编码
* @param bool $force 强制刷新
*/
public static function activePluginPrefix(?string $code = null, bool $force = false): string
{
$context = RequestContext::instance();
$current = $context->pluginCode();
$prefix = $context->pluginPrefix();
if ($current === '' || $prefix === '') {
return '';
}
if ($code !== null && $current !== $code) {
return '';
}
return in_array($prefix, self::pluginPrefixes($current, $force), true) ? $prefix : '';
}
/**
* 获取插件前缀集合。
* @param ?string $code 插件编码
* @param bool $force 强制刷新
* @return array<string, array<string>>|array<string>
*/
public static function pluginPrefixes(?string $code = null, bool $force = false): array
{
$plugins = self::allPlugins(false, $force);
if ($code === null) {
$items = [];
foreach ($plugins as $name => $plugin) {
$items[$name] = $plugin['prefixes'] ?? [];
}
return $items;
}
return $plugins[$code]['prefixes'] ?? [];
}
/**
* 按首段路径命中插件。
* @param string $pathinfo 请求路径
* @param ?string $switch 动态插件切换
*/
public static function matchPluginPath(string $pathinfo, ?string $switch = null): ?array
{
$pathinfo = trim($pathinfo, '\/');
$apiPrefix = self::pluginApiPrefix();
if ($pathinfo !== '' && $apiPrefix !== '') {
$paths = explode('/', $pathinfo, 3);
if (strval($paths[0] ?? '') === $apiPrefix) {
$prefix = strval($paths[1] ?? '');
if ($prefix !== '' && ($plugin = self::resolvePluginPrefix($prefix))) {
$plugin['entry'] = RequestContext::ENTRY_API;
$plugin['matched_prefix'] = $prefix;
$plugin['pathinfo'] = self::normalizeApiPathinfo(strval($paths[2] ?? 'index/index'));
return $plugin;
}
}
}
if ($pathinfo !== '') {
$paths = explode('/', $pathinfo, 2);
$prefix = strval($paths[0] ?? '');
if (strpos($prefix, '.')) {
$prefix = strstr($prefix, '.', true) ?: $prefix;
}
if ($prefix !== '' && ($plugin = self::resolvePluginPrefix($prefix))) {
$plugin['entry'] = RequestContext::ENTRY_WEB;
$plugin['matched_prefix'] = $prefix;
$plugin['pathinfo'] = $paths[1] ?? '';
return $plugin;
}
}
if ($switch && ($plugin = self::resolvePlugin($switch))) {
$plugin['entry'] = RequestContext::ENTRY_WEB;
$plugin['matched_prefix'] = '';
$plugin['pathinfo'] = $pathinfo;
return $plugin;
}
return null;
}
/**
* 获取 API 入口前缀。
*/
public static function pluginApiPrefix(): string
{
if (is_string($entry = sysvar(self::CACHE_ENTRY)) && $entry !== '') {
return $entry;
}
$entry = trim(strval(Library::$sapp->config->get('app.plugin.api_prefix', 'api')), '\/');
$entry = $entry !== '' ? $entry : 'api';
sysvar(self::CACHE_ENTRY, $entry);
return $entry;
}
/**
* 解析指定前缀绑定。
* @param ?string $prefix 路由前缀
* @param bool $append 关联安装信息
* @param bool $force 强制刷新
*/
public static function resolvePluginPrefix(?string $prefix, bool $append = false, bool $force = false): ?array
{
if ($prefix === null || $prefix === '') {
return null;
}
$code = self::pluginBindings($force)[$prefix] ?? null;
return $code ? self::getPlugin($code, $append, $force) : null;
}
/**
* 检测动态插件切换参数。
*/
public static function detectPluginSwitch(?Request $request = null): ?string
{
$config = self::pluginSwitchConfig();
if (empty($config['enabled'])) {
return null;
}
$request = $request ?: Library::$sapp->request;
$header = trim(strval($config['header'] ?? ''));
$query = trim(strval($config['query'] ?? ''));
$value = $header === '' ? '' : trim(strval($request->header($header) ?: ''));
if ($value === '' && $query !== '') {
$value = trim(strval($request->get($query, '')));
}
return $value === '' ? null : $value;
}
/**
* 激活当前请求插件。
* @param null|array|string $plugin 插件编码或定义
* @param string $prefix 当前请求前缀
*/
public static function activatePlugin($plugin = null, string $prefix = ''): ?array
{
$context = RequestContext::instance();
if (is_array($plugin)) {
$code = strval($plugin['code'] ?? '');
$current = $code === '' ? null : self::resolvePlugin($code);
} else {
$current = empty($plugin) ? null : self::resolvePlugin(strval($plugin));
}
if (empty($current)) {
$context->clearPlugin()->setEntryType(RequestContext::ENTRY_WEB);
return null;
}
$prefix = trim($prefix, '\/');
if ($prefix === '' || !in_array($prefix, $current['prefixes'] ?? [], true)) {
$prefix = $current['prefixes'][0] ?? '';
}
$context->setPlugin($current['code'], $prefix);
return $current;
}
/**
* 获取当前请求插件。
* @param bool $append 关联安装信息
* @param bool $force 强制刷新
*/
public static function currentPlugin(bool $append = false, bool $force = false): ?array
{
$code = RequestContext::instance()->pluginCode();
return $code === '' ? null : self::getPlugin($code, $append, $force);
}
/**
* 获取当前请求插件前缀。
*/
public static function currentPluginPrefix(): string
{
return RequestContext::instance()->pluginPrefix();
}
/**
* 设置当前请求插件入口类型。
*/
public static function activatePluginEntry(string $entryType = RequestContext::ENTRY_WEB): void
{
RequestContext::instance()->setEntryType($entryType);
}
/**
* 获取当前请求插件入口类型。
*/
public static function currentPluginEntry(): string
{
return RequestContext::instance()->entryType();
}
/**
* 获取当前请求插件编码。
*/
public static function currentPluginCode(): string
{
return RequestContext::instance()->pluginCode();
}
/**
* 获取插件菜单定义。
* @param null|array|string $plugin 插件编码或定义
* @param bool $check 检查权限
* @param bool $normalize 标准化输出
*/
public static function menus($plugin, bool $check = false, bool $normalize = false): array
{
if (is_array($plugin)) {
$code = strval($plugin['code'] ?? '');
$current = $code === '' ? $plugin : array_replace(self::resolvePlugin($code, true) ?: [], $plugin);
} else {
$current = self::resolvePlugin($plugin, true);
}
if (empty($current['service']) || !class_exists($current['service'])) {
return [];
}
$menus = (array)$current['service']::getMenus();
return ($check || $normalize) ? self::normalizeMenus($menus, $check) : $menus;
}
/**
* 获取版本信息。
*/
public static function getVersion(): string
{
$library = self::getPluginLibrarys('zoujingli/think-library');
return trim($library['version'] ?? 'v8.0.0', 'v');
}
/**
* 获取插件包版本信息。
* @param ?string $package 包名
* @param bool $force 强制刷新
* @return array|mixed
*/
public static function getPluginLibrarys(?string $package = null, bool $force = false)
{
$plugs = sysvar($keys = 'think.admin.version');
if (empty($plugs) || $force) {
foreach (array_unique([runpath('vendor/versions.php'), syspath('vendor/versions.php')]) as $file) {
if (is_file($file)) {
$plugs = sysvar($keys, include $file);
break;
}
}
}
return empty($package) ? $plugs : ($plugs[$package] ?? null);
}
/**
* 生成全部静态路径。
* @param string $path 后缀路径
* @return string[]
*/
public static function uris(string $path = ''): array
{
return self::uri($path, null);
}
/**
* 生成静态路径链接。
* @param string $path 后缀路径
* @param ?string $type 路径类型
* @param mixed $default 默认数据
* @return array|string
*/
public static function uri(string $path = '', ?string $type = '__ROOT__', $default = '')
{
$plugin = Library::$sapp->http->getName();
if (strlen($path)) {
$path = '/' . ltrim($path, '/');
}
$prefix = rtrim(dirname(Library::$sapp->request->basefile()), '\/');
$data = [
'__APP__' => rtrim(url('@')->build(), '\/') . $path,
'__ROOT__' => $prefix . $path,
'__PLUG__' => "{$prefix}/static/extra/{$plugin}{$path}",
'__FULL__' => Library::$sapp->request->domain() . $prefix . $path,
];
return is_null($type) ? $data : ($data[$type] ?? $default);
}
/**
* 打印调试数据到文件。
* @param mixed $data 输出的数据
* @param bool $new 强制替换文件
* @param null|string $file 文件名称
* @return false|int
*/
public static function putDebug($data, bool $new = false, ?string $file = null)
{
ob_start();
var_dump($data);
$output = preg_replace('/]=>\n(\s+)/m', '] => ', ob_get_clean());
if (is_null($file)) {
$file = runpath('runtime/' . date('Ymd') . '.log');
} elseif (!preg_match('#[/\\\]+#', $file)) {
$file = runpath("runtime/{$file}.log");
}
is_dir($dir = dirname($file)) or mkdir($dir, 0777, true);
return $new ? file_put_contents($file, $output) : file_put_contents($file, $output, FILE_APPEND);
}
/**
* 批量更新保存数据。
* @param Model|Query|string $query 数据查询对象
* @param array $data 需要保存的数据
* @param string $key 更新条件查询主键
* @param mixed $map 额外更新查询条件
* @return bool|int
* @throws \think\admin\Exception
*/
public static function update($query, array $data, string $key = 'id', $map = [])
{
try {
$query = QueryFactory::build($query)->master()->where($map);
if (empty($map[$key])) {
$query->where([$key => $data[$key] ?? null]);
}
return (clone $query)->count() > 1 ? $query->strict(false)->update($data) : $query->findOrEmpty()->save($data);
} catch (\Exception|\Throwable $exception) {
throw new \think\admin\Exception($exception->getMessage(), $exception->getCode());
}
}
/**
* 数据增量保存。
* @param Model|Query|string $query 数据查询对象
* @param array $data 需要保存的数据
* @param string $key 更新条件查询主键
* @param mixed $map 额外更新查询条件
* @return bool|int
* @throws \think\admin\Exception
*/
public static function save($query, array &$data, string $key = 'id', $map = [])
{
try {
$query = QueryFactory::build($query)->master()->strict(false);
if (empty($map[$key])) {
$query->where([$key => $data[$key] ?? null]);
}
$model = $query->where($map)->findOrEmpty();
$action = $model->isExists() ? 'onAdminUpdate' : 'onAdminInsert';
if ($model->save($data) === false) {
return false;
}
if ($model instanceof \think\admin\Model) {
$model->{$action}(strval($model->getAttr($key)));
}
$data = $model->toArray();
return $model[$key] ?? true;
} catch (\Exception $exception) {
throw new \think\admin\Exception($exception->getMessage(), $exception->getCode());
}
}
/**
* 扫描 app/* 本地应用目录.
*
* @return array<string, array<string, string>>
*/
private static function discoverLocalApps(): array
{
$apps = [];
$basePath = rtrim(Library::$sapp->getBasePath(), '\/') . DIRECTORY_SEPARATOR;
if (!is_dir($basePath)) {
return $apps;
}
foreach (scandir($basePath) ?: [] as $code) {
if ($code === '.' || $code === '..' || in_array($code, self::IGNORE_LOCAL_APPS, true)) {
continue;
}
if (!preg_match('/^[A-Za-z][A-Za-z0-9_]*$/', $code)) {
continue;
}
$path = $basePath . $code . DIRECTORY_SEPARATOR;
if (!is_dir($path) || !self::isLocalAppPath($path)) {
continue;
}
$apps[$code] = self::normalize($code, [
'type' => 'local',
'name' => ucfirst($code),
'path' => $path,
'space' => NodeService::space($code),
]);
}
return $apps;
}
/**
* 判断是否为本地应用目录.
*/
private static function isLocalAppPath(string $path): bool
{
foreach (['controller', 'route', 'view', 'config'] as $name) {
if (is_dir($path . $name)) {
return true;
}
}
return is_file($path . 'Service.php');
}
/**
* 递归收集菜单节点。
* @param array<int, array<string, mixed>> $menus
* @param array<int, string> $nodes
*/
private static function collectMenuNodes(array $menus, array &$nodes): void
{
foreach ($menus as $menu) {
if (!empty($menu['node'])) {
$nodes[] = strval($menu['node']);
}
if (!empty($menu['subs'])) {
self::collectMenuNodes((array)$menu['subs'], $nodes);
}
}
}
/**
* 标准化插件菜单并可选按权限过滤。
* @param array<int, array<string, mixed>> $menus
* @return array<int, array<string, mixed>>
*/
private static function normalizeMenus(array $menus, bool $check = false): array
{
foreach ($menus as $k1 => &$one) {
$one['title'] = lang($one['title'] ?? ($one['name'] ?? ''));
$one['url'] = $one['url'] ?? self::buildMenuUrl(strval($one['node'] ?? ''));
if (!empty($one['subs'])) {
foreach ($one['subs'] as $k2 => &$two) {
if ($check && isset($two['node']) && !auth($two['node'])) {
unset($one['subs'][$k2]);
continue;
}
$two['title'] = lang($two['title'] ?? ($two['name'] ?? ''));
$two['url'] = $two['url'] ?? self::buildMenuUrl(strval($two['node'] ?? ''));
}
$one['subs'] = array_values($one['subs']);
}
if ($check && isset($one['node']) && !auth($one['node'])) {
unset($menus[$k1]);
continue;
}
if ($one['url'] === '#' && empty($one['subs'])) {
unset($menus[$k1]);
}
}
return array_values($menus);
}
/**
* 生成菜单 URL缺少插件上下文时回退为系统 URL。
*/
private static function buildMenuUrl(string $node): string
{
if ($node === '') {
return '#';
}
if (function_exists('plguri') && self::currentPluginCode() !== '') {
return plguri($node);
}
return sysuri($node);
}
/**
* 标准化应用定义.
*
* @param string $code 应用编号
* @param array<string, mixed> $app 应用配置
* @return array<string, string>
*/
private static function normalize(string $code, array $app): array
{
$path = $app['path'] ?? '';
$path = $path === '' ? '' : rtrim((string)$path, '\/') . DIRECTORY_SEPARATOR;
return [
'code' => $code,
'type' => $app['type'] ?? 'local',
'name' => $app['name'] ?? ucfirst($code),
'path' => $path,
'alias' => $app['alias'] ?? '',
'space' => $app['space'] ?? NodeService::space($code),
'package' => $app['package'] ?? '',
'service' => $app['service'] ?? '',
];
}
/**
* 标准化插件定义。
* @param string $code 插件编码
* @param array $plugin 原始定义
*/
private static function normalizePlugin(string $code, array $plugin): array
{
$path = $plugin['path'] ?? '';
$path = $path === '' ? '' : rtrim((string)$path, '\/') . DIRECTORY_SEPARATOR;
$prefixes = self::effectivePluginPrefixes($code, $plugin);
return [
'code' => $code,
'type' => $plugin['type'] ?? 'plugin',
'name' => $plugin['name'] ?? ucfirst($code),
'path' => $path,
'alias' => $plugin['alias'] ?? '',
'prefix' => $prefixes[0] ?? '',
'prefixes' => $prefixes,
'space' => $plugin['space'] ?? '',
'package' => $plugin['package'] ?? '',
'service' => $plugin['service'] ?? '',
'document' => $plugin['document'] ?? '',
'description' => $plugin['description'] ?? '',
'platforms' => (array)($plugin['platforms'] ?? []),
'license' => (array)($plugin['license'] ?? []),
'version' => strval($plugin['version'] ?? ''),
'homepage' => strval($plugin['homepage'] ?? ''),
'show' => !array_key_exists('show', $plugin) || !empty($plugin['show']),
];
}
/**
* 获取插件有效前缀。
* @param string $code 插件编码
* @param array $plugin 插件定义
* @return string[]
*/
private static function effectivePluginPrefixes(string $code, array $plugin): array
{
$prefixes = self::configuredPluginPrefixes($code);
if ($prefixes === null) {
$prefixes = self::normalizePluginPrefixes($plugin['prefixes'] ?? [], $plugin['prefix'] ?? '', $plugin['alias'] ?? '', $code);
}
if (empty($prefixes)) {
$prefixes = [$code];
}
return $prefixes;
}
/**
* 获取配置文件中的插件前缀定义。
* @param string $code 插件编码
* @return null|string[]
*/
private static function configuredPluginPrefixes(string $code): ?array
{
$config = (array)Library::$sapp->config->get('app.plugin.bindings', []);
if (array_key_exists($code, $config)) {
return self::normalizePluginPrefixes($config[$code]);
}
foreach ($config as $item) {
if (!is_array($item)) {
continue;
}
$name = strval($item['code'] ?? $item['plugin'] ?? '');
if ($name === $code) {
return self::normalizePluginPrefixes($item['prefixes'] ?? ($item['prefix'] ?? []));
}
}
return null;
}
/**
* 标准化前缀集合。
* @param mixed ...$values 原始前缀
* @return string[]
*/
private static function normalizePluginPrefixes(...$values): array
{
$items = [];
foreach ($values as $value) {
foreach ((array)$value as $prefix) {
$prefix = trim((string)$prefix, " \t\n\r\0\x0B\\/");
if ($prefix === '') {
continue;
}
if (strpos($prefix, '/')) {
$prefix = strstr($prefix, '/', true) ?: $prefix;
}
if (strpos($prefix, '.')) {
$prefix = strstr($prefix, '.', true) ?: $prefix;
}
if ($prefix !== '' && !in_array($prefix, $items, true)) {
$items[] = $prefix;
}
}
}
return $items;
}
/**
* 附加插件安装信息。
* @param array $plugin 插件定义
*/
private static function appendPluginInstall(array $plugin): array
{
$versions = self::getPluginLibrarys();
$plugin['install'] = $versions[$plugin['package']] ?? [];
foreach (['type', 'name', 'document', 'description', 'homepage', 'version'] as $field) {
if (empty($plugin[$field])) {
$plugin[$field] = $plugin['install'][$field] ?? ($field === 'type' ? 'plugin' : '');
}
}
$plugin['platforms'] = array_values(array_unique(array_filter(array_merge(
(array)($plugin['platforms'] ?? []),
(array)($plugin['install']['platforms'] ?? [])
))));
$plugin['license'] = array_values(array_unique(array_filter(array_merge(
(array)($plugin['license'] ?? []),
(array)($plugin['install']['license'] ?? [])
))));
return $plugin;
}
/**
* 获取插件切换配置。
* @return array{enabled:bool,query:string,header:string}
*/
private static function pluginSwitchConfig(): array
{
$config = (array)Library::$sapp->config->get('app.plugin.switch', []);
return [
'enabled' => isset($config['enabled']) ? boolval($config['enabled']) : false,
'query' => strval($config['query'] ?? '_plugin'),
'header' => strval($config['header'] ?? 'X-Plugin-App'),
];
}
/**
* 标准化 API 入口路径。
* /api/{plugin}/upload/file -> api.upload/file.
*/
private static function normalizeApiPathinfo(string $pathinfo): string
{
$pathinfo = trim($pathinfo, '\/');
if ($pathinfo === '') {
return 'api.index/index';
}
if (strpos($pathinfo, 'api.') === 0) {
return $pathinfo;
}
[$controller, $action] = array_pad(explode('/', $pathinfo, 2), 2, 'index');
$controller = trim(strtr($controller, '/', '.'), '.');
return 'api.' . $controller . '/' . trim($action, '\/');
}
}