diff --git a/.github/workflows/php81.yaml b/.github/workflows/php81.yaml index fead114..77e68ff 100644 --- a/.github/workflows/php81.yaml +++ b/.github/workflows/php81.yaml @@ -8,14 +8,14 @@ on: jobs: test: name: Run Tests - uses: WebFiori/workflows/.github/workflows/test-php.yaml@main + uses: WebFiori/workflows/.github/workflows/test-php.yaml@v1.2.1 with: php-version: '8.1' code-coverage: name: Coverage needs: test - uses: WebFiori/workflows/.github/workflows/coverage-codecov.yaml@main + uses: WebFiori/workflows/.github/workflows/coverage-codecov.yaml@v1.2.1 with: php-version: '8.1' coverage-file: 'php-8.1-coverage.xml' diff --git a/.github/workflows/php82.yaml b/.github/workflows/php82.yaml index 978033a..9e431d3 100644 --- a/.github/workflows/php82.yaml +++ b/.github/workflows/php82.yaml @@ -8,7 +8,7 @@ on: jobs: test: name: Run Tests - uses: WebFiori/workflows/.github/workflows/test-php.yaml@main + uses: WebFiori/workflows/.github/workflows/test-php.yaml@v1.2.1 with: php-version: '8.2' phpunit-config: "tests/phpunit10.xml" @@ -16,7 +16,7 @@ jobs: code-coverage: name: Coverage needs: test - uses: WebFiori/workflows/.github/workflows/coverage-codecov.yaml@main + uses: WebFiori/workflows/.github/workflows/coverage-codecov.yaml@v1.2.1 with: php-version: '8.2' coverage-file: 'php-8.2-coverage.xml' diff --git a/.github/workflows/php83.yaml b/.github/workflows/php83.yaml index 56bbbd1..17c1972 100644 --- a/.github/workflows/php83.yaml +++ b/.github/workflows/php83.yaml @@ -12,7 +12,7 @@ jobs: test: name: Run Tests - uses: WebFiori/workflows/.github/workflows/test-php.yaml@main + uses: WebFiori/workflows/.github/workflows/test-php.yaml@v1.2.1 with: php-version: '8.3' phpunit-config: 'tests/phpunit10.xml' @@ -21,7 +21,7 @@ jobs: code-coverage: name: Coverage needs: test - uses: WebFiori/workflows/.github/workflows/coverage-codecov.yaml@main + uses: WebFiori/workflows/.github/workflows/coverage-codecov.yaml@v1.2.1 with: php-version: '8.3' coverage-file: 'php-8.3-coverage.xml' diff --git a/.github/workflows/php84.yaml b/.github/workflows/php84.yaml index 1ce3097..adcc94b 100644 --- a/.github/workflows/php84.yaml +++ b/.github/workflows/php84.yaml @@ -8,7 +8,7 @@ on: jobs: test: name: Run Tests - uses: WebFiori/workflows/.github/workflows/test-php.yaml@main + uses: WebFiori/workflows/.github/workflows/test-php.yaml@v1.2.1 with: php-version: '8.4' phpunit-config: "tests/phpunit10.xml" @@ -16,7 +16,7 @@ jobs: code-coverage: name: Coverage needs: test - uses: WebFiori/workflows/.github/workflows/coverage-codecov.yaml@main + uses: WebFiori/workflows/.github/workflows/coverage-codecov.yaml@v1.2.1 with: php-version: '8.4' coverage-file: 'php-8.4-coverage.xml' diff --git a/.github/workflows/php85.yaml b/.github/workflows/php85.yaml new file mode 100644 index 0000000..b6743fd --- /dev/null +++ b/.github/workflows/php85.yaml @@ -0,0 +1,27 @@ +name: Build PHP 8.5 + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] +jobs: + test: + name: Run Tests + uses: WebFiori/workflows/.github/workflows/test-php.yaml@v1.2.1 + with: + php-version: '8.5' + phpunit-config: "tests/phpunit10.xml" + + code-coverage: + name: Coverage + needs: test + uses: WebFiori/workflows/.github/workflows/coverage-codecov.yaml@v1.2.1 + with: + php-version: '8.5' + coverage-file: 'php-8.5-coverage.xml' + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + + + diff --git a/README.md b/README.md index 35ffc2d..7b879ab 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ Class library that can help in writing command line based applications with minimum dependencies using PHP.

- - + + @@ -56,6 +56,7 @@ Class library that can help in writing command line based applications with mini | | | | | | +| | ## Features * **Easy Command Creation**: Simple class-based approach to building CLI commands diff --git a/WebFiori/Cli/Commands/MakeCommand.php b/WebFiori/Cli/Commands/MakeCommand.php new file mode 100644 index 0000000..832f360 --- /dev/null +++ b/WebFiori/Cli/Commands/MakeCommand.php @@ -0,0 +1,221 @@ +templateManager = new TemplateManager(); + + parent::__construct('make:command', [ + '--name' => [ + ArgumentOption::DESCRIPTION => 'The name of the command (e.g., "user:create")', + ArgumentOption::OPTIONAL => false + ], + '--class' => [ + ArgumentOption::DESCRIPTION => 'The class name (e.g., "CreateUserCommand")', + ArgumentOption::OPTIONAL => true + ], + '--path' => [ + ArgumentOption::DESCRIPTION => 'Output directory path', + ArgumentOption::OPTIONAL => true, + ArgumentOption::DEFAULT => 'commands' + ], + '--namespace' => [ + ArgumentOption::DESCRIPTION => 'PHP namespace for the command class', + ArgumentOption::OPTIONAL => true + ], + '--interactive' => [ + ArgumentOption::DESCRIPTION => 'Generate command with interactive prompts', + ArgumentOption::OPTIONAL => true + ], + '--args' => [ + ArgumentOption::DESCRIPTION => 'Add command arguments (comma-separated)', + ArgumentOption::OPTIONAL => true + ], + '--template' => [ + ArgumentOption::DESCRIPTION => 'Template type to use', + ArgumentOption::OPTIONAL => true, + ArgumentOption::VALUES => $this->templateManager->getAvailableTemplates(), + ArgumentOption::DEFAULT => 'basic' + ] + ], 'Generate a new CLI command class with scaffolding'); + } + + public function exec(): int { + $this->println('🚀 WebFiori CLI Command Generator'); + $this->println('================================='); + $this->println(); + + // Get command details + $commandName = $this->getArgValue('--name'); + $className = $this->getArgValue('--class') ?? $this->generateClassName($commandName); + $outputPath = $this->getArgValue('--path') ?? 'commands'; + $namespace = $this->getArgValue('--namespace'); + $template = $this->getArgValue('--template') ?? 'basic'; + $interactive = $this->isArgProvided('--interactive'); + $args = $this->getArgValue('--args'); + + // Interactive mode for missing details + if (!$namespace) { + $namespace = $this->getInput('Enter namespace (optional): ') ?: null; + } + + // Validate inputs + if (!$this->validateInputs($commandName, $className)) { + return 1; + } + + // Generate command + try { + $filePath = $this->generateCommand([ + 'name' => $commandName, + 'class' => $className, + 'path' => $outputPath, + 'namespace' => $namespace, + 'template' => $template, + 'interactive' => $interactive, + 'args' => $args ? explode(',', $args) : [] + ]); + + $this->success("✅ Command generated successfully!"); + $this->info("📁 File: $filePath"); + $this->info("🏷️ Class: $className"); + $this->info("⚡ Command: $commandName"); + + $this->println(); + $this->println("Next steps:"); + $this->println("1. Register the command in your application"); + $this->println("2. Implement the exec() method logic"); + $this->println("3. Add any additional arguments or validation"); + + return 0; + } catch (\Exception $e) { + $this->error("❌ Failed to generate command: " . $e->getMessage()); + return 1; + } + } + + /** + * Generate class name from command name. + */ + private function generateClassName(string $commandName): string { + // Convert command-name or namespace:command to ClassName + $parts = preg_split('/[:\-_]/', $commandName); + $className = ''; + + foreach ($parts as $part) { + $className .= ucfirst(strtolower($part)); + } + + return $className . 'Command'; + } + + /** + * Validate command inputs. + */ + private function validateInputs(string $commandName, string $className): bool { + // Validate command name + if (!preg_match('/^[a-z][a-z0-9\-:_]*$/', $commandName)) { + $this->error('Command name must start with a letter and contain only lowercase letters, numbers, hyphens, colons, and underscores.'); + return false; + } + + // Validate class name + if (!preg_match('/^[A-Z][a-zA-Z0-9]*$/', $className)) { + $this->error('Class name must be a valid PHP class name (PascalCase).'); + return false; + } + + return true; + } + + /** + * Generate the command file. + */ + private function generateCommand(array $config): string { + $content = $this->templateManager->processTemplate($config['template'], [ + 'namespace' => $config['namespace'] ? "namespace {$config['namespace']};\n\n" : '', + 'use_statements' => $this->generateUseStatements($config), + 'class_name' => $config['class'], + 'command_name' => $config['name'], + 'command_description' => "Description for {$config['name']} command", + 'arguments' => $this->generateArguments($config['args']) + ]); + + // Ensure output directory exists + $outputDir = $config['path']; + if (!is_dir($outputDir)) { + mkdir($outputDir, 0755, true); + } + + // Generate file path + $fileName = $config['class'] . '.php'; + $filePath = rtrim($outputDir, '/') . '/' . $fileName; + + // Check if file exists + if (file_exists($filePath)) { + $overwrite = $this->confirm("File $filePath already exists. Overwrite?"); + if (!$overwrite) { + throw new \Exception("File already exists and overwrite was declined."); + } + } + + // Write file + file_put_contents($filePath, $content); + + return $filePath; + } + + /** + * Generate use statements. + */ + private function generateUseStatements(array $config): string { + $uses = [ + 'use WebFiori\Cli\Command;', + 'use WebFiori\Cli\ArgumentOption;' + ]; + + if ($config['interactive'] || $config['template'] === 'interactive') { + $uses[] = 'use WebFiori\Cli\InputValidator;'; + } + + return implode("\n", $uses); + } + + /** + * Generate command arguments array. + */ + private function generateArguments(array $args): string { + if (empty($args)) { + return '[]'; + } + + $argStrings = []; + foreach ($args as $arg) { + $arg = trim($arg); + $argName = '--' . strtolower(str_replace(' ', '-', $arg)); + $argStrings[] = " '$argName' => [\n" . + " ArgumentOption::DESCRIPTION => 'Description for $arg',\n" . + " ArgumentOption::OPTIONAL => true\n" . + " ]"; + } + + return "[\n" . implode(",\n", $argStrings) . "\n ]"; + } +} diff --git a/WebFiori/Cli/Table/TableData.php b/WebFiori/Cli/Table/TableData.php index 631355f..c6d14b0 100644 --- a/WebFiori/Cli/Table/TableData.php +++ b/WebFiori/Cli/Table/TableData.php @@ -95,7 +95,7 @@ public static function fromCsv(string $csv, bool $hasHeaders = true, string $del continue; } - $row = str_getcsv($line, $delimiter); + $row = str_getcsv($line, $delimiter, '"', '\\'); if ($hasHeaders && $headers === null) { $headers = $row; diff --git a/WebFiori/Cli/Templates/TemplateManager.php b/WebFiori/Cli/Templates/TemplateManager.php new file mode 100644 index 0000000..aa757f3 --- /dev/null +++ b/WebFiori/Cli/Templates/TemplateManager.php @@ -0,0 +1,55 @@ +templatesPath = $templatesPath ?? __DIR__ . '/stubs'; + } + + /** + * Get template content by name. + */ + public function getTemplate(string $name): string { + $templateFile = $this->templatesPath . '/' . $name . '.stub'; + + if (!file_exists($templateFile)) { + throw new \InvalidArgumentException("Template '$name' not found at: $templateFile"); + } + + return file_get_contents($templateFile); + } + + /** + * Get available templates. + */ + public function getAvailableTemplates(): array { + $templates = []; + $files = glob($this->templatesPath . '/*.stub'); + + foreach ($files as $file) { + $templates[] = basename($file, '.stub'); + } + + return $templates; + } + + /** + * Process template with variables. + */ + public function processTemplate(string $template, array $variables): string { + $content = $this->getTemplate($template); + + foreach ($variables as $key => $value) { + $content = str_replace('{{' . $key . '}}', $value, $content); + } + + return $content; + } +} diff --git a/WebFiori/Cli/Templates/stubs/basic.stub b/WebFiori/Cli/Templates/stubs/basic.stub new file mode 100644 index 0000000..85e8b45 --- /dev/null +++ b/WebFiori/Cli/Templates/stubs/basic.stub @@ -0,0 +1,23 @@ +println('🚀 Executing {{command_name}} command...'); + + // TODO: Implement your command logic here + + $this->success('✅ Command completed successfully!'); + return 0; + } +} diff --git a/WebFiori/Cli/Templates/stubs/crud.stub b/WebFiori/Cli/Templates/stubs/crud.stub new file mode 100644 index 0000000..52c8b94 --- /dev/null +++ b/WebFiori/Cli/Templates/stubs/crud.stub @@ -0,0 +1,73 @@ + [ + ArgumentOption::DESCRIPTION => 'CRUD action to perform', + ArgumentOption::OPTIONAL => true, + ArgumentOption::VALUES => ['create', 'read', 'show', 'update', 'delete', 'list'], + ArgumentOption::DEFAULT => 'list' + ] + ]), '{{command_description}}'); + } + + public function exec(): int { + $action = $this->getArgValue('--action') ?? 'list'; + + switch ($action) { + case 'create': + return $this->createRecord(); + case 'read': + case 'show': + return $this->showRecord(); + case 'update': + return $this->updateRecord(); + case 'delete': + return $this->deleteRecord(); + case 'list': + default: + return $this->listRecords(); + } + } + + private function createRecord(): int { + $this->info('Creating new record...'); + // TODO: Implement create logic + $this->success('✅ Record created successfully!'); + return 0; + } + + private function showRecord(): int { + $this->info('Showing record...'); + // TODO: Implement show logic + return 0; + } + + private function updateRecord(): int { + $this->info('Updating record...'); + // TODO: Implement update logic + $this->success('✅ Record updated successfully!'); + return 0; + } + + private function deleteRecord(): int { + $this->info('Deleting record...'); + // TODO: Implement delete logic + $this->success('✅ Record deleted successfully!'); + return 0; + } + + private function listRecords(): int { + $this->info('Listing records...'); + // TODO: Implement list logic + return 0; + } +} diff --git a/WebFiori/Cli/Templates/stubs/interactive.stub b/WebFiori/Cli/Templates/stubs/interactive.stub new file mode 100644 index 0000000..22cb7b2 --- /dev/null +++ b/WebFiori/Cli/Templates/stubs/interactive.stub @@ -0,0 +1,43 @@ +println('🔧 Interactive Command Setup'); + $this->println('=========================='); + + // Get user input + $name = $this->getInput('Enter name: '); + $email = $this->getInput('Enter email: ', null, new InputValidator(function($email) { + return filter_var($email, FILTER_VALIDATE_EMAIL) !== false; + }, 'Please enter a valid email address!')); + + // Confirm action + if ($this->confirm('Proceed with the operation?')) { + $this->processData($name, $email); + $this->success('✅ Operation completed successfully!'); + } else { + $this->info('Operation cancelled.'); + } + + return 0; + } + + /** + * Process the collected data. + */ + private function processData(string $name, string $email): void { + // TODO: Implement data processing logic + $this->info("Processing data for: $name ($email)"); + } +} diff --git a/examples/01-basic-hello-world/README.md b/examples/01-basic-hello-world/README.md index 1a03ffa..fed2549 100644 --- a/examples/01-basic-hello-world/README.md +++ b/examples/01-basic-hello-world/README.md @@ -128,3 +128,18 @@ class HelloCommand extends Command { ``` This example serves as the foundation for understanding WebFiori CLI basics before moving to more advanced features. + +## Related Examples + +### Next Steps +- **[02-arguments-and-options](../02-arguments-and-options/)** - Learn advanced argument handling and validation +- **[03-user-input](../03-user-input/)** - Add interactive user input to your commands +- **[04-output-formatting](../04-output-formatting/)** - Enhance output with colors and formatting + +### Advanced Features +- **[10-multi-command-app](../10-multi-command-app/)** - Build complete CLI applications +- **[12-command-scaffolding](../12-command-scaffolding/)** - Generate commands automatically + +### Similar Concepts +- **[05-interactive-commands](../05-interactive-commands/)** - Interactive command workflows +- **[11-masked-input](../11-masked-input/)** - Secure input handling diff --git a/examples/02-arguments-and-options/README.md b/examples/02-arguments-and-options/README.md index 0c3b8a9..c7358a1 100644 --- a/examples/02-arguments-and-options/README.md +++ b/examples/02-arguments-and-options/README.md @@ -350,3 +350,22 @@ class UserProfileCommand extends Command { ``` This example demonstrates advanced CLI application development with proper validation, error handling, and user experience design. + +## Related Examples + +### Prerequisites +- **[01-basic-hello-world](../01-basic-hello-world/)** - Start here for basic command concepts + +### Next Steps +- **[03-user-input](../03-user-input/)** - Interactive input and validation +- **[11-masked-input](../11-masked-input/)** - Secure input for sensitive data +- **[04-output-formatting](../04-output-formatting/)** - Enhanced output styling + +### Advanced Applications +- **[10-multi-command-app](../10-multi-command-app/)** - Complete CLI applications +- **[09-database-ops](../09-database-ops/)** - Database operations with validation +- **[12-command-scaffolding](../12-command-scaffolding/)** - Generate commands with arguments + +### Similar Concepts +- **[05-interactive-commands](../05-interactive-commands/)** - Menu-driven interfaces +- **[08-file-processing](../08-file-processing/)** - File operations with validation diff --git a/examples/03-user-input/README.md b/examples/03-user-input/README.md index 72a9cad..b30caad 100644 --- a/examples/03-user-input/README.md +++ b/examples/03-user-input/README.md @@ -373,3 +373,25 @@ private function collectBasicInfo() { - **User Experience**: Rich formatting with emojis and clear section divisions This example demonstrates advanced user input handling suitable for complex CLI applications requiring data collection and validation. + +## Related Examples + +### Prerequisites +- **[01-basic-hello-world](../01-basic-hello-world/)** - Basic command structure +- **[02-arguments-and-options](../02-arguments-and-options/)** - Argument handling and validation + +### Enhanced Input Methods +- **[11-masked-input](../11-masked-input/)** - Secure input for passwords and sensitive data +- **[05-interactive-commands](../05-interactive-commands/)** - Menu-driven interactive workflows + +### Output Enhancement +- **[04-output-formatting](../04-output-formatting/)** - Colors, styles, and formatting +- **[06-table-display](../06-table-display/)** - Structured data presentation +- **[07-progress-bars](../07-progress-bars/)** - Visual progress indicators + +### Complete Applications +- **[10-multi-command-app](../10-multi-command-app/)** - Full CLI applications with user management +- **[09-database-ops](../09-database-ops/)** - Database operations with user input + +### Development Tools +- **[12-command-scaffolding](../12-command-scaffolding/)** - Generate interactive commands automatically diff --git a/examples/04-output-formatting/README.md b/examples/04-output-formatting/README.md index b57f0c7..a9038e1 100644 --- a/examples/04-output-formatting/README.md +++ b/examples/04-output-formatting/README.md @@ -562,3 +562,25 @@ private function showSpinnerAnimation(): void { ``` This example demonstrates professional CLI output formatting suitable for creating visually appealing and user-friendly command-line applications. + +## Related Examples + +### Prerequisites +- **[01-basic-hello-world](../01-basic-hello-world/)** - Basic command and output concepts + +### Enhanced Output Features +- **[06-table-display](../06-table-display/)** - Structured data in formatted tables +- **[07-progress-bars](../07-progress-bars/)** - Professional progress indicators +- **[05-interactive-commands](../05-interactive-commands/)** - Interactive menus with formatting + +### Input with Formatting +- **[03-user-input](../03-user-input/)** - User input with formatted prompts +- **[11-masked-input](../11-masked-input/)** - Secure input with visual feedback + +### Complete Applications +- **[10-multi-command-app](../10-multi-command-app/)** - Full applications with consistent formatting +- **[09-database-ops](../09-database-ops/)** - Database operations with formatted output +- **[08-file-processing](../08-file-processing/)** - File operations with status formatting + +### Development Tools +- **[12-command-scaffolding](../12-command-scaffolding/)** - Generate commands with formatting templates diff --git a/examples/05-interactive-commands/README.md b/examples/05-interactive-commands/README.md index a41aef9..bce5db7 100644 --- a/examples/05-interactive-commands/README.md +++ b/examples/05-interactive-commands/README.md @@ -316,3 +316,26 @@ private function showContextHelp(): void { - **[03-user-input](../03-user-input/)**: Input validation and handling - **[04-output-formatting](../04-output-formatting/)**: ANSI colors and formatting +## Related Examples + +### Prerequisites +- **[01-basic-hello-world](../01-basic-hello-world/)** - Basic command structure +- **[03-user-input](../03-user-input/)** - User input and validation fundamentals +- **[04-output-formatting](../04-output-formatting/)** - ANSI colors and formatting + +### Enhanced Interactive Features +- **[11-masked-input](../11-masked-input/)** - Secure input for sensitive operations +- **[02-arguments-and-options](../02-arguments-and-options/)** - Command arguments and options + +### Visual Enhancements +- **[06-table-display](../06-table-display/)** - Display data in formatted tables +- **[07-progress-bars](../07-progress-bars/)** - Visual progress indicators + +### Complete Applications +- **[10-multi-command-app](../10-multi-command-app/)** - Full CLI applications with menus +- **[09-database-ops](../09-database-ops/)** - Database management with interactive menus +- **[08-file-processing](../08-file-processing/)** - File operations with user interaction + +### Development Tools +- **[12-command-scaffolding](../12-command-scaffolding/)** - Generate interactive commands automatically + diff --git a/examples/06-table-display/README.md b/examples/06-table-display/README.md index 14c6b63..b7d2cce 100644 --- a/examples/06-table-display/README.md +++ b/examples/06-table-display/README.md @@ -298,3 +298,25 @@ php main.php table-demo --help - **[05-interactive-commands](../05-interactive-commands/)** - Interactive menu systems - **[08-file-processing](../08-file-processing/)** - File data processing - **[10-multi-command-app](../10-multi-command-app/)** - Complete CLI applications + +## Related Examples + +### Prerequisites +- **[01-basic-hello-world](../01-basic-hello-world/)** - Basic command structure +- **[04-output-formatting](../04-output-formatting/)** - Colors and formatting basics + +### Enhanced Display Features +- **[07-progress-bars](../07-progress-bars/)** - Visual progress indicators +- **[05-interactive-commands](../05-interactive-commands/)** - Interactive menus with tables + +### Data Sources +- **[08-file-processing](../08-file-processing/)** - Process files and display results in tables +- **[09-database-ops](../09-database-ops/)** - Database queries with table output +- **[03-user-input](../03-user-input/)** - Collect data and display in tables + +### Complete Applications +- **[10-multi-command-app](../10-multi-command-app/)** - Full applications with data display +- **[02-arguments-and-options](../02-arguments-and-options/)** - Commands with formatted output + +### Development Tools +- **[12-command-scaffolding](../12-command-scaffolding/)** - Generate commands with table display diff --git a/examples/07-progress-bars/README.md b/examples/07-progress-bars/README.md index 72769e2..1b3b6a4 100644 --- a/examples/07-progress-bars/README.md +++ b/examples/07-progress-bars/README.md @@ -302,3 +302,24 @@ php main.php progress-demo --style=custom --items=20 --delay=50 - **[06-table-display](../06-table-display/)** - Data presentation techniques - **[08-file-processing](../08-file-processing/)** - File operations with progress - **[10-multi-command-app](../10-multi-command-app/)** - Complete CLI applications +## Related Examples + +### Prerequisites +- **[01-basic-hello-world](../01-basic-hello-world/)** - Basic command structure +- **[04-output-formatting](../04-output-formatting/)** - Colors and formatting + +### Visual Enhancement +- **[06-table-display](../06-table-display/)** - Structured data display +- **[05-interactive-commands](../05-interactive-commands/)** - Interactive workflows with progress + +### Long-Running Operations +- **[08-file-processing](../08-file-processing/)** - File operations with progress tracking +- **[09-database-ops](../09-database-ops/)** - Database operations with progress indicators +- **[03-user-input](../03-user-input/)** - Multi-step processes with progress + +### Complete Applications +- **[10-multi-command-app](../10-multi-command-app/)** - Full applications with progress feedback +- **[02-arguments-and-options](../02-arguments-and-options/)** - Commands with progress reporting + +### Development Tools +- **[12-command-scaffolding](../12-command-scaffolding/)** - Generate commands with progress bars diff --git a/examples/08-file-processing/README.md b/examples/08-file-processing/README.md index c530980..d3b0f60 100644 --- a/examples/08-file-processing/README.md +++ b/examples/08-file-processing/README.md @@ -274,3 +274,25 @@ class FileProcessCommand extends Command { - **[04-output-formatting](../04-output-formatting/)** - Text formatting and colors - **[07-progress-bars](../07-progress-bars/)** - Progress tracking for file operations - **[10-multi-command-app](../10-multi-command-app/)** - Complete CLI applications +## Related Examples + +### Prerequisites +- **[01-basic-hello-world](../01-basic-hello-world/)** - Basic command structure +- **[02-arguments-and-options](../02-arguments-and-options/)** - File path arguments and validation + +### Enhanced Processing +- **[07-progress-bars](../07-progress-bars/)** - Visual progress for file operations +- **[06-table-display](../06-table-display/)** - Display file data in formatted tables +- **[04-output-formatting](../04-output-formatting/)** - Formatted status messages + +### User Interaction +- **[03-user-input](../03-user-input/)** - Interactive file selection +- **[05-interactive-commands](../05-interactive-commands/)** - File operation menus +- **[11-masked-input](../11-masked-input/)** - Secure file path input + +### Complete Applications +- **[10-multi-command-app](../10-multi-command-app/)** - Full applications with file management +- **[09-database-ops](../09-database-ops/)** - Database import/export from files + +### Development Tools +- **[12-command-scaffolding](../12-command-scaffolding/)** - Generate file processing commands diff --git a/examples/09-database-ops/README.md b/examples/09-database-ops/README.md index 6788a8c..a33f8b5 100644 --- a/examples/09-database-ops/README.md +++ b/examples/09-database-ops/README.md @@ -599,3 +599,25 @@ class QueryOptimizer { } } ``` +## Related Examples + +### Prerequisites +- **[01-basic-hello-world](../01-basic-hello-world/)** - Basic command structure +- **[02-arguments-and-options](../02-arguments-and-options/)** - Database connection arguments + +### Enhanced Features +- **[06-table-display](../06-table-display/)** - Display query results in tables +- **[07-progress-bars](../07-progress-bars/)** - Progress for long database operations +- **[05-interactive-commands](../05-interactive-commands/)** - Database management menus + +### User Interaction +- **[03-user-input](../03-user-input/)** - Interactive database configuration +- **[11-masked-input](../11-masked-input/)** - Secure database password input +- **[04-output-formatting](../04-output-formatting/)** - Formatted database status + +### Data Processing +- **[08-file-processing](../08-file-processing/)** - Import/export database data +- **[10-multi-command-app](../10-multi-command-app/)** - Complete database CLI applications + +### Development Tools +- **[12-command-scaffolding](../12-command-scaffolding/)** - Generate database operation commands diff --git a/examples/10-multi-command-app/README.md b/examples/10-multi-command-app/README.md index 7b1facd..afb40ad 100644 --- a/examples/10-multi-command-app/README.md +++ b/examples/10-multi-command-app/README.md @@ -713,3 +713,23 @@ class ApiClient { } } ``` +## Related Examples + +### Building Blocks (Start Here) +- **[01-basic-hello-world](../01-basic-hello-world/)** - Basic command structure +- **[02-arguments-and-options](../02-arguments-and-options/)** - Command arguments and validation +- **[03-user-input](../03-user-input/)** - User input and interaction + +### Feature Integration +- **[04-output-formatting](../04-output-formatting/)** - Professional output styling +- **[05-interactive-commands](../05-interactive-commands/)** - Menu-driven interfaces +- **[06-table-display](../06-table-display/)** - Data presentation in tables +- **[07-progress-bars](../07-progress-bars/)** - Visual progress feedback +- **[11-masked-input](../11-masked-input/)** - Secure input handling + +### Specialized Operations +- **[08-file-processing](../08-file-processing/)** - File management commands +- **[09-database-ops](../09-database-ops/)** - Database operation commands + +### Development Tools +- **[12-command-scaffolding](../12-command-scaffolding/)** - Generate commands for your application diff --git a/examples/11-masked-input/README.md b/examples/11-masked-input/README.md index 8bb8c7e..92bf0cb 100644 --- a/examples/11-masked-input/README.md +++ b/examples/11-masked-input/README.md @@ -163,3 +163,25 @@ $this->assertContains('Password received: secret123', $output); --- **Ready to secure your CLI applications?** Try the different demo modes to see masked input in action! +## Related Examples + +### Prerequisites +- **[01-basic-hello-world](../01-basic-hello-world/)** - Basic command structure +- **[03-user-input](../03-user-input/)** - User input fundamentals + +### Enhanced Input Methods +- **[02-arguments-and-options](../02-arguments-and-options/)** - Command arguments with validation +- **[05-interactive-commands](../05-interactive-commands/)** - Interactive menus and workflows + +### Visual Enhancement +- **[04-output-formatting](../04-output-formatting/)** - Colors and formatting for prompts +- **[06-table-display](../06-table-display/)** - Display collected data in tables +- **[07-progress-bars](../07-progress-bars/)** - Progress indicators for data processing + +### Complete Applications +- **[10-multi-command-app](../10-multi-command-app/)** - Full applications with secure authentication +- **[09-database-ops](../09-database-ops/)** - Database operations with secure credentials +- **[08-file-processing](../08-file-processing/)** - File operations with secure paths + +### Development Tools +- **[12-command-scaffolding](../12-command-scaffolding/)** - Generate commands with masked input diff --git a/examples/12-command-scaffolding/README.md b/examples/12-command-scaffolding/README.md new file mode 100644 index 0000000..09d94ca --- /dev/null +++ b/examples/12-command-scaffolding/README.md @@ -0,0 +1,302 @@ +# Command Scaffolding Tools + +This example demonstrates the **command scaffolding functionality** in WebFiori CLI, which allows developers to quickly generate new command classes with proper structure, documentation, and templates. + +## Features + +- **Multiple Templates**: Basic, Interactive, CRUD, and File Processor templates +- **Smart Naming**: Automatic class name generation from command names +- **Namespace Support**: Generate commands with custom namespaces +- **Argument Generation**: Automatically create command arguments +- **Validation**: Input validation for command and class names +- **Overwrite Protection**: Confirmation prompts for existing files + +## Running the Example + +### Basic Command Generation +```bash +# Generate a basic command +php main.php make:command --name=hello-world + +# Generate with custom class name +php main.php make:command --name=user:create --class=CreateUserCommand + +# Generate with namespace +php main.php make:command --name=process-data --namespace="App\\Commands" +``` + +### Template-Based Generation +```bash +# Interactive command template +php main.php make:command --name=setup-wizard --template=interactive + +# CRUD operations template +php main.php make:command --name=user-manager --template=crud + +# File processor template +php main.php make:command --name=file-converter --template=file-processor +``` + +### Advanced Options +```bash +# Generate with arguments +php main.php make:command --name=backup-db --args="database,output-path,compress" + +# Custom output directory +php main.php make:command --name=deploy --path=src/Commands + +# All options combined +php main.php make:command \ + --name=api:sync \ + --class=ApiSyncCommand \ + --namespace="MyApp\\Commands" \ + --template=interactive \ + --args="endpoint,token,timeout" \ + --path=app/Commands +``` + +## Available Templates + +### 1. Basic Template +Simple command structure with minimal boilerplate: +```php +class HelloWorldCommand extends Command { + public function __construct() { + parent::__construct('hello-world', [], 'Description'); + } + + public function exec(): int { + $this->println('🚀 Executing hello-world command...'); + // TODO: Implement your command logic here + $this->success('✅ Command completed successfully!'); + return 0; + } +} +``` + +### 2. Interactive Template +Command with user input and validation: +```php +public function exec(): int { + $name = $this->getInput('Enter name: '); + $email = $this->getInput('Enter email: ', null, new InputValidator(function($email) { + return filter_var($email, FILTER_VALIDATE_EMAIL) !== false; + }, 'Please enter a valid email address!')); + + if ($this->confirm('Proceed with the operation?')) { + $this->processData($name, $email); + $this->success('✅ Operation completed successfully!'); + } + + return 0; +} +``` + +### 3. CRUD Template +Full CRUD operations structure: +```php +public function exec(): int { + $action = $this->getArgValue('--action') ?? 'list'; + + switch ($action) { + case 'create': return $this->createRecord(); + case 'read': return $this->showRecord(); + case 'update': return $this->updateRecord(); + case 'delete': return $this->deleteRecord(); + case 'list': + default: return $this->listRecords(); + } +} +``` + +### 4. File Processor Template +File processing with error handling: +```php +public function exec(): int { + $inputFile = $this->getArgValue('--input'); + + if (!$inputFile || !file_exists($inputFile)) { + $this->error('Input file is required and must exist!'); + return 1; + } + + try { + $this->processFile($inputFile, $this->getArgValue('--output')); + $this->success('✅ File processed successfully!'); + return 0; + } catch (\Exception $e) { + $this->error('❌ Error: ' . $e->getMessage()); + return 1; + } +} +``` + +## Generated Command Structure + +All generated commands include: + +### Proper Documentation +```php +/** + * CommandName - Generated CLI command. + * + * Description for command-name command + */ +class CommandNameCommand extends Command { +``` + +### Constructor with Arguments +```php +public function __construct() { + parent::__construct('command-name', [ + '--input-file' => [ + ArgumentOption::DESCRIPTION => 'Description for input file', + ArgumentOption::OPTIONAL => true + ] + ], 'Command description'); +} +``` + +### Structured Exec Method +```php +public function exec(): int { + // Template-specific implementation + return 0; +} +``` + +### Additional Helper Methods +Template-specific helper methods for common operations. + +## Smart Naming Conventions + +The scaffolding tool automatically converts command names to proper class names: + +| Command Name | Generated Class Name | +|--------------|---------------------| +| `hello-world` | `HelloWorldCommand` | +| `user:create` | `UserCreateCommand` | +| `api_sync` | `ApiSyncCommand` | +| `process-data-file` | `ProcessDataFileCommand` | + +## Validation Features + +### Command Name Validation +- Must start with a letter +- Can contain lowercase letters, numbers, hyphens, colons, underscores +- Examples: `hello`, `user:create`, `process-data` + +### Class Name Validation +- Must be valid PHP class name (PascalCase) +- Automatically generated if not provided +- Examples: `HelloCommand`, `UserCreateCommand` + +### File Overwrite Protection +```bash +$ php main.php make:command --name=existing-command +File /path/to/ExistingCommand.php already exists. Overwrite? (y/n): n +❌ Failed to generate command: File already exists and overwrite was declined. +``` + +## Integration with Your Application + +After generating commands, integrate them into your application: + +### 1. Register the Command +```php +use App\Commands\GeneratedCommand; + +$runner = new Runner(); +$runner->register(new GeneratedCommand()); +``` + +### 2. Implement Logic +Edit the generated `exec()` method to add your specific functionality. + +### 3. Add Tests +Create unit tests for your generated commands using `CommandTestCase`. + +## Best Practices + +### Naming Conventions +- Use kebab-case for command names: `user-create`, `data-export` +- Use namespaces for organization: `user:create`, `db:migrate` +- Keep names descriptive but concise + +### Template Selection +- **Basic**: Simple commands with minimal logic +- **Interactive**: Commands requiring user input +- **CRUD**: Data management commands +- **File Processor**: File manipulation commands + +### Organization +``` +app/ +├── Commands/ +│ ├── User/ +│ │ ├── CreateUserCommand.php +│ │ └── DeleteUserCommand.php +│ ├── Database/ +│ │ ├── MigrateCommand.php +│ │ └── SeedCommand.php +│ └── File/ +│ ├── ProcessCommand.php +│ └── ConvertCommand.php +``` + +## Example Workflow + +### 1. Generate User Management Commands +```bash +# Create user command +php main.php make:command --name=user:create --template=interactive --namespace="App\\Commands\\User" + +# List users command +php main.php make:command --name=user:list --template=crud --namespace="App\\Commands\\User" + +# Delete user command +php main.php make:command --name=user:delete --namespace="App\\Commands\\User" +``` + +### 2. Generate File Processing Commands +```bash +# CSV processor +php main.php make:command --name=csv:process --template=file-processor --args="input,output,delimiter" + +# Image converter +php main.php make:command --name=image:convert --template=file-processor --args="input,format,quality" +``` + +### 3. Generate API Commands +```bash +# API sync command +php main.php make:command --name=api:sync --template=interactive --args="endpoint,token" + +# Data export command +php main.php make:command --name=data:export --args="format,output,filter" +``` + +--- + +**Ready to boost your development speed?** Use the scaffolding tools to generate well-structured commands in seconds! +## Related Examples + +### Generated Command Examples +- **[01-basic-hello-world](../01-basic-hello-world/)** - Basic template structure +- **[02-arguments-and-options](../02-arguments-and-options/)** - Commands with arguments (CRUD template) +- **[03-user-input](../03-user-input/)** - Interactive commands (Interactive template) +- **[08-file-processing](../08-file-processing/)** - File operations (File Processor template) + +### Enhanced Features for Generated Commands +- **[04-output-formatting](../04-output-formatting/)** - Add formatting to generated commands +- **[05-interactive-commands](../05-interactive-commands/)** - Interactive workflows +- **[06-table-display](../06-table-display/)** - Data display in generated commands +- **[07-progress-bars](../07-progress-bars/)** - Progress indicators +- **[11-masked-input](../11-masked-input/)** - Secure input in generated commands + +### Complete Applications +- **[10-multi-command-app](../10-multi-command-app/)** - Applications built with scaffolded commands +- **[09-database-ops](../09-database-ops/)** - Database commands (perfect for CRUD template) + +### Development Workflow +Use this scaffolding tool to quickly generate commands for any of the above examples! diff --git a/examples/12-command-scaffolding/main.php b/examples/12-command-scaffolding/main.php new file mode 100644 index 0000000..f568443 --- /dev/null +++ b/examples/12-command-scaffolding/main.php @@ -0,0 +1,23 @@ +register(new MakeCommand()); + +// Start the application +exit($runner->start()); diff --git a/tests/WebFiori/Tests/Cli/MakeCommandTest.php b/tests/WebFiori/Tests/Cli/MakeCommandTest.php new file mode 100644 index 0000000..7ce555b --- /dev/null +++ b/tests/WebFiori/Tests/Cli/MakeCommandTest.php @@ -0,0 +1,236 @@ +testOutputDir)) { + $this->removeDirectory($this->testOutputDir); + } + } + + protected function tearDown(): void { + parent::tearDown(); + // Clean up test directory + if (is_dir($this->testOutputDir)) { + $this->removeDirectory($this->testOutputDir); + } + } + + /** + * Test basic command generation. + * + * @test + */ + public function testBasicCommandGeneration() { + $command = new MakeCommand(); + + $output = $this->executeSingleCommand($command, [ + '--name' => 'test-command', + '--class' => 'TestCommand', + '--path' => $this->testOutputDir + ], ['', 'y']); // Empty namespace, then confirm overwrite if needed + + // Check for success message (flexible matching) + $found = false; + foreach ($output as $line) { + if (strpos($line, 'Command generated successfully!') !== false) { + $found = true; + break; + } + } + $this->assertTrue($found, 'Success message not found in output'); + $this->assertEquals(0, $this->getExitCode()); + + // Check if file was created + $expectedFile = $this->testOutputDir . '/TestCommand.php'; + $this->assertTrue(file_exists($expectedFile), "Command file should be created"); + + // Check file content + $content = file_get_contents($expectedFile); + $this->assertStringContainsString('class TestCommand extends Command', $content); + $this->assertStringContainsString("'test-command'", $content); + } + + /** + * Test command generation with namespace. + * + * @test + */ + public function testCommandGenerationWithNamespace() { + $command = new MakeCommand(); + + $output = $this->executeSingleCommand($command, [ + '--name' => 'user:create', + '--namespace' => 'App\\Commands', + '--path' => $this->testOutputDir + ]); + + $this->assertEquals(0, $this->getExitCode()); + + $expectedFile = $this->testOutputDir . '/UserCreateCommand.php'; + $this->assertTrue(file_exists($expectedFile)); + + $content = file_get_contents($expectedFile); + $this->assertStringContainsString('namespace App\\Commands;', $content); + $this->assertStringContainsString('class UserCreateCommand extends Command', $content); + } + + /** + * Test interactive template generation. + * + * @test + */ + public function testInteractiveTemplate() { + $command = new MakeCommand(); + + $output = $this->executeSingleCommand($command, [ + '--name' => 'setup-wizard', + '--template' => 'interactive', + '--path' => $this->testOutputDir + ], ['']); // Empty namespace input + + $this->assertEquals(0, $this->getExitCode()); + + $expectedFile = $this->testOutputDir . '/SetupWizardCommand.php'; + $content = file_get_contents($expectedFile); + + $this->assertStringContainsString('InputValidator', $content); + $this->assertStringContainsString('getInput(', $content); + $this->assertStringContainsString('confirm(', $content); + } + + /** + * Test CRUD template generation. + * + * @test + */ + public function testCrudTemplate() { + $command = new MakeCommand(); + + $output = $this->executeSingleCommand($command, [ + '--name' => 'user-manager', + '--template' => 'crud', + '--path' => $this->testOutputDir + ], ['']); // Empty namespace input + + $this->assertEquals(0, $this->getExitCode()); + + $expectedFile = $this->testOutputDir . '/UserManagerCommand.php'; + $content = file_get_contents($expectedFile); + + $this->assertStringContainsString('createRecord()', $content); + $this->assertStringContainsString('updateRecord()', $content); + $this->assertStringContainsString('deleteRecord()', $content); + $this->assertStringContainsString('listRecords()', $content); + } + + /** + * Test command generation with arguments. + * + * @test + */ + public function testCommandWithArguments() { + $command = new MakeCommand(); + + $output = $this->executeSingleCommand($command, [ + '--name' => 'process-data', + '--args' => 'input file,output format,verbose mode', + '--path' => $this->testOutputDir + ], ['']); // Empty namespace input + + $this->assertEquals(0, $this->getExitCode()); + + $expectedFile = $this->testOutputDir . '/ProcessDataCommand.php'; + $content = file_get_contents($expectedFile); + + $this->assertStringContainsString('--input-file', $content); + $this->assertStringContainsString('--output-format', $content); + $this->assertStringContainsString('--verbose-mode', $content); + } + + /** + * Test invalid command name validation. + * + * @test + */ + public function testInvalidCommandName() { + $command = new MakeCommand(); + + $output = $this->executeSingleCommand($command, [ + '--name' => 'Invalid Command Name!', + '--path' => $this->testOutputDir + ], ['']); // Empty namespace input + + $this->assertEquals(1, $this->getExitCode()); + // Check for validation error message (flexible matching) + $found = false; + foreach ($output as $line) { + if (strpos($line, 'Command name must start with a letter') !== false) { + $found = true; + break; + } + } + $this->assertTrue($found, 'Validation error message not found in output'); + } + + /** + * Test file overwrite confirmation. + * + * @test + */ + public function testFileOverwriteConfirmation() { + $command = new MakeCommand(); + + // Create file first + $this->executeSingleCommand($command, [ + '--name' => 'existing-command', + '--path' => $this->testOutputDir + ], ['']); // Empty namespace input + + // Try to create again with 'no' confirmation + $output = $this->executeSingleCommand($command, [ + '--name' => 'existing-command', + '--path' => $this->testOutputDir + ], ['', 'n']); // Empty namespace, then 'no' to overwrite + + $this->assertEquals(1, $this->getExitCode()); + // Check for overwrite declined message (flexible matching) + $found = false; + foreach ($output as $line) { + if (strpos($line, 'File already exists and overwrite was declined') !== false) { + $found = true; + break; + } + } + $this->assertTrue($found, 'Overwrite declined message not found in output'); + } + + /** + * Helper method to remove directory recursively. + */ + private function removeDirectory(string $dir): void { + if (!is_dir($dir)) { + return; + } + + $files = array_diff(scandir($dir), ['.', '..']); + foreach ($files as $file) { + $path = $dir . '/' . $file; + is_dir($path) ? $this->removeDirectory($path) : unlink($path); + } + rmdir($dir); + } +}