ComposerGenerator.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. <?php
  2. declare(strict_types=1);
  3. namespace SixShop\MakerBundle\Generator;
  4. use Symfony\Component\Console\Style\SymfonyStyle;
  5. class ComposerGenerator
  6. {
  7. private string $templatePath;
  8. public function __construct()
  9. {
  10. $this->templatePath = __DIR__ . '/../../templates/composer.json.tpl.php';
  11. }
  12. /**
  13. * 获取用户输入的包信息
  14. */
  15. public function gatherPackageInfo(SymfonyStyle $io, ?string $targetPath = null): ?array
  16. {
  17. try {
  18. // 尝试读取现有的composer.json文件以获取默认值
  19. $existingInfo = $this->readExistingComposerInfo($targetPath, $io);
  20. // 如果找到了完整的现有信息,询问用户是否直接使用
  21. if ($this->hasCompleteExistingInfo($existingInfo)) {
  22. $io->section('发现现有扩展配置');
  23. $io->table(
  24. ['字段', '现有值'],
  25. [
  26. ['包名', $existingInfo['name'] ?? 'N/A'],
  27. ['描述', $existingInfo['description'] ?? 'N/A'],
  28. ['命名空间', $existingInfo['namespace'] ?? 'N/A'],
  29. ['扩展ID', $existingInfo['id'] ?? 'N/A']
  30. ]
  31. );
  32. if ($io->confirm('确定使用当前配置?', true)) {
  33. return [
  34. 'packageName' => $existingInfo['name'],
  35. 'description' => $existingInfo['description'],
  36. 'namespace' => $existingInfo['namespace'],
  37. 'id' => $existingInfo['id']
  38. ];
  39. }
  40. }
  41. // 获取包名
  42. $defaultPackageName = $existingInfo['name'] ?? null;
  43. $packageName = $io->ask(
  44. '请输入Composer包名 (例如: six-shop/hello 或sixdec/abc)',
  45. $defaultPackageName
  46. );
  47. if (empty($packageName)) {
  48. throw new \InvalidArgumentException('包名不能为空!');
  49. }
  50. // 验证包名格式
  51. if (!preg_match('/^[a-z0-9-_]+\/[a-z0-9-_]+$/', $packageName)) {
  52. throw new \InvalidArgumentException('包名格式不正确! 正确格式: six-shop/hello 或sixdec/abc');
  53. }
  54. // 获取包描述
  55. $defaultDescription = $existingInfo['description'] ?? 'A SixShop extension package';
  56. $description = $io->ask('请输入包描述 (可选)', $defaultDescription);
  57. // 获取命名空间
  58. $defaultNamespace = $existingInfo['namespace'] ?? $this->generateDefaultNamespace($packageName);
  59. // 用户界面显示单反斜杠格式,用户输入也是单反斜杠
  60. $userNamespace = $io->ask('请输入命名空间 (例如: SixShop\\Hello\\)', $defaultNamespace);
  61. // PHP代码内部使用单反斜杠存储
  62. $namespace = rtrim($userNamespace, '\\') . '\\';
  63. // 获取扩展ID
  64. $defaultId = $existingInfo['id'] ?? str_replace('/', '-', $packageName);
  65. $id = $io->ask('请输入扩展ID (例如: six-shop-hello)', $defaultId);
  66. return [
  67. 'packageName' => $packageName,
  68. 'description' => $description,
  69. 'namespace' => $namespace,
  70. 'id' => $id
  71. ];
  72. } catch (\InvalidArgumentException $e) {
  73. $io->error($e->getMessage());
  74. return null;
  75. }
  76. }
  77. /**
  78. * 生成默认命名空间
  79. */
  80. private function generateDefaultNamespace(string $packageName): string
  81. {
  82. $parts = explode('/', $packageName);
  83. $vendor = $this->convertToNamespace($parts[0]);
  84. $package = $this->convertToNamespace($parts[1]);
  85. return $vendor . '\\' . $package . '\\';
  86. }
  87. /**
  88. * 将字符串转换为命名空间格式
  89. */
  90. private function convertToNamespace(string $string): string
  91. {
  92. $parts = explode('-', $string);
  93. $parts = array_map('ucfirst', $parts);
  94. return implode('', $parts);
  95. }
  96. /**
  97. * 读取现有的composer.json文件以获取默认值
  98. */
  99. private function readExistingComposerInfo(?string $targetPath, SymfonyStyle $io): array
  100. {
  101. $existingInfo = [];
  102. if ($targetPath === null) {
  103. return $existingInfo;
  104. }
  105. if (is_dir($targetPath)) {
  106. // 首先检查目标目录下是否直接有composer.json文件
  107. $directComposerPath = $targetPath . '/composer.json';
  108. if (file_exists($directComposerPath)) {
  109. $existingInfo = $this->parseComposerFile($directComposerPath, $io);
  110. if (!empty($existingInfo)) {
  111. return $existingInfo;
  112. }
  113. }
  114. // 然后扫描目标目录下的所有子目录,查找composer.json文件
  115. $directories = array_filter(glob($targetPath . '/*'), 'is_dir');
  116. foreach ($directories as $dir) {
  117. $composerPath = $dir . '/composer.json';
  118. if (file_exists($composerPath)) {
  119. $existingInfo = $this->parseComposerFile($composerPath, $io);
  120. if (!empty($existingInfo)) {
  121. // 只取第一个找到的文件
  122. break;
  123. }
  124. }
  125. }
  126. }
  127. return $existingInfo;
  128. }
  129. /**
  130. * 解析composer.json文件并提取相关信息
  131. */
  132. private function parseComposerFile(string $composerPath, SymfonyStyle $io): array
  133. {
  134. $existingInfo = [];
  135. $composerContent = file_get_contents($composerPath);
  136. if ($composerContent !== false) {
  137. $composerData = json_decode($composerContent, true);
  138. if (json_last_error() === JSON_ERROR_NONE && is_array($composerData)) {
  139. $io->note('发现现有的composer.json文件: ' . $composerPath);
  140. // 提取相关信息
  141. $existingInfo['name'] = $composerData['name'] ?? null;
  142. $existingInfo['description'] = $composerData['description'] ?? null;
  143. // 从 autoload PSR-4 中提取命名空间
  144. if (isset($composerData['autoload']['psr-4'])) {
  145. $psr4 = $composerData['autoload']['psr-4'];
  146. $namespaces = array_keys($psr4);
  147. if (!empty($namespaces)) {
  148. // 取第一个命名空间,JSON已经解析为单反斜杠,保持PHP内部单反斜杠格式
  149. $existingInfo['namespace'] = rtrim($namespaces[0], '\\') . '\\';
  150. }
  151. }
  152. // 从 extra.sixshop 中提取ID
  153. if (isset($composerData['extra']['sixshop']['id'])) {
  154. $existingInfo['id'] = $composerData['extra']['sixshop']['id'];
  155. }
  156. }
  157. }
  158. return $existingInfo;
  159. }
  160. /**
  161. * 检查是否有完整的现有信息
  162. */
  163. private function hasCompleteExistingInfo(array $existingInfo): bool
  164. {
  165. return !empty($existingInfo['name']) &&
  166. !empty($existingInfo['description']) &&
  167. !empty($existingInfo['namespace']) &&
  168. !empty($existingInfo['id']);
  169. }
  170. /**
  171. * 生成composer.json内容
  172. */
  173. public function generateContent(string $packageName, string $namespace, string $id, string $description = 'A SixShop extension package'): string
  174. {
  175. if (!file_exists($this->templatePath)) {
  176. throw new \RuntimeException('模板文件不存在: ' . $this->templatePath);
  177. }
  178. // 准备模板变量
  179. $name = $packageName;
  180. // 生成composer.json内容
  181. ob_start();
  182. include $this->templatePath;
  183. return ob_get_clean();
  184. }
  185. /**
  186. * 保存composer.json文件到指定目录
  187. */
  188. public function saveComposerFile(string $packageName, string $content, string $targetPath, SymfonyStyle $io): bool
  189. {
  190. $extensionDir = $targetPath;
  191. // 确保目录存在
  192. if (!is_dir($extensionDir)) {
  193. if (!mkdir($extensionDir, 0755, true)) {
  194. $io->error('无法创建目录: ' . $extensionDir);
  195. return false;
  196. }
  197. }
  198. $outputPath = $extensionDir . '/composer.json';
  199. if (file_put_contents($outputPath, $content) !== false) {
  200. $io->success('composer.json 文件已成功生成: ' . $outputPath);
  201. return true;
  202. } else {
  203. $io->error('无法写入文件: ' . $outputPath);
  204. return false;
  205. }
  206. }
  207. /**
  208. * 完整的生成和保存流程
  209. */
  210. public function createComposerJson(string $packageName, string $namespace, string $id, string $description, string $targetPath, SymfonyStyle $io): bool
  211. {
  212. try {
  213. // 生成内容
  214. $content = $this->generateContent($packageName, $namespace, $id, $description);
  215. // 输出生成的内容
  216. $io->section('生成的 composer.json 内容:');
  217. $io->text($content);
  218. // 询问是否保存文件
  219. if ($io->confirm('是否将内容保存到 composer.json 文件?', true)) {
  220. return $this->saveComposerFile($packageName, $content, $targetPath, $io);
  221. }
  222. return true;
  223. } catch (\Exception $e) {
  224. $io->error('生成 composer.json 时发生错误: ' . $e->getMessage());
  225. return false;
  226. }
  227. }
  228. /**
  229. * 完整的从用户输入到生成文件的工作流
  230. */
  231. public function createComposerFromUserInput(SymfonyStyle $io, ?string $targetPath = null): bool
  232. {
  233. try {
  234. // 获取用户输入
  235. $packageInfo = $this->gatherPackageInfo($io);
  236. // 如果没有提供目标路径,使用默认路径
  237. if ($targetPath === null) {
  238. $vendorDir = dirname(__DIR__, 5);
  239. $targetPath = $vendorDir . '/runtime/extension';
  240. }
  241. // 生成composer.json
  242. return $this->createComposerJson(
  243. $packageInfo['packageName'],
  244. $packageInfo['namespace'],
  245. $packageInfo['id'],
  246. $packageInfo['description'],
  247. $targetPath,
  248. $io
  249. );
  250. } catch (\InvalidArgumentException $e) {
  251. $io->error($e->getMessage());
  252. return false;
  253. }
  254. }
  255. }