Files
laralog/app/Blog/Support/PluginManager.php
T
ak 01861f8809 feat: 插件使用说明(usage)+ 页面缓存命中统计
- plugin.json 新增 usage 字段(多行文本),后台插件卡片折叠展示;四个内置插件补齐使用说明
- PageCacheMiddleware 记录命中/未命中计数(按日滚动),仪表盘新增「页面缓存」统计卡(条目数 + 命中率),用于判断前台缓存效果、是否需额外加速
- 文档与测试同步
2026-08-12 03:57:22 +08:00

262 lines
7.9 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);
namespace App\Blog\Support;
use App\Models\PluginRecord;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Str;
use RuntimeException;
class PluginManager
{
/** @var array<string, array<int, callable>> */
private array $actions = [];
/** @var array<string, array<int, callable>> */
private array $filters = [];
/** @var array<string, class-string<\App\Blog\Support\PluginServiceProvider>> */
private array $providers = [];
public function path(?string $plugin = null): string
{
return config('plugins.path').($plugin ? '/'.$plugin : '');
}
public function exists(string $plugin): bool
{
return is_dir($this->path($plugin)) && File::exists($this->path($plugin).'/plugin.json');
}
public function manifest(string $plugin): array
{
$file = $this->path($plugin).'/plugin.json';
if (! File::exists($file)) {
return [];
}
return json_decode(File::get($file), true) ?? [];
}
public function all(): array
{
$plugins = [];
foreach (File::directories(config('plugins.path')) as $dir) {
$basename = basename($dir);
$manifest = $this->manifest($basename);
if (! $manifest) {
continue;
}
[$vendor, $name] = array_pad(explode('.', $basename), 2, $basename);
$requires = $manifest['requires'] ?? [];
$dependencies = [];
$dependencyErrors = [];
foreach ($requires as $req) {
$installed = $this->exists($req);
$enabled = $installed && $this->isEnabled($req);
$dependencies[] = [
'key' => $req,
'title' => $this->manifest($req)['title'] ?? $req,
'ok' => $enabled,
'reason' => $installed ? ($enabled ? '' : '未启用') : '未安装',
];
if (! $enabled) {
$dependencyErrors[] = $req;
}
}
$plugins[$basename] = [
'key' => $basename,
'vendor' => $vendor,
'name' => $name,
'title' => $manifest['title'] ?? $name,
'version' => $manifest['version'] ?? '0.0.0',
'description' => $manifest['description'] ?? '',
'usage' => $manifest['usage'] ?? '',
'author' => $manifest['author'] ?? '',
'enabled' => $this->isEnabled($basename),
'requires' => $requires,
'dependencies' => $dependencies,
'dependency_errors' => $dependencyErrors,
];
}
return $plugins;
}
public function isEnabled(string $plugin): bool
{
if (\Illuminate\Support\Facades\Schema::hasTable('plugin_records')) {
[$vendor, $name] = array_pad(explode('.', $plugin), 2, $plugin);
$record = PluginRecord::query()->where('vendor', $vendor)->where('name', $name)->first();
if ($record) {
return (bool) $record->enabled;
}
}
return in_array($plugin, config('plugins.enabled', []), true);
}
public function requires(string $plugin): array
{
return $this->manifest($plugin)['requires'] ?? [];
}
/**
* 依赖未满足的问题列表(空数组 = 依赖完好)。
*/
public function dependencyProblems(string $plugin): array
{
$problems = [];
foreach ($this->requires($plugin) as $req) {
if (! $this->exists($req)) {
$problems[] = "缺少依赖插件:{$req}(未安装)";
} elseif (! $this->isEnabled($req)) {
$problems[] = '依赖插件未启用:'.$req.''.$this->manifest($req)['title'].'';
}
}
return $problems;
}
/**
* 已启用且依赖本插件的插件列表。
*/
public function dependents(string $plugin): array
{
$dependents = [];
foreach ($this->all() as $key => $p) {
if ($p['enabled'] && in_array($plugin, $p['requires'] ?? [], true)) {
$dependents[] = $key;
}
}
return $dependents;
}
public function enable(string $plugin): void
{
$problems = $this->dependencyProblems($plugin);
if ($problems) {
throw new RuntimeException('无法启用 '.$plugin.''.implode('', $problems));
}
[$vendor, $name] = array_pad(explode('.', $plugin), 2, $plugin);
PluginRecord::query()->updateOrCreate(
['vendor' => $vendor, 'name' => $name],
['enabled' => true]
);
}
public function disable(string $plugin): void
{
$dependents = $this->dependents($plugin);
if ($dependents) {
throw new RuntimeException('无法停用 '.$plugin.':以下已启用插件依赖它:'.implode('、', $dependents).'。请先停用这些插件。');
}
[$vendor, $name] = array_pad(explode('.', $plugin), 2, $plugin);
PluginRecord::query()->updateOrCreate(
['vendor' => $vendor, 'name' => $name],
['enabled' => false]
);
}
public function uninstall(string $plugin): void
{
$dependents = $this->dependents($plugin);
if ($dependents) {
throw new RuntimeException('无法卸载 '.$plugin.':以下已启用插件依赖它:'.implode('、', $dependents).'。请先停用这些插件。');
}
[$vendor, $name] = array_pad(explode('.', $plugin), 2, $plugin);
PluginRecord::query()->where('vendor', $vendor)->where('name', $name)->delete();
}
/**
* 解析插件提供者类:plugins/{vendor}.{name}/src/PluginServiceProvider.php
*
* @return class-string<\App\Blog\Support\PluginServiceProvider>|null
*/
public function resolveProvider(string $plugin): ?string
{
$class = $this->providers[$plugin] ?? null;
if ($class) {
return $class;
}
// 类名兜底:先找 src/ServiceProvider.php,再找 src/PluginServiceProvider.php
$manifest = $this->manifest($plugin);
$class = $manifest['provider'] ?? null;
if (! $class) {
$candidates = ['/src/ServiceProvider.php', '/src/PluginServiceProvider.php'];
foreach ($candidates as $candidate) {
if (File::exists($this->path($plugin).$candidate)) {
[$vendor, $name] = array_pad(explode('.', $plugin), 2, $plugin);
$class = 'Plugins\\'.Str::studly($vendor).'\\'.Str::studly($name).'\\'.basename($candidate, '.php');
break;
}
}
}
return $this->providers[$plugin] = $class;
}
public function registerProvider(string $plugin, string $class): void
{
$this->providers[$plugin] = $class;
}
/* ---------------- Hooks ---------------- */
public function addAction(string $hook, callable $callback, int $priority = 10): void
{
$this->actions[$hook][$priority][] = $callback;
ksort($this->actions[$hook]);
}
public function addFilter(string $hook, callable $callback, int $priority = 10): void
{
$this->filters[$hook][$priority][] = $callback;
ksort($this->filters[$hook]);
}
public function doAction(string $hook, mixed ...$args): void
{
foreach ($this->actions[$hook] ?? [] as $callbacks) {
foreach ($callbacks as $callback) {
$callback(...$args);
}
}
}
public function applyFilters(string $hook, mixed $value, mixed ...$args): mixed
{
foreach ($this->filters[$hook] ?? [] as $callbacks) {
foreach ($callbacks as $callback) {
$value = $callback($value, ...$args);
}
}
return $value;
}
}