Skip to content

Commit 69e05c0

Browse files
authored
chore: Merge pull request #38 from WebFiori/feat-scaffolding
Feat scaffolding
2 parents e4ee130 + 87534c6 commit 69e05c0

File tree

26 files changed

+1245
-11
lines changed

26 files changed

+1245
-11
lines changed

.github/workflows/php81.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@ on:
88
jobs:
99
test:
1010
name: Run Tests
11-
uses: WebFiori/workflows/.github/workflows/test-php.yaml@main
11+
uses: WebFiori/workflows/.github/workflows/test-php.yaml@v1.2.1
1212
with:
1313
php-version: '8.1'
1414

1515
code-coverage:
1616
name: Coverage
1717
needs: test
18-
uses: WebFiori/workflows/.github/workflows/coverage-codecov.yaml@main
18+
uses: WebFiori/workflows/.github/workflows/coverage-codecov.yaml@v1.2.1
1919
with:
2020
php-version: '8.1'
2121
coverage-file: 'php-8.1-coverage.xml'

.github/workflows/php82.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@ on:
88
jobs:
99
test:
1010
name: Run Tests
11-
uses: WebFiori/workflows/.github/workflows/test-php.yaml@main
11+
uses: WebFiori/workflows/.github/workflows/test-php.yaml@v1.2.1
1212
with:
1313
php-version: '8.2'
1414
phpunit-config: "tests/phpunit10.xml"
1515

1616
code-coverage:
1717
name: Coverage
1818
needs: test
19-
uses: WebFiori/workflows/.github/workflows/coverage-codecov.yaml@main
19+
uses: WebFiori/workflows/.github/workflows/coverage-codecov.yaml@v1.2.1
2020
with:
2121
php-version: '8.2'
2222
coverage-file: 'php-8.2-coverage.xml'

.github/workflows/php83.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212

1313
test:
1414
name: Run Tests
15-
uses: WebFiori/workflows/.github/workflows/test-php.yaml@main
15+
uses: WebFiori/workflows/.github/workflows/test-php.yaml@v1.2.1
1616
with:
1717
php-version: '8.3'
1818
phpunit-config: 'tests/phpunit10.xml'
@@ -21,7 +21,7 @@ jobs:
2121
code-coverage:
2222
name: Coverage
2323
needs: test
24-
uses: WebFiori/workflows/.github/workflows/coverage-codecov.yaml@main
24+
uses: WebFiori/workflows/.github/workflows/coverage-codecov.yaml@v1.2.1
2525
with:
2626
php-version: '8.3'
2727
coverage-file: 'php-8.3-coverage.xml'

.github/workflows/php84.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@ on:
88
jobs:
99
test:
1010
name: Run Tests
11-
uses: WebFiori/workflows/.github/workflows/test-php.yaml@main
11+
uses: WebFiori/workflows/.github/workflows/test-php.yaml@v1.2.1
1212
with:
1313
php-version: '8.4'
1414
phpunit-config: "tests/phpunit10.xml"
1515

1616
code-coverage:
1717
name: Coverage
1818
needs: test
19-
uses: WebFiori/workflows/.github/workflows/coverage-codecov.yaml@main
19+
uses: WebFiori/workflows/.github/workflows/coverage-codecov.yaml@v1.2.1
2020
with:
2121
php-version: '8.4'
2222
coverage-file: 'php-8.4-coverage.xml'

.github/workflows/php85.yaml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: Build PHP 8.5
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
branches: [ main ]
8+
jobs:
9+
test:
10+
name: Run Tests
11+
uses: WebFiori/workflows/.github/workflows/test-php.yaml@v1.2.1
12+
with:
13+
php-version: '8.5'
14+
phpunit-config: "tests/phpunit10.xml"
15+
16+
code-coverage:
17+
name: Coverage
18+
needs: test
19+
uses: WebFiori/workflows/.github/workflows/coverage-codecov.yaml@v1.2.1
20+
with:
21+
php-version: '8.5'
22+
coverage-file: 'php-8.5-coverage.xml'
23+
secrets:
24+
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
25+
26+
27+

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
Class library that can help in writing command line based applications with minimum dependencies using PHP.
33

44
<p align="center">
5-
<a target="_blank" href="https://github.com/WebFiori/cli/actions/workflows/php84.yaml">
6-
<img src="https://github.com/WebFiori/cli/actions/workflows/php84.yaml/badge.svg?branch=main">
5+
<a target="_blank" href="https://github.com/WebFiori/cli/actions/workflows/php85.yaml">
6+
<img src="https://github.com/WebFiori/cli/actions/workflows/php85.yaml/badge.svg?branch=main">
77
</a>
88
<a href="https://codecov.io/gh/WebFiori/cli">
99
<img src="https://codecov.io/gh/WebFiori/cli/branch/main/graph/badge.svg" />
@@ -56,6 +56,7 @@ Class library that can help in writing command line based applications with mini
5656
| <a target="_blank" href="https://github.com/WebFiori/cli/actions/workflows/php82.yaml"><img src="https://github.com/WebFiori/cli/actions/workflows/php82.yaml/badge.svg?branch=main"></a> |
5757
| <a target="_blank" href="https://github.com/WebFiori/cli/actions/workflows/php83.yaml"><img src="https://github.com/WebFiori/cli/actions/workflows/php83.yaml/badge.svg?branch=main"></a> |
5858
| <a target="_blank" href="https://github.com/WebFiori/cli/actions/workflows/php84.yaml"><img src="https://github.com/WebFiori/cli/actions/workflows/php84.yaml/badge.svg?branch=main"></a> |
59+
| <a target="_blank" href="https://github.com/WebFiori/cli/actions/workflows/php85.yaml"><img src="https://github.com/WebFiori/cli/actions/workflows/php85.yaml/badge.svg?branch=main"></a> |
5960

6061
## Features
6162
* **Easy Command Creation**: Simple class-based approach to building CLI commands
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
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+
}

WebFiori/Cli/Table/TableData.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ public static function fromCsv(string $csv, bool $hasHeaders = true, string $del
9595
continue;
9696
}
9797

98-
$row = str_getcsv($line, $delimiter);
98+
$row = str_getcsv($line, $delimiter, '"', '\\');
9999

100100
if ($hasHeaders && $headers === null) {
101101
$headers = $row;

0 commit comments

Comments
 (0)