|
| 1 | +<?php |
| 2 | +declare(strict_types=1); |
| 3 | +namespace WebFiori\Cli\Commands; |
| 4 | + |
| 5 | +use WebFiori\Cli\ArgumentOption; |
| 6 | +use WebFiori\Cli\Command; |
| 7 | +use WebFiori\Cli\InputValidator; |
| 8 | +use WebFiori\Cli\Templates\TemplateManager; |
| 9 | +use WebFiori\File\File; |
| 10 | + |
| 11 | +/** |
| 12 | + * Command scaffolding tool for generating new CLI commands. |
| 13 | + * |
| 14 | + * This command helps developers quickly create new command classes with proper |
| 15 | + * structure, documentation, and optional features like arguments, validation, |
| 16 | + * and interactive prompts. |
| 17 | + */ |
| 18 | +class MakeCommand extends Command { |
| 19 | + |
| 20 | + private TemplateManager $templateManager; |
| 21 | + |
| 22 | + public function __construct() { |
| 23 | + $this->templateManager = new TemplateManager(); |
| 24 | + |
| 25 | + parent::__construct('make:command', [ |
| 26 | + '--name' => [ |
| 27 | + ArgumentOption::DESCRIPTION => 'The name of the command (e.g., "user:create")', |
| 28 | + ArgumentOption::OPTIONAL => false |
| 29 | + ], |
| 30 | + '--class' => [ |
| 31 | + ArgumentOption::DESCRIPTION => 'The class name (e.g., "CreateUserCommand")', |
| 32 | + ArgumentOption::OPTIONAL => true |
| 33 | + ], |
| 34 | + '--path' => [ |
| 35 | + ArgumentOption::DESCRIPTION => 'Output directory path', |
| 36 | + ArgumentOption::OPTIONAL => true, |
| 37 | + ArgumentOption::DEFAULT => 'commands' |
| 38 | + ], |
| 39 | + '--namespace' => [ |
| 40 | + ArgumentOption::DESCRIPTION => 'PHP namespace for the command class', |
| 41 | + ArgumentOption::OPTIONAL => true |
| 42 | + ], |
| 43 | + '--interactive' => [ |
| 44 | + ArgumentOption::DESCRIPTION => 'Generate command with interactive prompts', |
| 45 | + ArgumentOption::OPTIONAL => true |
| 46 | + ], |
| 47 | + '--args' => [ |
| 48 | + ArgumentOption::DESCRIPTION => 'Add command arguments (comma-separated)', |
| 49 | + ArgumentOption::OPTIONAL => true |
| 50 | + ], |
| 51 | + '--template' => [ |
| 52 | + ArgumentOption::DESCRIPTION => 'Template type to use', |
| 53 | + ArgumentOption::OPTIONAL => true, |
| 54 | + ArgumentOption::VALUES => $this->templateManager->getAvailableTemplates(), |
| 55 | + ArgumentOption::DEFAULT => 'basic' |
| 56 | + ] |
| 57 | + ], 'Generate a new CLI command class with scaffolding'); |
| 58 | + } |
| 59 | + |
| 60 | + public function exec(): int { |
| 61 | + $this->println('🚀 WebFiori CLI Command Generator'); |
| 62 | + $this->println('================================='); |
| 63 | + $this->println(); |
| 64 | + |
| 65 | + // Get command details |
| 66 | + $commandName = $this->getArgValue('--name'); |
| 67 | + $className = $this->getArgValue('--class') ?? $this->generateClassName($commandName); |
| 68 | + $outputPath = $this->getArgValue('--path') ?? 'commands'; |
| 69 | + $namespace = $this->getArgValue('--namespace'); |
| 70 | + $template = $this->getArgValue('--template') ?? 'basic'; |
| 71 | + $interactive = $this->isArgProvided('--interactive'); |
| 72 | + $args = $this->getArgValue('--args'); |
| 73 | + |
| 74 | + // Interactive mode for missing details |
| 75 | + if (!$namespace) { |
| 76 | + $namespace = $this->getInput('Enter namespace (optional): ') ?: null; |
| 77 | + } |
| 78 | + |
| 79 | + // Validate inputs |
| 80 | + if (!$this->validateInputs($commandName, $className)) { |
| 81 | + return 1; |
| 82 | + } |
| 83 | + |
| 84 | + // Generate command |
| 85 | + try { |
| 86 | + $filePath = $this->generateCommand([ |
| 87 | + 'name' => $commandName, |
| 88 | + 'class' => $className, |
| 89 | + 'path' => $outputPath, |
| 90 | + 'namespace' => $namespace, |
| 91 | + 'template' => $template, |
| 92 | + 'interactive' => $interactive, |
| 93 | + 'args' => $args ? explode(',', $args) : [] |
| 94 | + ]); |
| 95 | + |
| 96 | + $this->success("✅ Command generated successfully!"); |
| 97 | + $this->info("📁 File: $filePath"); |
| 98 | + $this->info("🏷️ Class: $className"); |
| 99 | + $this->info("⚡ Command: $commandName"); |
| 100 | + |
| 101 | + $this->println(); |
| 102 | + $this->println("Next steps:"); |
| 103 | + $this->println("1. Register the command in your application"); |
| 104 | + $this->println("2. Implement the exec() method logic"); |
| 105 | + $this->println("3. Add any additional arguments or validation"); |
| 106 | + |
| 107 | + return 0; |
| 108 | + } catch (\Exception $e) { |
| 109 | + $this->error("❌ Failed to generate command: " . $e->getMessage()); |
| 110 | + return 1; |
| 111 | + } |
| 112 | + } |
| 113 | + |
| 114 | + /** |
| 115 | + * Generate class name from command name. |
| 116 | + */ |
| 117 | + private function generateClassName(string $commandName): string { |
| 118 | + // Convert command-name or namespace:command to ClassName |
| 119 | + $parts = preg_split('/[:\-_]/', $commandName); |
| 120 | + $className = ''; |
| 121 | + |
| 122 | + foreach ($parts as $part) { |
| 123 | + $className .= ucfirst(strtolower($part)); |
| 124 | + } |
| 125 | + |
| 126 | + return $className . 'Command'; |
| 127 | + } |
| 128 | + |
| 129 | + /** |
| 130 | + * Validate command inputs. |
| 131 | + */ |
| 132 | + private function validateInputs(string $commandName, string $className): bool { |
| 133 | + // Validate command name |
| 134 | + if (!preg_match('/^[a-z][a-z0-9\-:_]*$/', $commandName)) { |
| 135 | + $this->error('Command name must start with a letter and contain only lowercase letters, numbers, hyphens, colons, and underscores.'); |
| 136 | + return false; |
| 137 | + } |
| 138 | + |
| 139 | + // Validate class name |
| 140 | + if (!preg_match('/^[A-Z][a-zA-Z0-9]*$/', $className)) { |
| 141 | + $this->error('Class name must be a valid PHP class name (PascalCase).'); |
| 142 | + return false; |
| 143 | + } |
| 144 | + |
| 145 | + return true; |
| 146 | + } |
| 147 | + |
| 148 | + /** |
| 149 | + * Generate the command file. |
| 150 | + */ |
| 151 | + private function generateCommand(array $config): string { |
| 152 | + $content = $this->templateManager->processTemplate($config['template'], [ |
| 153 | + 'namespace' => $config['namespace'] ? "namespace {$config['namespace']};\n\n" : '', |
| 154 | + 'use_statements' => $this->generateUseStatements($config), |
| 155 | + 'class_name' => $config['class'], |
| 156 | + 'command_name' => $config['name'], |
| 157 | + 'command_description' => "Description for {$config['name']} command", |
| 158 | + 'arguments' => $this->generateArguments($config['args']) |
| 159 | + ]); |
| 160 | + |
| 161 | + // Ensure output directory exists |
| 162 | + $outputDir = $config['path']; |
| 163 | + if (!is_dir($outputDir)) { |
| 164 | + mkdir($outputDir, 0755, true); |
| 165 | + } |
| 166 | + |
| 167 | + // Generate file path |
| 168 | + $fileName = $config['class'] . '.php'; |
| 169 | + $filePath = rtrim($outputDir, '/') . '/' . $fileName; |
| 170 | + |
| 171 | + // Check if file exists |
| 172 | + if (file_exists($filePath)) { |
| 173 | + $overwrite = $this->confirm("File $filePath already exists. Overwrite?"); |
| 174 | + if (!$overwrite) { |
| 175 | + throw new \Exception("File already exists and overwrite was declined."); |
| 176 | + } |
| 177 | + } |
| 178 | + |
| 179 | + // Write file |
| 180 | + file_put_contents($filePath, $content); |
| 181 | + |
| 182 | + return $filePath; |
| 183 | + } |
| 184 | + |
| 185 | + /** |
| 186 | + * Generate use statements. |
| 187 | + */ |
| 188 | + private function generateUseStatements(array $config): string { |
| 189 | + $uses = [ |
| 190 | + 'use WebFiori\Cli\Command;', |
| 191 | + 'use WebFiori\Cli\ArgumentOption;' |
| 192 | + ]; |
| 193 | + |
| 194 | + if ($config['interactive'] || $config['template'] === 'interactive') { |
| 195 | + $uses[] = 'use WebFiori\Cli\InputValidator;'; |
| 196 | + } |
| 197 | + |
| 198 | + return implode("\n", $uses); |
| 199 | + } |
| 200 | + |
| 201 | + /** |
| 202 | + * Generate command arguments array. |
| 203 | + */ |
| 204 | + private function generateArguments(array $args): string { |
| 205 | + if (empty($args)) { |
| 206 | + return '[]'; |
| 207 | + } |
| 208 | + |
| 209 | + $argStrings = []; |
| 210 | + foreach ($args as $arg) { |
| 211 | + $arg = trim($arg); |
| 212 | + $argName = '--' . strtolower(str_replace(' ', '-', $arg)); |
| 213 | + $argStrings[] = " '$argName' => [\n" . |
| 214 | + " ArgumentOption::DESCRIPTION => 'Description for $arg',\n" . |
| 215 | + " ArgumentOption::OPTIONAL => true\n" . |
| 216 | + " ]"; |
| 217 | + } |
| 218 | + |
| 219 | + return "[\n" . implode(",\n", $argStrings) . "\n ]"; |
| 220 | + } |
| 221 | +} |
0 commit comments