From e6e47bc0b2f7b0f47e6b4379872b21548ee92498 Mon Sep 17 00:00:00 2001 From: rtcoder Date: Wed, 17 Jun 2026 13:47:53 +0200 Subject: [PATCH 1/2] Add CLI create generators --- README.md | 22 +++++ REFACTORING.md | 1 + composer.json | 1 + docs/index.html | 17 +++- src/Console/Commands/CreateCommand.php | 68 ++++++++++++++++ src/Console/Commands/CreateController.php | 60 ++++++++++++++ src/Console/Commands/CreateDto.php | 62 ++++++++++++++ src/Console/Commands/CreateMiddleware.php | 59 ++++++++++++++ src/Console/Commands/CreateModel.php | 51 ++++++++++++ src/Console/Commands/CreateModule.php | 99 +++++++++++++++++++++++ src/Console/Commands/CreateService.php | 51 ++++++++++++ src/Console/Generator/FileGenerator.php | 52 ++++++++++++ src/Console/Generator/GeneratesFiles.php | 74 +++++++++++++++++ src/Console/Generator/NameFormatter.php | 48 +++++++++++ tests/Feature/ConsoleCreateTest.php | 72 +++++++++++++++++ tests/Support/TestSupport.php | 54 +++++++++++++ 16 files changed, 790 insertions(+), 1 deletion(-) create mode 100644 src/Console/Commands/CreateCommand.php create mode 100644 src/Console/Commands/CreateController.php create mode 100644 src/Console/Commands/CreateDto.php create mode 100644 src/Console/Commands/CreateMiddleware.php create mode 100644 src/Console/Commands/CreateModel.php create mode 100644 src/Console/Commands/CreateModule.php create mode 100644 src/Console/Commands/CreateService.php create mode 100644 src/Console/Generator/FileGenerator.php create mode 100644 src/Console/Generator/GeneratesFiles.php create mode 100644 src/Console/Generator/NameFormatter.php create mode 100644 tests/Feature/ConsoleCreateTest.php diff --git a/README.md b/README.md index 46c5633..78b66f4 100644 --- a/README.md +++ b/README.md @@ -262,6 +262,26 @@ Run the example module command: php shift.php health ``` +Generate module scaffolding: + +```sh +php shift.php create:module Billing +``` + +Generate module-owned classes: + +```sh +php shift.php create:controller --module=Billing InvoiceController +php shift.php create:controller Billing:InvoiceController +php shift.php create:model Billing:Invoice +php shift.php create:service Billing:Invoice +php shift.php create:command Billing:SyncInvoices +php shift.php create:middleware Billing:Audit +php shift.php create:dto Billing:CreateInvoice +``` + +Generator commands normalize module and class names to PHP class conventions. Missing suffixes are added for controllers, services, middleware, and DTOs. + ## Modules ShiftPHP supports a modular monolith structure under `application/modules`. @@ -276,6 +296,8 @@ application/modules/Health/ └── Commands/ ``` +Generated models, middleware, and DTOs live under `Models/`, `Middleware/`, and `Dto/` inside the target module. + A module registers itself through `Module.php`: ```php diff --git a/REFACTORING.md b/REFACTORING.md index 9498e97..b0fbf7b 100644 --- a/REFACTORING.md +++ b/REFACTORING.md @@ -26,6 +26,7 @@ ShiftPHP is moving toward an API-only modular monolith. View templates, compiled - [x] Module configuration loading. - [x] Module lifecycle hooks, for example `boot()` after service registration. - [x] Framework source moved to `src/` for package split preparation. +- [x] CLI create generators for modules and module-owned classes. - [x] Removal of view storage and example page assets from runtime. - [x] Removal of legacy `application/controllers` and `application/routes.php`. - [x] Domain-oriented framework namespaces: diff --git a/composer.json b/composer.json index 3689d3f..3443953 100644 --- a/composer.json +++ b/composer.json @@ -20,6 +20,7 @@ "autoload": { "psr-4": { "Shift\\": "src", + "Console\\Commands\\": "src/Console/Commands", "Modules\\": "application/modules" } } diff --git a/docs/index.html b/docs/index.html index 7b93dbb..3ce6eba 100644 --- a/docs/index.html +++ b/docs/index.html @@ -111,6 +111,8 @@

Modules

|-- Services/ `-- Commands/ +

Generator commands can also create Models/, Middleware/, and Dto/ directories when those artifacts are added.

+

Every module boundary implements Shift\Modules\ModuleInterface. Most modules can extend Shift\Modules\AbstractModule and override only the methods they need.

namespace Modules\Health;
@@ -386,11 +388,24 @@ 

Service Container

CLI

-

The CLI entry point is shift.php. Built-in commands live under Shift\Console\Commands, and module commands are loaded from module command mappings.

+

The CLI entry point is shift.php. Built-in commands live under Console\Commands, and module commands are loaded from module command mappings.

php shift.php route:list
 php shift.php health
+

Create commands scaffold modules and module-owned classes:

+ +
php shift.php create:module Billing
+php shift.php create:controller --module=Billing InvoiceController
+php shift.php create:controller Billing:InvoiceController
+php shift.php create:model Billing:Invoice
+php shift.php create:service Billing:Invoice
+php shift.php create:command Billing:SyncInvoices
+php shift.php create:middleware Billing:Audit
+php shift.php create:dto Billing:CreateInvoice
+ +

Controller, service, middleware, and DTO generators add the expected class suffix when it is missing. Commands accept either --module=Billing Name or Billing:Name.

+

Commands implement Shift\Console\CommandInterface.

diff --git a/src/Console/Commands/CreateCommand.php b/src/Console/Commands/CreateCommand.php new file mode 100644 index 0000000..3b751ba --- /dev/null +++ b/src/Console/Commands/CreateCommand.php @@ -0,0 +1,68 @@ +moduleAndClassFromArgs($args); + } catch (\InvalidArgumentException) { + $this->cli->error($this->getHelp()); + return; + } + + $class = NameFormatter::className($rawName); + $path = $this->modulePath($module) . '/Commands/' . $class . '.php'; + + $this->writeAndReport($path, $this->stub($module, $class, NameFormatter::commandName($class))); + } + + public function getHelp(): string + { + return 'Usage: php shift.php create:command --module={name} {CommandName}'; + } + + public function getDescription(): string + { + return 'Create a module CLI command.'; + } + + private function stub(string $module, string $class, string $command): string + { + return <<success('Command executed.'); + } + + public function getHelp(): string + { + return 'Usage: php shift.php {$command}'; + } + + public function getDescription(): string + { + return 'Module command.'; + } +} + +PHP; + } +} diff --git a/src/Console/Commands/CreateController.php b/src/Console/Commands/CreateController.php new file mode 100644 index 0000000..db1dd33 --- /dev/null +++ b/src/Console/Commands/CreateController.php @@ -0,0 +1,60 @@ +moduleAndClassFromArgs($args); + } catch (\InvalidArgumentException) { + $this->cli->error($this->getHelp()); + return; + } + + $class = NameFormatter::className($rawName, 'Controller'); + $path = $this->modulePath($module) . '/Controllers/' . $class . '.php'; + + $this->writeAndReport($path, $this->stub($module, $class)); + } + + public function getHelp(): string + { + return 'Usage: php shift.php create:controller --module={name} {ControllerName}'; + } + + public function getDescription(): string + { + return 'Create a module controller.'; + } + + private function stub(string $module, string $class): string + { + return <<json([ + 'status' => 'ok', + ]); + } +} + +PHP; + } +} diff --git a/src/Console/Commands/CreateDto.php b/src/Console/Commands/CreateDto.php new file mode 100644 index 0000000..9c91c67 --- /dev/null +++ b/src/Console/Commands/CreateDto.php @@ -0,0 +1,62 @@ +moduleAndClassFromArgs($args); + } catch (\InvalidArgumentException) { + $this->cli->error($this->getHelp()); + return; + } + + $class = NameFormatter::className($rawName, 'Dto'); + $path = $this->modulePath($module) . '/Dto/' . $class . '.php'; + + $this->writeAndReport($path, $this->stub($module, $class)); + } + + public function getHelp(): string + { + return 'Usage: php shift.php create:dto --module={name} {DtoName}'; + } + + public function getDescription(): string + { + return 'Create a module request DTO.'; + } + + private function stub(string $module, string $class): string + { + return <<moduleAndClassFromArgs($args); + } catch (\InvalidArgumentException) { + $this->cli->error($this->getHelp()); + return; + } + + $class = NameFormatter::className($rawName, 'Middleware'); + $path = $this->modulePath($module) . '/Middleware/' . $class . '.php'; + + $this->writeAndReport($path, $this->stub($module, $class)); + } + + public function getHelp(): string + { + return 'Usage: php shift.php create:middleware --module={name} {MiddlewareName}'; + } + + public function getDescription(): string + { + return 'Create a module middleware.'; + } + + private function stub(string $module, string $class): string + { + return <<moduleAndClassFromArgs($args); + } catch (\InvalidArgumentException) { + $this->cli->error($this->getHelp()); + return; + } + + $class = NameFormatter::className($rawName); + $path = $this->modulePath($module) . '/Models/' . $class . '.php'; + + $this->writeAndReport($path, $this->stub($module, $class)); + } + + public function getHelp(): string + { + return 'Usage: php shift.php create:model --module={name} {ModelName}'; + } + + public function getDescription(): string + { + return 'Create a module model.'; + } + + private function stub(string $module, string $class): string + { + return <<cli->error($this->getHelp()); + return; + } + + $module = NameFormatter::moduleName($rawName); + $slug = NameFormatter::slug($rawName); + $path = $this->modulePath($module); + + foreach (['Commands', 'Controllers', 'Services'] as $directory) { + $this->files->ensureDirectory($path . '/' . $directory); + } + + $this->writeAndReport($path . '/Module.php', $this->moduleStub($module, $slug)); + $this->writeAndReport($path . '/config.php', $this->configStub($slug)); + } + + public function getHelp(): string + { + return 'Usage: php shift.php create:module {name}'; + } + + public function getDescription(): string + { + return 'Create a new Shift module.'; + } + + private function moduleStub(string $module, string $slug): string + { + return <<load(\$router, [ + ]); + } + + public function getCommandMappings(): array + { + return [ + [ + 'dir' => __DIR__ . '/Commands/', + 'namespace' => 'Modules\\\\{$module}\\\\Commands\\\\', + ], + ]; + } +} + +PHP; + } + + private function configStub(string $slug): string + { + return << true, + 'module' => '{$slug}', +]; + +PHP; + } +} diff --git a/src/Console/Commands/CreateService.php b/src/Console/Commands/CreateService.php new file mode 100644 index 0000000..d199cda --- /dev/null +++ b/src/Console/Commands/CreateService.php @@ -0,0 +1,51 @@ +moduleAndClassFromArgs($args); + } catch (\InvalidArgumentException) { + $this->cli->error($this->getHelp()); + return; + } + + $class = NameFormatter::className($rawName, 'Service'); + $path = $this->modulePath($module) . '/Services/' . $class . '.php'; + + $this->writeAndReport($path, $this->stub($module, $class)); + } + + public function getHelp(): string + { + return 'Usage: php shift.php create:service --module={name} {ServiceName}'; + } + + public function getDescription(): string + { + return 'Create a module service.'; + } + + private function stub(string $module, string $class): string + { + return << */ + private array $created = []; + + /** @var list */ + private array $skipped = []; + + public function ensureDirectory(string $path): void + { + if (is_dir($path)) { + return; + } + + mkdir($path, 0775, true); + $this->created[] = $path; + } + + public function writeFile(string $path, string $content): void + { + $directory = dirname($path); + $this->ensureDirectory($directory); + + if (file_exists($path)) { + $this->skipped[] = $path; + return; + } + + file_put_contents($path, $content); + $this->created[] = $path; + } + + /** + * @return list + */ + public function created(): array + { + return $this->created; + } + + /** + * @return list + */ + public function skipped(): array + { + return $this->skipped; + } +} diff --git a/src/Console/Generator/GeneratesFiles.php b/src/Console/Generator/GeneratesFiles.php new file mode 100644 index 0000000..a75fc43 --- /dev/null +++ b/src/Console/Generator/GeneratesFiles.php @@ -0,0 +1,74 @@ +files = new FileGenerator(); + $this->cli = new Cli(); + } + + /** + * @param list $args + * @return array{0: string, 1: string} + */ + protected function moduleAndClassFromArgs(array $args): array + { + $module = null; + $name = null; + + foreach ($args as $arg) { + if (!is_string($arg) || $arg === '') { + continue; + } + + if (str_starts_with($arg, '--module=')) { + $module = substr($arg, 9); + continue; + } + + if ($module === null && str_contains($arg, ':')) { + [$module, $name] = explode(':', $arg, 2); + continue; + } + + $name ??= $arg; + } + + if ($module === null || $module === '' || $name === null || $name === '') { + throw new \InvalidArgumentException('Module and class name are required.'); + } + + return [ + NameFormatter::moduleName($module), + $name, + ]; + } + + protected function modulePath(string $module): string + { + return rtrim($this->modulesPath, '/') . '/' . $module; + } + + protected function writeAndReport(string $path, string $content): void + { + $beforeSkipped = count($this->files->skipped()); + $this->files->writeFile($path, $content); + + if (count($this->files->skipped()) > $beforeSkipped) { + $this->cli->warning('Skipped existing file: ' . $path); + return; + } + + $this->cli->success('Created: ' . $path); + } +} diff --git a/src/Console/Generator/NameFormatter.php b/src/Console/Generator/NameFormatter.php new file mode 100644 index 0000000..aa3f33e --- /dev/null +++ b/src/Console/Generator/NameFormatter.php @@ -0,0 +1,48 @@ + strtolower($part), $parts); + + return implode(':', $parts); + } + + public static function slug(string $name): string + { + $parts = preg_split('/[^a-zA-Z0-9]+/', $name, -1, PREG_SPLIT_NO_EMPTY) ?: []; + $parts = array_map(static fn (string $part): string => strtolower($part), $parts); + + return implode('-', $parts); + } + + private static function studly(string $name): string + { + $parts = preg_split('/[^a-zA-Z0-9]+/', $name, -1, PREG_SPLIT_NO_EMPTY) ?: []; + $parts = array_map(static fn (string $part): string => ucfirst($part), $parts); + + return implode('', $parts); + } +} diff --git a/tests/Feature/ConsoleCreateTest.php b/tests/Feature/ConsoleCreateTest.php new file mode 100644 index 0000000..cbd9525 --- /dev/null +++ b/tests/Feature/ConsoleCreateTest.php @@ -0,0 +1,72 @@ + function (): void { + $modulesPath = makeTempModulesPath(); + + try { + (new CreateModule($modulesPath))->execute('billing'); + + assertFileExists($modulesPath . '/Billing/Module.php', 'Module.php should be created.'); + assertFileExists($modulesPath . '/Billing/config.php', 'config.php should be created.'); + assertDirectoryExists($modulesPath . '/Billing/Commands', 'Commands directory should be created.'); + assertDirectoryExists($modulesPath . '/Billing/Controllers', 'Controllers directory should be created.'); + assertDirectoryExists($modulesPath . '/Billing/Services', 'Services directory should be created.'); + + $module = file_get_contents($modulesPath . '/Billing/Module.php'); + $config = file_get_contents($modulesPath . '/Billing/config.php'); + assertStringContains('namespace Modules\\Billing;', $module, 'Module namespace should match module name.'); + assertStringContains("'namespace' => 'Modules\\\\Billing\\\\Commands\\\\'", $module, 'Module should expose command mappings.'); + assertStringContains("'module' => 'billing'", $config, 'Module config should use the module slug.'); + } finally { + removeDirectory(dirname($modulesPath)); + } + }, + 'create:controller supports module option and inline module syntax' => function (): void { + $modulesPath = makeTempModulesPath(); + + try { + (new CreateController($modulesPath))->execute('--module=billing', 'invoice'); + (new CreateController($modulesPath))->execute('billing:PaymentController'); + + assertFileExists($modulesPath . '/Billing/Controllers/InvoiceController.php', 'Controller suffix should be added.'); + assertFileExists($modulesPath . '/Billing/Controllers/PaymentController.php', 'Inline module syntax should be supported.'); + + $controller = file_get_contents($modulesPath . '/Billing/Controllers/InvoiceController.php'); + assertStringContains('namespace Modules\\Billing\\Controllers;', $controller, 'Controller namespace should match module.'); + assertStringContains('class InvoiceController extends Controller', $controller, 'Controller class should extend base controller.'); + } finally { + removeDirectory(dirname($modulesPath)); + } + }, + 'create artifact commands write module-owned classes' => function (): void { + $modulesPath = makeTempModulesPath(); + + try { + (new CreateModel($modulesPath))->execute('billing:Invoice'); + (new CreateService($modulesPath))->execute('billing:Invoice'); + (new CreateCommand($modulesPath))->execute('billing:SyncInvoices'); + (new CreateMiddleware($modulesPath))->execute('billing:Audit'); + (new CreateDto($modulesPath))->execute('billing:CreateInvoice'); + + assertFileExists($modulesPath . '/Billing/Models/Invoice.php', 'Model should be created.'); + assertFileExists($modulesPath . '/Billing/Services/InvoiceService.php', 'Service suffix should be added.'); + assertFileExists($modulesPath . '/Billing/Commands/SyncInvoices.php', 'Command should be created.'); + assertFileExists($modulesPath . '/Billing/Middleware/AuditMiddleware.php', 'Middleware suffix should be added.'); + assertFileExists($modulesPath . '/Billing/Dto/CreateInvoiceDto.php', 'DTO suffix should be added.'); + + $command = file_get_contents($modulesPath . '/Billing/Commands/SyncInvoices.php'); + assertStringContains('return \'Usage: php shift.php sync:invoices\';', $command, 'Generated command help should use CLI command syntax.'); + } finally { + removeDirectory(dirname($modulesPath)); + } + }, +]; diff --git a/tests/Support/TestSupport.php b/tests/Support/TestSupport.php index 063b195..31c78ae 100644 --- a/tests/Support/TestSupport.php +++ b/tests/Support/TestSupport.php @@ -45,3 +45,57 @@ function makeRequest(string $method, string $uri, string $body = '', array $quer $body ); } + +function assertFileExists(string $path, string $message): void +{ + if (!is_file($path)) { + throw new RuntimeException($message . "\nMissing file: {$path}"); + } +} + +function assertDirectoryExists(string $path, string $message): void +{ + if (!is_dir($path)) { + throw new RuntimeException($message . "\nMissing directory: {$path}"); + } +} + +function assertStringContains(string $needle, string|false $haystack, string $message): void +{ + if (!is_string($haystack) || !str_contains($haystack, $needle)) { + throw new RuntimeException($message . "\nNeedle: {$needle}\nHaystack: " . var_export($haystack, true)); + } +} + +function makeTempModulesPath(): string +{ + $root = sys_get_temp_dir() . '/shift-create-' . bin2hex(random_bytes(6)); + $modules = $root . '/modules'; + + mkdir($modules, 0775, true); + + return $modules; +} + +function removeDirectory(string $path): void +{ + if (!is_dir($path)) { + return; + } + + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS), + RecursiveIteratorIterator::CHILD_FIRST + ); + + foreach ($iterator as $file) { + if ($file->isDir()) { + rmdir($file->getPathname()); + continue; + } + + unlink($file->getPathname()); + } + + rmdir($path); +} From 3fb40fb3645f460737d27a2bf04100301a18c0c4 Mon Sep 17 00:00:00 2001 From: rtcoder Date: Wed, 17 Jun 2026 14:00:18 +0200 Subject: [PATCH 2/2] Extract CLI generator stubs --- README.md | 1 + REFACTORING.md | 2 +- docs/index.html | 1 + src/Console/Commands/CreateCommand.php | 36 ++---------- src/Console/Commands/CreateController.php | 27 ++------- src/Console/Commands/CreateDto.php | 29 ++-------- src/Console/Commands/CreateMiddleware.php | 26 ++------- src/Console/Commands/CreateModel.php | 18 ++---- src/Console/Commands/CreateModule.php | 64 +++------------------ src/Console/Commands/CreateService.php | 18 ++---- src/Console/Generator/GeneratesFiles.php | 11 ++++ src/Console/Generator/StubRenderer.php | 35 +++++++++++ src/Console/Generator/stubs/command.stub | 24 ++++++++ src/Console/Generator/stubs/config.stub | 6 ++ src/Console/Generator/stubs/controller.stub | 16 ++++++ src/Console/Generator/stubs/dto.stub | 18 ++++++ src/Console/Generator/stubs/middleware.stub | 15 +++++ src/Console/Generator/stubs/model.stub | 7 +++ src/Console/Generator/stubs/module.stub | 36 ++++++++++++ src/Console/Generator/stubs/service.stub | 7 +++ 20 files changed, 210 insertions(+), 187 deletions(-) create mode 100644 src/Console/Generator/StubRenderer.php create mode 100644 src/Console/Generator/stubs/command.stub create mode 100644 src/Console/Generator/stubs/config.stub create mode 100644 src/Console/Generator/stubs/controller.stub create mode 100644 src/Console/Generator/stubs/dto.stub create mode 100644 src/Console/Generator/stubs/middleware.stub create mode 100644 src/Console/Generator/stubs/model.stub create mode 100644 src/Console/Generator/stubs/module.stub create mode 100644 src/Console/Generator/stubs/service.stub diff --git a/README.md b/README.md index 78b66f4..e0e65a6 100644 --- a/README.md +++ b/README.md @@ -281,6 +281,7 @@ php shift.php create:dto Billing:CreateInvoice ``` Generator commands normalize module and class names to PHP class conventions. Missing suffixes are added for controllers, services, middleware, and DTOs. +Generator templates live in `src/Console/Generator/stubs`. ## Modules diff --git a/REFACTORING.md b/REFACTORING.md index b0fbf7b..54ed3be 100644 --- a/REFACTORING.md +++ b/REFACTORING.md @@ -26,7 +26,7 @@ ShiftPHP is moving toward an API-only modular monolith. View templates, compiled - [x] Module configuration loading. - [x] Module lifecycle hooks, for example `boot()` after service registration. - [x] Framework source moved to `src/` for package split preparation. -- [x] CLI create generators for modules and module-owned classes. +- [x] CLI create generators for modules and module-owned classes with file-based stubs. - [x] Removal of view storage and example page assets from runtime. - [x] Removal of legacy `application/controllers` and `application/routes.php`. - [x] Domain-oriented framework namespaces: diff --git a/docs/index.html b/docs/index.html index 3ce6eba..9439dd4 100644 --- a/docs/index.html +++ b/docs/index.html @@ -405,6 +405,7 @@

CLI

php shift.php create:dto Billing:CreateInvoice

Controller, service, middleware, and DTO generators add the expected class suffix when it is missing. Commands accept either --module=Billing Name or Billing:Name.

+

Generator templates live in src/Console/Generator/stubs.

Commands implement Shift\Console\CommandInterface.

diff --git a/src/Console/Commands/CreateCommand.php b/src/Console/Commands/CreateCommand.php index 3b751ba..68588aa 100644 --- a/src/Console/Commands/CreateCommand.php +++ b/src/Console/Commands/CreateCommand.php @@ -22,7 +22,11 @@ public function execute(mixed ...$args): void $class = NameFormatter::className($rawName); $path = $this->modulePath($module) . '/Commands/' . $class . '.php'; - $this->writeAndReport($path, $this->stub($module, $class, NameFormatter::commandName($class))); + $this->writeAndReport($path, $this->renderStub('command', [ + 'module' => $module, + 'class' => $class, + 'command' => NameFormatter::commandName($class), + ])); } public function getHelp(): string @@ -35,34 +39,4 @@ public function getDescription(): string return 'Create a module CLI command.'; } - private function stub(string $module, string $class, string $command): string - { - return <<success('Command executed.'); - } - - public function getHelp(): string - { - return 'Usage: php shift.php {$command}'; - } - - public function getDescription(): string - { - return 'Module command.'; - } -} - -PHP; - } } diff --git a/src/Console/Commands/CreateController.php b/src/Console/Commands/CreateController.php index db1dd33..61bc81c 100644 --- a/src/Console/Commands/CreateController.php +++ b/src/Console/Commands/CreateController.php @@ -22,7 +22,10 @@ public function execute(mixed ...$args): void $class = NameFormatter::className($rawName, 'Controller'); $path = $this->modulePath($module) . '/Controllers/' . $class . '.php'; - $this->writeAndReport($path, $this->stub($module, $class)); + $this->writeAndReport($path, $this->renderStub('controller', [ + 'module' => $module, + 'class' => $class, + ])); } public function getHelp(): string @@ -35,26 +38,4 @@ public function getDescription(): string return 'Create a module controller.'; } - private function stub(string $module, string $class): string - { - return <<json([ - 'status' => 'ok', - ]); - } -} - -PHP; - } } diff --git a/src/Console/Commands/CreateDto.php b/src/Console/Commands/CreateDto.php index 9c91c67..8c025b8 100644 --- a/src/Console/Commands/CreateDto.php +++ b/src/Console/Commands/CreateDto.php @@ -22,7 +22,10 @@ public function execute(mixed ...$args): void $class = NameFormatter::className($rawName, 'Dto'); $path = $this->modulePath($module) . '/Dto/' . $class . '.php'; - $this->writeAndReport($path, $this->stub($module, $class)); + $this->writeAndReport($path, $this->renderStub('dto', [ + 'module' => $module, + 'class' => $class, + ])); } public function getHelp(): string @@ -35,28 +38,4 @@ public function getDescription(): string return 'Create a module request DTO.'; } - private function stub(string $module, string $class): string - { - return <<modulePath($module) . '/Middleware/' . $class . '.php'; - $this->writeAndReport($path, $this->stub($module, $class)); + $this->writeAndReport($path, $this->renderStub('middleware', [ + 'module' => $module, + 'class' => $class, + ])); } public function getHelp(): string @@ -35,25 +38,4 @@ public function getDescription(): string return 'Create a module middleware.'; } - private function stub(string $module, string $class): string - { - return <<modulePath($module) . '/Models/' . $class . '.php'; - $this->writeAndReport($path, $this->stub($module, $class)); + $this->writeAndReport($path, $this->renderStub('model', [ + 'module' => $module, + 'class' => $class, + ])); } public function getHelp(): string @@ -35,17 +38,4 @@ public function getDescription(): string return 'Create a module model.'; } - private function stub(string $module, string $class): string - { - return <<files->ensureDirectory($path . '/' . $directory); } - $this->writeAndReport($path . '/Module.php', $this->moduleStub($module, $slug)); - $this->writeAndReport($path . '/config.php', $this->configStub($slug)); + $this->writeAndReport($path . '/Module.php', $this->renderStub('module', [ + 'module' => $module, + 'slug' => $slug, + ])); + $this->writeAndReport($path . '/config.php', $this->renderStub('config', [ + 'slug' => $slug, + ])); } public function getHelp(): string @@ -41,59 +46,4 @@ public function getDescription(): string return 'Create a new Shift module.'; } - private function moduleStub(string $module, string $slug): string - { - return <<load(\$router, [ - ]); - } - - public function getCommandMappings(): array - { - return [ - [ - 'dir' => __DIR__ . '/Commands/', - 'namespace' => 'Modules\\\\{$module}\\\\Commands\\\\', - ], - ]; - } -} - -PHP; - } - - private function configStub(string $slug): string - { - return << true, - 'module' => '{$slug}', -]; - -PHP; - } } diff --git a/src/Console/Commands/CreateService.php b/src/Console/Commands/CreateService.php index d199cda..30bb5fb 100644 --- a/src/Console/Commands/CreateService.php +++ b/src/Console/Commands/CreateService.php @@ -22,7 +22,10 @@ public function execute(mixed ...$args): void $class = NameFormatter::className($rawName, 'Service'); $path = $this->modulePath($module) . '/Services/' . $class . '.php'; - $this->writeAndReport($path, $this->stub($module, $class)); + $this->writeAndReport($path, $this->renderStub('service', [ + 'module' => $module, + 'class' => $class, + ])); } public function getHelp(): string @@ -35,17 +38,4 @@ public function getDescription(): string return 'Create a module service.'; } - private function stub(string $module, string $class): string - { - return <<files = new FileGenerator(); + $this->stubs = new StubRenderer(); $this->cli = new Cli(); } @@ -71,4 +74,12 @@ protected function writeAndReport(string $path, string $content): void $this->cli->success('Created: ' . $path); } + + /** + * @param array $variables + */ + protected function renderStub(string $stub, array $variables): string + { + return $this->stubs->render($stub, $variables); + } } diff --git a/src/Console/Generator/StubRenderer.php b/src/Console/Generator/StubRenderer.php new file mode 100644 index 0000000..ee28133 --- /dev/null +++ b/src/Console/Generator/StubRenderer.php @@ -0,0 +1,35 @@ + $variables + */ + public function render(string $stub, array $variables): string + { + $path = rtrim($this->stubPath, '/') . '/' . $stub . '.stub'; + + if (!is_file($path)) { + throw new \RuntimeException("Stub {$stub} not found."); + } + + $content = file_get_contents($path); + + if (!is_string($content)) { + throw new \RuntimeException("Stub {$stub} cannot be read."); + } + + foreach ($variables as $name => $value) { + $content = str_replace('{{ ' . $name . ' }}', $value, $content); + } + + return $content; + } +} diff --git a/src/Console/Generator/stubs/command.stub b/src/Console/Generator/stubs/command.stub new file mode 100644 index 0000000..5e1bf27 --- /dev/null +++ b/src/Console/Generator/stubs/command.stub @@ -0,0 +1,24 @@ +success('Command executed.'); + } + + public function getHelp(): string + { + return 'Usage: php shift.php {{ command }}'; + } + + public function getDescription(): string + { + return 'Module command.'; + } +} diff --git a/src/Console/Generator/stubs/config.stub b/src/Console/Generator/stubs/config.stub new file mode 100644 index 0000000..4e0706a --- /dev/null +++ b/src/Console/Generator/stubs/config.stub @@ -0,0 +1,6 @@ + true, + 'module' => '{{ slug }}', +]; diff --git a/src/Console/Generator/stubs/controller.stub b/src/Console/Generator/stubs/controller.stub new file mode 100644 index 0000000..1b29870 --- /dev/null +++ b/src/Console/Generator/stubs/controller.stub @@ -0,0 +1,16 @@ +json([ + 'status' => 'ok', + ]); + } +} diff --git a/src/Console/Generator/stubs/dto.stub b/src/Console/Generator/stubs/dto.stub new file mode 100644 index 0000000..6507109 --- /dev/null +++ b/src/Console/Generator/stubs/dto.stub @@ -0,0 +1,18 @@ +load($router, [ + ]); + } + + public function getCommandMappings(): array + { + return [ + [ + 'dir' => __DIR__ . '/Commands/', + 'namespace' => 'Modules\\{{ module }}\\Commands\\', + ], + ]; + } +} diff --git a/src/Console/Generator/stubs/service.stub b/src/Console/Generator/stubs/service.stub new file mode 100644 index 0000000..b60afb8 --- /dev/null +++ b/src/Console/Generator/stubs/service.stub @@ -0,0 +1,7 @@ +