> */ private array $actions = []; /** @var array> */ private array $filters = []; /** @var array> */ 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 { $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; } }