diff --git a/src/Templates/PluginMain.php.template b/src/Templates/PluginMain.php.template index 3787e18e..a0607351 100644 --- a/src/Templates/PluginMain.php.template +++ b/src/Templates/PluginMain.php.template @@ -4,10 +4,13 @@ declare(strict_types=1); namespace {namespace}; +use ZM\Annotation\OneBot\BotCommand; +use ZM\Context\BotContext; + class {class} { - #[\BotCommand(match: '测试{basename}')] - public function firstBotCommand(\BotContext $ctx): void + #[BotCommand(match: '测试{basename}')] + public function firstBotCommand(BotContext $ctx): void { $ctx->reply('这是{name}插件的第一个命令!'); } diff --git a/src/ZM/Command/Plugin/PluginCommand.php b/src/ZM/Command/Plugin/PluginCommand.php index 94b18812..8618aa4f 100644 --- a/src/ZM/Command/Plugin/PluginCommand.php +++ b/src/ZM/Command/Plugin/PluginCommand.php @@ -125,9 +125,9 @@ abstract class PluginCommand extends Command { return match ($type) { ZM_PLUGIN_TYPE_NATIVE => '内部', - ZM_PLUGIN_TYPE_PHAR => 'Phar', - ZM_PLUGIN_TYPE_SOURCE => '源码', - ZM_PLUGIN_TYPE_COMPOSER => 'Composer 外部加载', + ZM_PLUGIN_TYPE_PHAR => 'Phar', + ZM_PLUGIN_TYPE_SOURCE => '源码', + ZM_PLUGIN_TYPE_COMPOSER => 'Composer', default => '未知模式' }; } diff --git a/src/ZM/Command/Plugin/PluginInstallCommand.php b/src/ZM/Command/Plugin/PluginInstallCommand.php index c491a517..9e8069a1 100644 --- a/src/ZM/Command/Plugin/PluginInstallCommand.php +++ b/src/ZM/Command/Plugin/PluginInstallCommand.php @@ -7,10 +7,9 @@ namespace ZM\Command\Plugin; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; -use ZM\Exception\FileSystemException; -use ZM\Plugin\PluginManager; +use ZM\Plugin\Strategy\ComposerStrategy; +use ZM\Plugin\Strategy\GitStrategy; use ZM\Store\FileSystem; -use ZM\Utils\ZMRequest; #[AsCommand(name: 'plugin:install', description: '从 GitHub 或其他 Git 源码托管站安装插件')] class PluginInstallCommand extends PluginCommand @@ -18,6 +17,7 @@ class PluginInstallCommand extends PluginCommand protected function configure() { $this->addArgument('address', InputArgument::REQUIRED, '插件地址'); + $this->addOption('github-token', null, InputOption::VALUE_REQUIRED, '提供的 GitHub Token'); // 下面是辅助用的,和 server:start 一样 $this->addOption('config-dir', null, InputOption::VALUE_REQUIRED, '指定其他配置文件目录'); @@ -29,87 +29,36 @@ class PluginInstallCommand extends PluginCommand protected function handle(): int { $addr = $this->input->getArgument('address'); - $name = ob_uuidgen(); $this->plugin_dir = FileSystem::isRelativePath(config('global.plugin.load_dir', 'plugins')) ? (WORKING_DIR . '/' . config('global.plugin.load_dir', 'plugins')) : config('global.plugin.load_dir', 'plugins'); - // 先通过 GitHub API 获取看看存不存在 zmplugin.json - // 解析 git https 路径中的仓库所有者和仓库名 - $git_url = parse_url($addr); - if ($git_url['host'] === 'github.com') { - $path = explode('/', $git_url['path']); - $owner = $path[1]; - $repo = $path[2]; - if (str_ends_with($repo, '.git')) { - $repo = substr($repo, 0, -4); - } - $api = ZMRequest::get('https://api.github.com/repos/' . $owner . '/' . $repo . '/contents/zmplugin.json', ['User-Agent' => 'ZMFramework']); - if ($api === false) { - $this->error('GitHub API 请求失败'); - return static::FAILURE; - } - $api = json_decode($api, true); - if (isset($api['message'])) { - $this->error('该项目中不存在 zmplugin.json 元信息!'); - return static::FAILURE; - } - $contents = implode('', array_map(fn ($x) => base64_decode($x), explode("\n", $api['content']))); - $json = json_decode($contents, true); - if (!isset($json['name'])) { - $this->error('插件元信息内没有名字!'); - return static::FAILURE; - } - $plugin_name = $json['name']; - if (PluginManager::isPluginExists($plugin_name)) { - $this->error('插件 ' . $plugin_name . ' 已存在,无法再次安装!'); - return static::FAILURE; - } + + // 先检查传入的参数是什么类型。如果是 a/b 类型则为 composer 包 + if (count(explode('/', $addr)) === 2) { + $st = new ComposerStrategy($addr, $this->plugin_dir, logger: $this); + } elseif ((parse_url($addr)['scheme'] ?? null) !== null) { + $st = new GitStrategy($addr, $this->plugin_dir, logger: $this); + } else { + $this->error('无法检测输入要安装插件的链接或名字,请检查后再试!'); + return static::FAILURE; } - $this->info('正在从 ' . $addr . ' 克隆插件仓库'); + + if ($token = $this->input->getOption('github-token')) { + $option = ['github-token' => $token]; + } else { + $option = []; + } + // 然后调用安装并看是否成功 try { - FileSystem::createDir($this->plugin_dir); - } catch (FileSystemException $exception) { - $this->error("无法创建插件目录 {$this->plugin_dir}:{$exception->getMessage()}"); - return static::FAILURE; - } - passthru('cd ' . escapeshellarg($this->plugin_dir) . ' && git clone --depth=1 ' . escapeshellarg($addr) . ' ' . $name, $code); - if ($code !== 0) { - $this->error('无法从指定 Git 地址拉取项目,请检查地址名是否正确'); - return static::FAILURE; - } - if (!file_exists($this->plugin_dir . '/' . $name . '/zmplugin.json')) { - $this->error('项目不存在 zmplugin.json 插件元信息,无法安装,请手动删除目录 ' . $name); - // TODO: 使用 rmdir 和 unlink 删除 git 目录 - return static::FAILURE; - } - $this->output->writeln('正在检查元信息完整性'); - $getname = json_decode(file_get_contents($this->plugin_dir . '/' . $name . '/zmplugin.json'), true)['name'] ?? null; - if ($getname === null) { - $this->error('无法获取元信息 zmplugin.json'); - return static::FAILURE; - } - $code = rename($this->plugin_dir . '/' . $name, $this->plugin_dir . '/' . $getname); - if ($code === false) { - $this->error('无法重命名文件夹 ' . $name); - return static::FAILURE; - } - if (file_exists($this->plugin_dir . '/' . $getname . '/composer.json')) { - $this->info('插件存在 composer.json,正在安装 composer 相关依赖(需要系统环境变量中包含 composer 路径)'); - $cwd = getcwd(); - chdir($this->plugin_dir . '/' . $getname); - // 使用内建 Composer - if (file_exists(WORKING_DIR . '/runtime/composer.phar')) { - $this->info('使用内建 Composer'); - passthru(PHP_BINARY . ' ' . escapeshellarg(WORKING_DIR . '/runtime/composer.phar') . ' install --no-dev', $code); - } else { - $this->info('使用系统 Composer'); - passthru('composer install --no-dev', $code); - } - chdir($cwd); - if ($code != 0) { - $this->error('无法安装 Composer 依赖,请检查 Composer 是否可以正常运行'); + if (!$st->install($option)) { + $this->error('插件安装失败,' . $st->getError()); return static::FAILURE; } + } catch (\Throwable $e) { + $this->error('niu: ' . $e->getMessage()); + $this->error($e->getTraceAsString()); + return static::FAILURE; } - $this->info('插件 ' . $getname . ' 安装成功!'); + + $this->info('插件 ' . $st->getInstalledName() . ' 安装成功!'); return static::SUCCESS; } } diff --git a/src/ZM/Command/Plugin/PluginListCommand.php b/src/ZM/Command/Plugin/PluginListCommand.php index 9ecdfd90..e982a58b 100644 --- a/src/ZM/Command/Plugin/PluginListCommand.php +++ b/src/ZM/Command/Plugin/PluginListCommand.php @@ -11,13 +11,30 @@ use ZM\Plugin\PluginManager; #[AsCommand(name: 'plugin:list', description: '显示插件列表')] class PluginListCommand extends PluginCommand { + protected function configure() + { + $this->addOption('name-list', 'N', null, '只输出插件列表的名字'); + } + protected function handle(): int { $all = PluginManager::getPlugins(); + if ($all === []) { + $this->info('当前未安装任何插件'); + return static::SUCCESS; + } + if ($this->input->getOption('name-list')) { + $this->info('插件列表: '); + foreach ($all as $k => $v) { + $this->write($k); + } + return static::SUCCESS; + } $table = new Table($this->output); - $table->setHeaders(['名称', '版本', '类型']); + $table->setColumnMaxWidth(2, 27); + $table->setHeaders(['名称', '版本', '简介', '类型']); foreach ($all as $k => $v) { - $table->addRow([$k, $v->getVersion(), $this->getTypeDisplayName($v->getPluginType())]); + $table->addRow([$k, $v->getVersion(), $v->getDescription(), $this->getTypeDisplayName($v->getPluginType())]); } $table->setStyle('box'); $table->render(); diff --git a/src/ZM/Plugin/PluginManager.php b/src/ZM/Plugin/PluginManager.php index 5521252d..a3689e06 100644 --- a/src/ZM/Plugin/PluginManager.php +++ b/src/ZM/Plugin/PluginManager.php @@ -25,7 +25,7 @@ class PluginManager } /** - * 传入插件父目录,扫描插件目录下的所有插件并注册添加 + * 传入插件父目录,扫描插件目录下的所有插件并注册添加(开发插件) * * @param string $dir 插件目录 * @return int 返回添加插件的数量 @@ -54,21 +54,31 @@ class PluginManager } // 先看有没有 zmplugin.json,没有则不是正常的插件,发个 notice 然后跳过 - $meta_file = $item . '/zmplugin.json'; + $meta_file = $item . '/composer.json'; if (!is_file($meta_file)) { - logger()->notice('插件目录 {dir} 没有插件元信息(zmplugin.json),跳过扫描。', ['dir' => $item]); + logger()->notice('插件目录 {dir} 没有插件元信息(composer.json),跳过扫描。', ['dir' => $item]); continue; } // 检验元信息是否合法,不合法发个 notice 然后跳过 $json_meta = json_decode(file_get_contents($meta_file), true); if (!is_array($json_meta)) { - logger()->notice('插件目录 {dir} 的插件元信息(zmplugin.json)不是有效的 JSON,跳过扫描。', ['dir' => $item]); + logger()->notice('插件目录 {dir} 的插件元信息(composer.json)不是有效的 JSON,跳过扫描。', ['dir' => $item]); + continue; + } + if (!isset($json_meta['extra']['zm-plugin-version'], $json_meta['name'])) { + logger()->notice('插件目录 {dir} 的插件元信息未提供版本和名称,不是有效的插件,跳过扫描。', ['dir' => $item]); continue; } // 构造一个元信息对象 - $meta = new PluginMeta($json_meta, ZM_PLUGIN_TYPE_SOURCE, $item); + $meta = new PluginMeta( + name: $json_meta['name'], + version: $json_meta['extra']['zm-plugin-version'], + description: $json_meta['description'] ?? '', + plugin_type: ZM_PLUGIN_TYPE_SOURCE, + root_dir: $item + ); if ($meta->getEntryFile() === null && $meta->getAutoloadFile() === null) { logger()->notice('插件 ' . $item . ' 不存在入口文件,也没有自动加载文件和内建 Composer,跳过加载'); continue; @@ -93,18 +103,29 @@ class PluginManager // 加载这个 Phar 文件 $phar = require $phar_path; // 读取元信息 - $plugin_file_path = zm_dir('phar://' . $phar_path . '/zmplugin.json'); + $plugin_file_path = zm_dir('phar://' . $phar_path . '/composer.json'); if (!file_exists($plugin_file_path)) { - throw new PluginException('插件元信息 zmplugin.json 文件不存在'); + throw new PluginException('插件元信息 composer.json 文件不存在'); } // 解析元信息的 JSON - $meta_json = json_decode(file_get_contents($plugin_file_path), true); + $json_meta = json_decode(file_get_contents($plugin_file_path), true); // 失败抛出异常 - if (!is_array($meta_json)) { + if (!is_array($json_meta)) { throw new PluginException('插件信息文件解析失败'); } + // 解析 name 和版本失败 + if (!isset($json_meta['extra']['zm-plugin-version'], $json_meta['name'])) { + throw new PluginException('插件文件 ' . $phar_path . ' 的插件元信息未提供版本和名称,不是有效的插件'); + } // $phar 这时应该是一个 ZMPlugin 对象,写入元信息 - $meta = new PluginMeta($meta_json, ZM_PLUGIN_TYPE_PHAR, zm_dir('phar://' . $phar_path)); + // 构造一个元信息对象 + $meta = new PluginMeta( + name: $json_meta['name'], + version: $json_meta['extra']['zm-plugin-version'], + description: $json_meta['description'] ?? '', + plugin_type: ZM_PLUGIN_TYPE_PHAR, + root_dir: zm_dir('phar://' . $phar_path) + ); // 如果已经返回了一个插件对象,那么直接塞进去实体 if ($phar instanceof ZMPlugin) { $meta->bindEntity($phar); @@ -144,7 +165,7 @@ class PluginManager $cnt = 0; foreach ($json['packages'] as $item) { $root_dir = $vendor_dir . '/' . $item['name']; - $meta_file = zm_dir($root_dir . '/zmplugin.json'); + $meta_file = zm_dir($root_dir . '/composer.json'); if (!file_exists($meta_file)) { continue; } @@ -152,12 +173,22 @@ class PluginManager // 检验元信息是否合法,不合法发个 notice 然后跳过 $json_meta = json_decode(file_get_contents($meta_file), true); if (!is_array($json_meta)) { - logger()->notice('插件目录 {dir} 的插件元信息(zmplugin.json)不是有效的 JSON,跳过扫描。', ['dir' => $item]); + logger()->notice('插件目录 {dir} 的插件元信息(composer.json)不是有效的 JSON,跳过扫描。', ['dir' => $item]); + continue; + } + // 解析 name 和版本失败 + if (!isset($json_meta['extra']['zm-plugin-version'], $json_meta['name'])) { continue; } // 构造一个元信息对象 - $meta = new PluginMeta($json_meta, ZM_PLUGIN_TYPE_COMPOSER, zm_dir($root_dir)); + $meta = new PluginMeta( + name: $json_meta['name'], + version: $json_meta['extra']['zm-plugin-version'], + description: $json_meta['description'] ?? '', + plugin_type: ZM_PLUGIN_TYPE_COMPOSER, + root_dir: $root_dir + ); if ($meta->getEntryFile() === null && $meta->getAutoloadFile() === null) { logger()->notice('插件 ' . $item . ' 不存在入口文件,也没有自动加载文件和内建 Composer,跳过加载'); continue; @@ -223,6 +254,7 @@ class PluginManager if ($meta->getPluginType() !== ZM_PLUGIN_TYPE_NATIVE) { logger()->info('正在启用插件 ' . $name); } + /* 插件从 zmplugin.json 改为 composer 了,所以不需要自己判断依赖 // 先判断依赖关系,如果声明了依赖,但依赖不合规则报错崩溃 foreach ($meta->getDependencies() as $dep_name => $dep_version) { // 缺少依赖的插件,不行 @@ -233,7 +265,7 @@ class PluginManager if (VersionComparator::compareVersionRange(self::$plugins[$dep_name]->getVersion(), $dep_version) === false) { throw new PluginException('插件 ' . $name . ' 依赖插件 ' . $dep_name . ',但是这个插件的版本不符合要求'); } - } + }*/ // 如果插件为单文件形式,且设置了 pluginLoad 事件,那就调用 $meta->getEntity()?->emitPluginLoad($parser); if (($entity = $meta->getEntity()) instanceof ZMPlugin) { diff --git a/src/ZM/Plugin/PluginMeta.php b/src/ZM/Plugin/PluginMeta.php index 1fe7d241..ce88e95c 100644 --- a/src/ZM/Plugin/PluginMeta.php +++ b/src/ZM/Plugin/PluginMeta.php @@ -20,41 +20,22 @@ class PluginMeta implements \JsonSerializable /** @var string 插件描述 */ private string $description; - /** @var array 插件的依赖列表 */ - private array $dependencies; - /** @var null|string 插件的根目录 */ private ?string $root_dir; /** @var int 插件类型 */ private int $plugin_type; - /** @var array 元信息原文 */ - private array $metas; - private bool $enabled = true; private ?ZMPlugin $entity = null; - /** - * @param array $meta 元信息数组格式 - * @param int $plugin_type 插件类型 - * @param null|string $root_dir 插件根目录 - */ - public function __construct(array $meta, int $plugin_type = ZM_PLUGIN_TYPE_NATIVE, ?string $root_dir = null) + public function __construct(string $name, string $version = '1.0-dev', string $description = '', int $plugin_type = ZM_PLUGIN_TYPE_NATIVE, ?string $root_dir = null) { - // 设置名称 - $this->name = $meta['name'] ?? ''; - // 设置版本 - $this->version = $meta['version'] ?? '1.0-dev'; - // 设置描述 - $this->description = $meta['description'] ?? ''; - // 设置依赖 - $this->dependencies = $meta['dependencies'] ?? []; - $this->metas = $meta; - // 设置插件根目录 + $this->name = $name; + $this->version = $version; + $this->description = $description; $this->plugin_type = $plugin_type; - // 设置插件根目录 $this->root_dir = $root_dir; } @@ -148,11 +129,6 @@ class PluginMeta implements \JsonSerializable return $this->description; } - public function getDependencies(): array - { - return $this->dependencies; - } - public function getRootDir(): string { return $this->root_dir; @@ -174,15 +150,9 @@ class PluginMeta implements \JsonSerializable 'name' => $this->name, 'version' => $this->version, 'description' => $this->description, - 'dependencies' => $this->dependencies, ]; } - public function getMetas(): array - { - return $this->metas; - } - public function getEntity(): ?ZMPlugin { return $this->entity; diff --git a/src/ZM/Plugin/Strategy/ComposerStrategy.php b/src/ZM/Plugin/Strategy/ComposerStrategy.php new file mode 100644 index 00000000..b59721f2 --- /dev/null +++ b/src/ZM/Plugin/Strategy/ComposerStrategy.php @@ -0,0 +1,15 @@ +error = 'Not implemented'; + return false; + } +} diff --git a/src/ZM/Plugin/Strategy/GitStrategy.php b/src/ZM/Plugin/Strategy/GitStrategy.php new file mode 100644 index 00000000..cca5d949 --- /dev/null +++ b/src/ZM/Plugin/Strategy/GitStrategy.php @@ -0,0 +1,149 @@ +git_api_link = $option['git-api-link']; + } + if (isset($option['github-token'])) { + $this->token = $option['github-token']; + } + + $git_url = parse_url($this->input); + + // GitHub 做特殊处理,直接调用 API 检查 + $is_github = false; + $plugin_name = null; + if ($git_url['host'] === 'github.com' && ($option['github-skip-check'] ?? false) !== true) { + if (!$this->checkGitAPI($git_url, $plugin_name)) { + return false; + } + $is_github = true; + } + + // 使用 Composer 管理插件,将仓库链接绑定到 composer.json + $this->logger->info('正在使用 Composer 下载并安装插件'); + $composer = ZMUtil::getComposerMetadata($this->root_composer_path); + $origin_composer = $composer; + $already_has_repo = false; + // 不破坏原有队列,加入 GitHub 的 repo + if (!isset($composer['repositories'])) { + $composer['repositories'] = []; + } + if (is_assoc_array($composer['repositories'])) { + $composer['repositories'] = [$composer['repositories']]; + } + foreach ($composer['repositories'] as $v) { + if (($v['url'] ?? '') === $this->input) { + $already_has_repo = true; + break; + } + } + if (!$already_has_repo) { + $composer['repositories'][] = [ + 'type' => $is_github ? 'github' : 'git', + 'url' => $this->input, + '.belongs' => $plugin_name, + ]; + } + // 写入 composer.json + if (ZMUtil::putComposerMetadata($this->root_composer_path, $composer) === false) { + $this->error = '写入 composer.json 失败'; + return false; + } + $env = getenv('COMPOSER_EXECUTABLE'); + if ($env === false) { + $env = 'composer'; + } + if ($plugin_name === null) { + $this->error = '没有从 Git 获取到插件的元信息,目前无法从 GitHub 以外的 Git 仓库下载插件,后续会更新!'; + ZMUtil::putComposerMetadata($this->root_composer_path, $origin_composer); + return false; + } + if ($plugin_name === '') { + $this->error = '获取插件名称失败!'; + ZMUtil::putComposerMetadata($this->root_composer_path, $origin_composer); + return false; + } + if (function_exists('pcntl_signal')) { + pcntl_signal(SIGINT, function () use ($origin_composer) { + echo "强行中断,恢复 Composer 中\n"; + ZMUtil::putComposerMetadata($this->root_composer_path, $origin_composer); + }); + } + passthru("{$env} require {$plugin_name}", $code); + if (function_exists('pcntl_signal')) { + pcntl_signal(SIGINT, SIG_IGN); + } + if ($code !== 0) { + $this->error = '使用 composer 引入 Git 插件出现了一些错误,请看上方错误'; + ZMUtil::putComposerMetadata($this->root_composer_path, $origin_composer); + return false; + } + return true; + } + + /** + * 用于调用 GitHub 的 API 用作不下载就检查插件是否合规 + * + * @param array $git_url 解析后的链接 + */ + private function checkGitAPI(array $git_url, ?string &$plugin_name = null): bool + { + $this->logger->info('正在检查 GitHub 插件是否为框架插件'); + [, $owner, $repo] = explode('/', $git_url['path']); + if (str_ends_with($repo, '.git')) { + $repo = substr($repo, 0, -4); + } + + // 调用 HTTP 客户端获取 API 信息 + $header = ['User-Agent' => 'zhamao-framework']; + if ($this->token !== '') { + $header['Authorization'] = 'token ' . $this->token; + } + $api = ZMRequest::get( + str_replace(['{owner}', '{repo}'], [$owner, $repo], $this->git_api_link), + $header, + only_body: false + ); + if ($api->getStatusCode() !== 200) { + $this->error = "GitHub API 请求失败[{$api->getStatusCode()}]"; + if ($api->getStatusCode() === 403) { + $this->error .= '可能是 API 滥用导致的,建议生成一个 GitHub Token。'; + } + return false; + } + + // 检查插件的 composer.json 是否合规 + $content = json_decode($api->getBody()->getContents(), true); + if (isset($content['message'])) { + $this->error = '该 GitHub 仓库中不存在 composer.json 文件!'; + return false; + } + $contents = implode('', array_map(fn ($x) => base64_decode($x), explode("\n", $content['content']))); + $json = json_decode($contents, true); + if (!$this->checkComposerIntegrity($json)) { + return false; + } + $plugin_name = $json['name']; + $this->installed_name = $plugin_name; + return true; + } +} diff --git a/src/ZM/Plugin/Strategy/PluginInstallStrategy.php b/src/ZM/Plugin/Strategy/PluginInstallStrategy.php new file mode 100644 index 00000000..edc23a92 --- /dev/null +++ b/src/ZM/Plugin/Strategy/PluginInstallStrategy.php @@ -0,0 +1,67 @@ +root_composer_path === '') { + $this->root_composer_path = zm_dir(WORKING_DIR); + } + if ($this->logger === null) { + $this->logger = ob_logger(); + } + } + + abstract public function install(array $option = []): bool; + + public function getError(): string + { + return $this->error; + } + + public function getInstalledName(): string + { + return $this->installed_name; + } + + /** + * 用于检查 Composer 文件的信息是否完整 + */ + protected function checkComposerIntegrity(mixed $composer): bool + { + // 必须是 array + if (!is_array($composer)) { + $this->error = 'composer.json 元信息获取出错'; + return false; + } + if (!isset($composer['extra']['zm-plugin-version'])) { + $this->error = 'composer.json 内没有标明该炸毛插件的版本,或该仓库不是炸毛插件'; + return false; + } + if (!isset($composer['name'])) { + $this->error = 'composer.json 插件元信息内没有名字!'; + return false; + } + $plugin_name = $composer['name']; + if (PluginManager::isPluginExists($plugin_name)) { + $this->error = "插件 {$plugin_name} 已存在,无法再次安装"; + return false; + } + return true; + } +} diff --git a/src/ZM/ZMApplication.php b/src/ZM/ZMApplication.php index 15f5c6a3..a1ed6bba 100644 --- a/src/ZM/ZMApplication.php +++ b/src/ZM/ZMApplication.php @@ -50,7 +50,7 @@ class ZMApplication extends ZMPlugin */ public function run() { - $meta = new PluginMeta(['name' => 'native'], ZM_PLUGIN_TYPE_NATIVE); + $meta = new PluginMeta(name: 'native', plugin_type: ZM_PLUGIN_TYPE_NATIVE); $meta->bindEntity($this); PluginManager::addPlugin($meta); (new Framework($this->args))->init()->start();