Files
laralog/app/Blog/Support/PluginManager.php
T
ak 4f59afb7ce feat: 插件依赖强制校验 + 文章后台表单/表格注入 hook
- PluginManager: requires 依赖链校验(enable 需依赖已启用,disable 阻止有启用依赖者的插件,boot 跳过依赖未满足插件),all() 输出依赖状态供后台展示
- Plugins 页:卡片显示依赖链徽章,依赖未满足时禁用启用按钮,启停失败弹错误通知
- PostResource: form()/table() 增加 filament.post_form / filament.post_table 注入点
- membership 插件:注入「付费设置」Section(meta.price 单篇价格 + meta.members_only 会员专享)与表格价格/会员列
2026-08-12 00:18:40 +08:00

255 lines
7.6 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'] ?? '',
'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
{
[$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;
}
}