/... → 部署到 /templates// * - static/... → 部署到 /public/static/templates// * * 模板不携带 DB 迁移,安装即文件落地;与插件(Addon)的 importsql 机制解耦。 */ namespace ywxapp\library; class TemplateInstaller { /** * 从 zip 部署模板到项目。 * * @param string $zipPath 模板包 zip 绝对路径 * @param string $rootDir 项目根目录(含 templates/ 与 public/) * @return array 部署后的清单 ['name','version','target_addon','tpl_dir','static_dir'] * @throws \RuntimeException */ public static function install(string $zipPath, string $rootDir): array { if (!is_file($zipPath)) { throw new \RuntimeException("模板包不存在: {$zipPath}"); } if (!class_exists('ZipArchive')) { throw new \RuntimeException('ZipArchive 扩展不可用'); } $zip = new ZipArchive(); if ($zip->open($zipPath) !== true) { throw new \RuntimeException("无法打开模板包: {$zipPath}"); } // 先探测模板名(templates// 前缀) $name = ''; for ($i = 0; $i < $zip->numFiles; $i++) { $entry = $zip->getNameIndex($i); if (preg_match('#^templates/([^/]+)/#', $entry, $m)) { $name = $m[1]; break; } } if ($name === '') { $zip->close(); throw new \RuntimeException('zip 中未找到 templates// 结构,可能不是合法模板包'); } $tplTarget = rtrim($rootDir, '/\\') . '/templates/' . $name . '/'; $staticTarget = rtrim($rootDir, '/\\') . '/public/static/templates/' . $name . '/'; for ($i = 0; $i < $zip->numFiles; $i++) { $entry = $zip->getNameIndex($i); if ($entry === '' || $entry === null) { continue; } $prefix = 'templates/' . $name . '/'; if (strpos($entry, $prefix) === 0) { self::writeEntry($zip, $entry, $tplTarget . substr($entry, strlen($prefix))); } elseif (strpos($entry, 'static/') === 0) { $rel = substr($entry, strlen('static/')); if ($rel === '') { continue; } self::writeEntry($zip, $entry, $staticTarget . $rel); } } $zip->close(); $metaFile = $tplTarget . 'template.json'; $meta = is_file($metaFile) ? (json_decode(file_get_contents($metaFile), true) ?? []) : []; return [ 'name' => $name, 'version' => $meta['version'] ?? '', 'title' => $meta['title'] ?? $name, 'target_addon' => $meta['target_addon'] ?? [], 'tpl_dir' => $tplTarget, 'static_dir' => $staticTarget, ]; } /** * 将 zip 内单个条目写出到目标路径(目录建目录,文件写内容)。 */ protected static function writeEntry(ZipArchive $zip, string $entry, string $target): void { if (substr($entry, -1) === '/') { if (!is_dir($target)) { @mkdir($target, 0755, true); } return; } $dir = dirname($target); if (!is_dir($dir)) { @mkdir($dir, 0755, true); } $content = $zip->getFromName($entry); if ($content === false) { return; } file_put_contents($target, $content); } }