setModuleUid($uid); } public function __construct(string $uid) { $this->installDir = root_path() . 'modules' . DIRECTORY_SEPARATOR; $this->backupsDir = $this->installDir . 'backups' . DIRECTORY_SEPARATOR; if (!is_dir($this->installDir)) { mkdir($this->installDir, 0755, true); } if (!is_dir($this->backupsDir)) { mkdir($this->backupsDir, 0755, true); } if ($uid) { $this->setModuleUid($uid); } else { $this->uid = ''; $this->modulesDir = $this->installDir; } } public function getInstallState(): int { if (!is_dir($this->modulesDir)) { return self::UNINSTALLED; } $info = $this->getInfo(); if ($info && isset($info['state'])) { return $info['state']; } return Filesystem::dirIsEmpty($this->modulesDir) ? self::UNINSTALLED : self::DIRECTORY_OCCUPIED; } /** * 从 Webman Request 上传安装(适配 Multipart 上传) * @return array 模块基本信息 * @throws Throwable */ public static function uploadFromRequest(Request $request): array { $file = $request->file('file'); if (!$file) { throw new Exception('Parameter error'); } $token = $request->post('token', $request->get('token', '')); if (!$token) { throw new Exception('Please login to the official website account first'); } $uploadDir = root_path() . 'public' . DIRECTORY_SEPARATOR . 'storage' . DIRECTORY_SEPARATOR . 'upload' . DIRECTORY_SEPARATOR; if (!is_dir($uploadDir)) { mkdir($uploadDir, 0755, true); } $saveName = 'temp' . DIRECTORY_SEPARATOR . date('YmdHis') . '_' . ($file->getUploadName() ?? 'module.zip'); $savePath = $uploadDir . $saveName; $saveDir = dirname($savePath); if (!is_dir($saveDir)) { mkdir($saveDir, 0755, true); } $file->move($savePath); $relativePath = 'storage/upload/' . str_replace(DIRECTORY_SEPARATOR, '/', $saveName); try { return self::instance('')->doUpload($token, $relativePath); } finally { if (is_file($savePath)) { @unlink($savePath); } } } /** * 下载模块文件 * @throws Throwable */ public function download(): string { $req = function_exists('request') ? request() : null; $token = $req ? ($req->post('token', $req->get('token', ''))) : ''; $version = $req ? ($req->post('version', $req->get('version', ''))) : ''; $orderId = $req ? ($req->post('orderId', $req->get('orderId', 0))) : 0; if (!$orderId) { throw new Exception('Order not found'); } $zipFile = Server::download($this->uid, $this->installDir, [ 'version' => $version, 'orderId' => $orderId, 'nuxtVersion' => Server::getNuxtVersion(), 'sysVersion' => config('buildadmin.version', ''), 'installed' => Server::getInstalledIds($this->installDir), 'ba-user-token' => $token, ]); Filesystem::delDir($this->modulesDir); Filesystem::unzip($zipFile); @unlink($zipFile); $this->checkPackage(); $this->setInfo([ 'state' => self::WAIT_INSTALL, ]); return $zipFile; } /** * 上传安装(token + 文件相对路径) * @throws Throwable */ public function doUpload(string $token, string $file): array { $file = Filesystem::fsFit(root_path() . 'public' . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $file)); if (!is_file($file)) { throw new Exception('Zip file not found'); } $copyTo = $this->installDir . 'uploadTemp' . date('YmdHis') . '.zip'; copy($file, $copyTo); $copyToDir = Filesystem::unzip($copyTo); $copyToDir .= DIRECTORY_SEPARATOR; @unlink($file); @unlink($copyTo); $info = Server::getIni($copyToDir); if (empty($info['uid'])) { Filesystem::delDir($copyToDir); throw new Exception('Basic configuration of the Module is incomplete'); } $this->setModuleUid($info['uid']); $upgrade = false; if (is_dir($this->modulesDir)) { $oldInfo = $this->getInfo(); if ($oldInfo && !empty($oldInfo['uid'])) { $versions = explode('.', $oldInfo['version'] ?? '0.0.0'); if (isset($versions[2])) { $versions[2]++; } $nextVersion = implode('.', $versions); $upgrade = Version::compare($nextVersion, $info['version'] ?? ''); if ($upgrade) { if (!in_array($oldInfo['state'], [self::UNINSTALLED, self::WAIT_INSTALL, self::DISABLE])) { Filesystem::delDir($copyToDir); throw new Exception('Please disable the module before updating'); } } else { Filesystem::delDir($copyToDir); throw new Exception('Module already exists'); } } if (!Filesystem::dirIsEmpty($this->modulesDir) && !$upgrade) { Filesystem::delDir($copyToDir); throw new Exception('The directory required by the module is occupied'); } } try { Server::installPreCheck([ 'uid' => $info['uid'], 'version' => $info['version'] ?? '', 'sysVersion' => config('buildadmin.version', ''), 'nuxtVersion' => Server::getNuxtVersion(), 'moduleVersion' => $info['version'] ?? '', 'ba-user-token' => $token, 'installed' => Server::getInstalledIds($this->installDir), 'server' => 1, ]); } catch (Throwable $e) { Filesystem::delDir($copyToDir); throw $e; } $newInfo = ['state' => self::WAIT_INSTALL]; if ($upgrade) { $info['update'] = 1; Filesystem::delDir($this->modulesDir); } rename($copyToDir, $this->modulesDir); $this->checkPackage(); $this->setInfo($newInfo); return $info; } /** * 安装模块 * @throws Throwable */ public function install(bool $update): array { $state = $this->getInstallState(); if ($update) { if (!in_array($state, [self::UNINSTALLED, self::WAIT_INSTALL, self::DISABLE])) { throw new Exception('Please disable the module before updating'); } if ($state == self::UNINSTALLED || $state != self::WAIT_INSTALL) { $this->download(); } } else { if ($state == self::INSTALLED || $state == self::DIRECTORY_OCCUPIED || $state == self::DISABLE) { throw new Exception('Module already exists'); } if ($state == self::UNINSTALLED) { $this->download(); } } Server::importSql($this->modulesDir); $info = $this->getInfo(); if ($update) { $info['update'] = 1; Server::execEvent($this->uid, 'update'); } $req = function_exists('request') ? request() : null; $extend = $req ? ($req->post('extend') ?? []) : []; if (!isset($extend['conflictHandle'])) { Server::execEvent($this->uid, 'install'); } $this->enable('install'); return $info; } /** * 卸载 * @throws Throwable */ public function uninstall(): void { $info = $this->getInfo(); if (($info['state'] ?? 0) != self::DISABLE) { throw new Exception('Please disable the module first', 0, [ 'uid' => $this->uid, ]); } Server::execEvent($this->uid, 'uninstall'); Filesystem::delDir($this->modulesDir); } /** * 修改模块状态 * @throws Throwable */ public function changeState(bool $state): array { $info = $this->getInfo(); if (!$state) { $canDisable = [ self::INSTALLED, self::CONFLICT_PENDING, self::DEPENDENT_WAIT_INSTALL, ]; if (!in_array($info['state'] ?? 0, $canDisable)) { throw new Exception('The current state of the module cannot be set to disabled', 0, [ 'uid' => $this->uid, 'state' => $info['state'] ?? 0, ]); } return $this->disable(); } if (($info['state'] ?? 0) != self::DISABLE) { throw new Exception('The current state of the module cannot be set to enabled', 0, [ 'uid' => $this->uid, 'state' => $info['state'] ?? 0, ]); } $this->setInfo([ 'state' => self::WAIT_INSTALL, ]); return $info; } /** * 启用 * @throws Throwable */ public function enable(string $trigger): void { Server::installWebBootstrap($this->uid, $this->modulesDir); Server::createRuntime($this->modulesDir); $this->conflictHandle($trigger); Server::execEvent($this->uid, 'enable'); $this->dependUpdateHandle(); } /** * 禁用 * @throws Throwable */ public function disable(): array { $req = function_exists('request') ? request() : null; $update = $req ? filter_var($req->post('update', false), FILTER_VALIDATE_BOOLEAN) : false; $confirmConflict = $req ? filter_var($req->post('confirmConflict', false), FILTER_VALIDATE_BOOLEAN) : false; $dependConflictSolution = $req ? ($req->post('dependConflictSolution') ?? []) : []; $info = $this->getInfo(); $zipFile = $this->backupsDir . $this->uid . '-install.zip'; $zipDir = false; if (is_file($zipFile)) { try { $zipDir = $this->backupsDir . $this->uid . '-install' . DIRECTORY_SEPARATOR; Filesystem::unzip($zipFile, $zipDir); } catch (Exception) { // skip } } $conflictFile = Server::getFileList($this->modulesDir, true); $dependConflict = $this->disableDependCheck(); if (($conflictFile || !self::isEmptyArray($dependConflict)) && !$confirmConflict) { $dependConflictTemp = []; foreach ($dependConflict as $env => $item) { foreach ($item as $depend => $v) { $dependConflictTemp[] = [ 'env' => $env, 'depend' => $depend, 'dependTitle' => $depend . ' ' . $v, 'solution' => 'delete', ]; } } throw new Exception('Module file updated', -1, [ 'uid' => $this->uid, 'conflictFile' => $conflictFile, 'dependConflict' => $dependConflictTemp, ]); } Server::execEvent($this->uid, 'disable', ['update' => $update]); $delNpmDepend = false; $delNuxtNpmDepend = false; $delComposerDepend = false; foreach ($dependConflictSolution as $env => $depends) { if (!$depends) continue; if ($env == 'require' || $env == 'require-dev') { $delComposerDepend = true; } elseif ($env == 'dependencies' || $env == 'devDependencies') { $delNpmDepend = true; } elseif ($env == 'nuxtDependencies' || $env == 'nuxtDevDependencies') { $delNuxtNpmDepend = true; } } $dependJsonFiles = [ 'composer' => 'composer.json', 'webPackage' => 'web' . DIRECTORY_SEPARATOR . 'package.json', 'webNuxtPackage' => 'web-nuxt' . DIRECTORY_SEPARATOR . 'package.json', ]; $dependWaitInstall = []; if ($delComposerDepend) { $conflictFile[] = $dependJsonFiles['composer']; $dependWaitInstall[] = [ 'pm' => false, 'command' => 'composer.update', 'type' => 'composer_dependent_wait_install', ]; } if ($delNpmDepend) { $conflictFile[] = $dependJsonFiles['webPackage']; $dependWaitInstall[] = [ 'pm' => true, 'command' => 'web-install', 'type' => 'npm_dependent_wait_install', ]; } if ($delNuxtNpmDepend) { $conflictFile[] = $dependJsonFiles['webNuxtPackage']; $dependWaitInstall[] = [ 'pm' => true, 'command' => 'nuxt-install', 'type' => 'nuxt_npm_dependent_wait_install', ]; } if ($conflictFile) { $overwriteDir = Server::getOverwriteDir(); foreach ($conflictFile as $key => $item) { $paths = explode(DIRECTORY_SEPARATOR, $item); if (in_array($paths[0], $overwriteDir) || in_array($item, $dependJsonFiles)) { $conflictFile[$key] = $item; } else { $conflictFile[$key] = Filesystem::fsFit(str_replace(root_path(), '', $this->modulesDir . $item)); } if (!is_file(root_path() . $conflictFile[$key])) { unset($conflictFile[$key]); } } $backupsZip = $this->backupsDir . $this->uid . '-disable-' . date('YmdHis') . '.zip'; Filesystem::zip($conflictFile, $backupsZip); } $serverDepend = new Depends(root_path() . 'composer.json', 'composer'); $webDep = new Depends(root_path() . 'web' . DIRECTORY_SEPARATOR . 'package.json'); $webNuxtDep = new Depends(root_path() . 'web-nuxt' . DIRECTORY_SEPARATOR . 'package.json'); foreach ($dependConflictSolution as $env => $depends) { if (!$depends) continue; $dev = stripos($env, 'dev') !== false; if ($env == 'require' || $env == 'require-dev') { $serverDepend->removeDepends($depends, $dev); } elseif ($env == 'dependencies' || $env == 'devDependencies') { $webDep->removeDepends($depends, $dev); } elseif ($env == 'nuxtDependencies' || $env == 'nuxtDevDependencies') { $webNuxtDep->removeDepends($depends, $dev); } } $composerConfig = Server::getConfig($this->modulesDir, 'composerConfig'); if ($composerConfig) { $serverDepend->removeComposerConfig($composerConfig); } $protectedFiles = Server::getConfig($this->modulesDir, 'protectedFiles'); foreach ($protectedFiles as &$protectedFile) { $protectedFile = Filesystem::fsFit(root_path() . $protectedFile); } $moduleFile = Server::getFileList($this->modulesDir); foreach ($moduleFile as &$file) { $moduleFilePath = Filesystem::fsFit($this->modulesDir . $file); $file = Filesystem::fsFit(root_path() . $file); if (!file_exists($file)) continue; if (!file_exists($moduleFilePath)) { if (!is_dir(dirname($moduleFilePath))) { mkdir(dirname($moduleFilePath), 0755, true); } copy($file, $moduleFilePath); } if (in_array($file, $protectedFiles)) { continue; } if (file_exists($file)) { unlink($file); } Filesystem::delEmptyDir(dirname($file)); } if ($zipDir) { $unrecoverableFiles = [ Filesystem::fsFit(root_path() . 'composer.json'), Filesystem::fsFit(root_path() . 'web/package.json'), Filesystem::fsFit(root_path() . 'web-nuxt/package.json'), ]; foreach ( new RecursiveIteratorIterator( new RecursiveDirectoryIterator($zipDir, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::SELF_FIRST ) as $item ) { $backupsFile = Filesystem::fsFit(root_path() . str_replace($zipDir, '', $item->getPathname())); if (in_array($backupsFile, $moduleFile) && !in_array($backupsFile, $protectedFiles)) { continue; } if ($item->isDir()) { if (!is_dir($backupsFile)) { mkdir($backupsFile, 0755, true); } } elseif (!in_array($backupsFile, $unrecoverableFiles)) { copy($item->getPathname(), $backupsFile); } } } if ($zipDir && is_dir($zipDir)) { Filesystem::delDir($zipDir); } Server::uninstallWebBootstrap($this->uid); $this->setInfo([ 'state' => self::DISABLE, ]); if ($update) { throw new Exception('update', -3, [ 'uid' => $this->uid, ]); } if (!empty($dependWaitInstall)) { throw new Exception('dependent wait install', -2, [ 'uid' => $this->uid, 'wait_install' => $dependWaitInstall, ]); } return $info; } /** * 处理依赖和文件冲突 * @throws Throwable */ public function conflictHandle(string $trigger): bool { $info = $this->getInfo(); if (!in_array($info['state'] ?? 0, [self::WAIT_INSTALL, self::CONFLICT_PENDING])) { return false; } $fileConflict = Server::getFileList($this->modulesDir, true); $dependConflict = Server::dependConflictCheck($this->modulesDir); $installFiles = Server::getFileList($this->modulesDir); $depends = Server::getDepend($this->modulesDir); $coverFiles = []; $discardFiles = []; $serverDep = new Depends(root_path() . 'composer.json', 'composer'); $webDep = new Depends(root_path() . 'web' . DIRECTORY_SEPARATOR . 'package.json'); $webNuxtDep = new Depends(root_path() . 'web-nuxt' . DIRECTORY_SEPARATOR . 'package.json'); $req = function_exists('request') ? request() : null; $extend = $req ? ($req->post('extend') ?? []) : []; if ($fileConflict || !self::isEmptyArray($dependConflict)) { if (!$extend) { $fileConflictTemp = []; foreach ($fileConflict as $key => $item) { $fileConflictTemp[$key] = [ 'newFile' => $this->uid . DIRECTORY_SEPARATOR . $item, 'oldFile' => $item, 'solution' => 'cover', ]; } $dependConflictTemp = []; foreach ($dependConflict as $env => $item) { $dev = stripos($env, 'dev') !== false; foreach ($item as $depend => $v) { $oldDepend = ''; if (in_array($env, ['require', 'require-dev'])) { $oldDepend = $depend . ' ' . $serverDep->hasDepend($depend, $dev); } elseif (in_array($env, ['dependencies', 'devDependencies'])) { $oldDepend = $depend . ' ' . $webDep->hasDepend($depend, $dev); } elseif (in_array($env, ['nuxtDependencies', 'nuxtDevDependencies'])) { $oldDepend = $depend . ' ' . $webNuxtDep->hasDepend($depend, $dev); } $dependConflictTemp[] = [ 'env' => $env, 'newDepend' => $depend . ' ' . $v, 'oldDepend' => $oldDepend, 'depend' => $depend, 'solution' => 'cover', ]; } } $this->setInfo([ 'state' => self::CONFLICT_PENDING, ]); throw new Exception('Module file conflicts', -1, [ 'fileConflict' => $fileConflictTemp, 'dependConflict' => $dependConflictTemp, 'uid' => $this->uid, 'state' => self::CONFLICT_PENDING, ]); } if ($fileConflict && isset($extend['fileConflict'])) { foreach ($installFiles as $ikey => $installFile) { if (isset($extend['fileConflict'][$installFile])) { if ($extend['fileConflict'][$installFile] == 'discard') { $discardFiles[] = $installFile; unset($installFiles[$ikey]); } else { $coverFiles[] = $installFile; } } } } if (!self::isEmptyArray($dependConflict) && isset($extend['dependConflict'])) { foreach ($depends as $fKey => $fItem) { foreach ($fItem as $cKey => $cItem) { if (isset($extend['dependConflict'][$fKey][$cKey])) { if ($extend['dependConflict'][$fKey][$cKey] == 'discard') { unset($depends[$fKey][$cKey]); } } } } } } if ($depends) { foreach ($depends as $key => $item) { if (!$item) continue; if ($key == 'require' || $key == 'require-dev') { $coverFiles[] = 'composer.json'; continue; } if ($key == 'dependencies' || $key == 'devDependencies') { $coverFiles[] = 'web' . DIRECTORY_SEPARATOR . 'package.json'; } if ($key == 'nuxtDependencies' || $key == 'nuxtDevDependencies') { $coverFiles[] = 'web-nuxt' . DIRECTORY_SEPARATOR . 'package.json'; } } } if ($coverFiles) { $backupsZip = $trigger == 'install' ? $this->backupsDir . $this->uid . '-install.zip' : $this->backupsDir . $this->uid . '-cover-' . date('YmdHis') . '.zip'; Filesystem::zip($coverFiles, $backupsZip); } if ($depends) { $npm = false; $composer = false; $nuxtNpm = false; $composerConfig = Server::getConfig($this->modulesDir, 'composerConfig'); if ($composerConfig) { $serverDep->setComposerConfig($composerConfig); } foreach ($depends as $key => $item) { if (!$item) continue; if ($key == 'require') { $composer = true; $serverDep->addDepends($item, false, true); } elseif ($key == 'require-dev') { $composer = true; $serverDep->addDepends($item, true, true); } elseif ($key == 'dependencies') { $npm = true; $webDep->addDepends($item, false, true); } elseif ($key == 'devDependencies') { $npm = true; $webDep->addDepends($item, true, true); } elseif ($key == 'nuxtDependencies') { $nuxtNpm = true; $webNuxtDep->addDepends($item, false, true); } elseif ($key == 'nuxtDevDependencies') { $nuxtNpm = true; $webNuxtDep->addDepends($item, true, true); } } if ($npm) { $info['npm_dependent_wait_install'] = 1; $info['state'] = self::DEPENDENT_WAIT_INSTALL; } if ($composer) { $info['composer_dependent_wait_install'] = 1; $info['state'] = self::DEPENDENT_WAIT_INSTALL; } if ($nuxtNpm) { $info['nuxt_npm_dependent_wait_install'] = 1; $info['state'] = self::DEPENDENT_WAIT_INSTALL; } $info = $info ?? $this->getInfo(); if (($info['state'] ?? 0) != self::DEPENDENT_WAIT_INSTALL) { $this->setInfo(['state' => self::INSTALLED]); } else { $this->setInfo([], $info); } } else { $this->setInfo(['state' => self::INSTALLED]); } $overwriteDir = Server::getOverwriteDir(); foreach ($overwriteDir as $dirItem) { $baseDir = $this->modulesDir . $dirItem; $destDir = root_path() . $dirItem; if (!is_dir($baseDir)) continue; foreach ( new RecursiveIteratorIterator( new RecursiveDirectoryIterator($baseDir, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::SELF_FIRST ) as $item ) { $destDirItem = Filesystem::fsFit($destDir . DIRECTORY_SEPARATOR . str_replace($baseDir, '', $item->getPathname())); if ($item->isDir()) { Filesystem::mkdir($destDirItem); } elseif (!in_array(str_replace(root_path(), '', $destDirItem), $discardFiles)) { Filesystem::mkdir(dirname($destDirItem)); copy($item->getPathname(), $destDirItem); } } if (config('buildadmin.module_pure_install', false)) { Filesystem::delDir($baseDir); } } return true; } /** * 依赖升级处理 * @throws Throwable */ public function dependUpdateHandle(): void { $info = $this->getInfo(); if (($info['state'] ?? 0) == self::DEPENDENT_WAIT_INSTALL) { $waitInstall = []; if (isset($info['composer_dependent_wait_install'])) { $waitInstall[] = 'composer_dependent_wait_install'; } if (isset($info['npm_dependent_wait_install'])) { $waitInstall[] = 'npm_dependent_wait_install'; } if (isset($info['nuxt_npm_dependent_wait_install'])) { $waitInstall[] = 'nuxt_npm_dependent_wait_install'; } if ($waitInstall) { throw new Exception('dependent wait install', -2, [ 'uid' => $this->uid, 'state' => self::DEPENDENT_WAIT_INSTALL, 'wait_install' => $waitInstall, ]); } else { $this->setInfo(['state' => self::INSTALLED]); } } } /** * 依赖安装完成标记 * @throws Throwable */ public function dependentInstallComplete(string $type): void { $info = $this->getInfo(); if (($info['state'] ?? 0) == self::DEPENDENT_WAIT_INSTALL) { if ($type == 'npm') { unset($info['npm_dependent_wait_install']); } if ($type == 'nuxt_npm') { unset($info['nuxt_npm_dependent_wait_install']); } if ($type == 'composer') { unset($info['composer_dependent_wait_install']); } if ($type == 'all') { unset($info['npm_dependent_wait_install'], $info['composer_dependent_wait_install'], $info['nuxt_npm_dependent_wait_install']); } if (!isset($info['npm_dependent_wait_install']) && !isset($info['composer_dependent_wait_install']) && !isset($info['nuxt_npm_dependent_wait_install'])) { $info['state'] = self::INSTALLED; } $this->setInfo([], $info); } } public function disableDependCheck(): array { $depend = Server::getDepend($this->modulesDir); if (!$depend) return []; $serverDep = new Depends(root_path() . 'composer.json', 'composer'); $webDep = new Depends(root_path() . 'web' . DIRECTORY_SEPARATOR . 'package.json'); $webNuxtDep = new Depends(root_path() . 'web-nuxt' . DIRECTORY_SEPARATOR . 'package.json'); foreach ($depend as $key => $depends) { $dev = stripos($key, 'dev') !== false; if ($key == 'require' || $key == 'require-dev') { foreach ($depends as $dependKey => $dependItem) { if (!$serverDep->hasDepend($dependKey, $dev)) { unset($depends[$dependKey]); } } $depend[$key] = $depends; } elseif ($key == 'dependencies' || $key == 'devDependencies') { foreach ($depends as $dependKey => $dependItem) { if (!$webDep->hasDepend($dependKey, $dev)) { unset($depends[$dependKey]); } } $depend[$key] = $depends; } elseif ($key == 'nuxtDependencies' || $key == 'nuxtDevDependencies') { foreach ($depends as $dependKey => $dependItem) { if (!$webNuxtDep->hasDepend($dependKey, $dev)) { unset($depends[$dependKey]); } } $depend[$key] = $depends; } } return $depend; } /** * 检查包是否完整 * @throws Throwable */ public function checkPackage(): bool { if (!is_dir($this->modulesDir)) { throw new Exception('Module package file does not exist'); } $info = $this->getInfo(); $infoKeys = ['uid', 'title', 'intro', 'author', 'version', 'state']; foreach ($infoKeys as $value) { if (!array_key_exists($value, $info)) { Filesystem::delDir($this->modulesDir); throw new Exception('Basic configuration of the Module is incomplete'); } } return true; } public function getInfo(): array { return Server::getIni($this->modulesDir); } /** * @throws Throwable */ public function setInfo(array $kv = [], array $arr = []): bool { if ($kv) { $info = $this->getInfo(); foreach ($kv as $k => $v) { $info[$k] = $v; } return Server::setIni($this->modulesDir, $info); } if ($arr) { return Server::setIni($this->modulesDir, $arr); } throw new Exception('Parameter error'); } public static function isEmptyArray($arr): bool { foreach ($arr as $item) { if (is_array($item)) { if (!self::isEmptyArray($item)) return false; } elseif ($item) { return false; } } return true; } public function setModuleUid(string $uid): static { $this->uid = $uid; $this->modulesDir = $this->installDir . $uid . DIRECTORY_SEPARATOR; return $this; } }