M4: 插件框架(钩子系统/ZIP安装/市场客户端)+ blog-seo 插件(JSON-LD/OG/llms.txt/Markdown导出)+ 后台插件/主题管理页
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
<?php
|
||||
|
||||
namespace App\Blog\Support;
|
||||
|
||||
use App\Models\PluginRecord;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
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);
|
||||
|
||||
$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),
|
||||
];
|
||||
}
|
||||
|
||||
return $plugins;
|
||||
}
|
||||
|
||||
public function isEnabled(string $plugin): bool
|
||||
{
|
||||
[$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 enable(string $plugin): void
|
||||
{
|
||||
[$vendor, $name] = array_pad(explode('.', $plugin), 2, $plugin);
|
||||
PluginRecord::query()->updateOrCreate(
|
||||
['vendor' => $vendor, 'name' => $name],
|
||||
['enabled' => true]
|
||||
);
|
||||
}
|
||||
|
||||
public function disable(string $plugin): void
|
||||
{
|
||||
[$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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Blog\Support;
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
abstract class PluginServiceProvider
|
||||
{
|
||||
/**
|
||||
* 插件入口:由 PluginManagerServiceProvider 在应用启动时调用。
|
||||
*/
|
||||
public static function bootPlugin(PluginManager $manager): void
|
||||
{
|
||||
(new static)->boot($manager);
|
||||
}
|
||||
|
||||
abstract protected function boot(PluginManager $manager): void;
|
||||
|
||||
protected function addAction(PluginManager $manager, string $hook, callable $callback, int $priority = 10): void
|
||||
{
|
||||
$manager->addAction($hook, $callback, $priority);
|
||||
}
|
||||
|
||||
protected function addFilter(PluginManager $manager, string $hook, callable $callback, int $priority = 10): void
|
||||
{
|
||||
$manager->addFilter($hook, $callback, $priority);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载插件路由文件。
|
||||
*/
|
||||
protected function loadRoutes(string $file): void
|
||||
{
|
||||
if (file_exists($file)) {
|
||||
Route::middleware('web')->group($file);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册插件视图命名空间:plugins/{vendor}.{name}/views -> plugin.{vendor}.{name}
|
||||
*/
|
||||
protected function loadViews(string $viewsPath, string $namespace): void
|
||||
{
|
||||
if (is_dir($viewsPath)) {
|
||||
\Illuminate\Support\Facades\View::addNamespace($namespace, $viewsPath);
|
||||
}
|
||||
}
|
||||
|
||||
protected function slug(string $string): string
|
||||
{
|
||||
return Str::slug($string);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user