Files
laralog/app/Blog/Support/PluginManager.php
T
goukiandCursor 469160dc3f
Build and Deploy / verify (push) Failing after 15m17s
Build and Deploy / deploy (push) Skipped
feat: add admin env badge, quick post button, and harden plugin enable
Show local/prod in the Filament topbar, jump to create post in one click, tighten sidebar spacing, and stop Hello World from 500ing when its tables are missing.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-18 08:24:16 +00:00

296 lines
8.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\Artisan;
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));
}
$this->migratePlugin($plugin);
[$vendor, $name] = array_pad(explode('.', $plugin), 2, $plugin);
PluginRecord::query()->updateOrCreate(
['vendor' => $vendor, 'name' => $name],
['enabled' => true]
);
}
/**
* Run the plugin's own migrations so enabling a demo/third-party plugin
* does not 500 on missing tables.
*/
public function migratePlugin(string $plugin): void
{
$relativePath = 'plugins/'.$plugin.'/database/migrations';
if (! is_dir(base_path($relativePath))) {
return;
}
$exitCode = Artisan::call('migrate', [
'--path' => $relativePath,
'--force' => true,
'--no-interaction' => true,
]);
if ($exitCode !== 0) {
throw new RuntimeException('插件迁移失败:'.$plugin.'。'.trim(Artisan::output()));
}
}
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) {
try {
$callback(...$args);
} catch (\Throwable $e) {
report($e);
}
}
}
}
public function applyFilters(string $hook, mixed $value, mixed ...$args): mixed
{
foreach ($this->filters[$hook] ?? [] as $callbacks) {
foreach ($callbacks as $callback) {
try {
$value = $callback($value, ...$args);
} catch (\Throwable $e) {
report($e);
}
}
}
return $value;
}
}