From 71f8600c4349ebaae56b744b8217b752ee99276a Mon Sep 17 00:00:00 2001 From: philippe Date: Tue, 3 Feb 2026 15:41:42 +0100 Subject: [PATCH 1/6] Refactor generator command helpers --- src/Console/GeneratorCommand.php | 139 +++++++++------------- src/Console/Utils/GeneratorFileEditor.php | 63 ++++++++++ 2 files changed, 117 insertions(+), 85 deletions(-) create mode 100644 src/Console/Utils/GeneratorFileEditor.php diff --git a/src/Console/GeneratorCommand.php b/src/Console/GeneratorCommand.php index 7289db8a1..1cedbeb0b 100644 --- a/src/Console/GeneratorCommand.php +++ b/src/Console/GeneratorCommand.php @@ -3,6 +3,7 @@ namespace Code16\Sharp\Console; use Code16\Sharp\Config\SharpConfigBuilder; +use Code16\Sharp\Console\Utils\GeneratorFileEditor; use Code16\Sharp\Utils\Links\LinkToDashboard; use Code16\Sharp\Utils\Links\LinkToEntityList; use Code16\Sharp\Utils\Links\LinkToSingleShowPage; @@ -102,13 +103,7 @@ protected function entityStatePrompt(): void $this->components->twoColumnDetail('Entity state', $this->getSharpRootNamespace().'\\'.$entityStatePath.'\\'.$name.'EntityState.php'); - $listClass = $this->getSharpRootNamespace().'\\'.Str::plural($entityName).'\\'.$entityName.'EntityList'; - - if (! class_exists($listClass)) { - $listClass = $this->getSharpRootNamespace().'\\'.Str::plural($entityName).'\\'.$entityName.'List'; - } - - if (class_exists($listClass)) { + if ($listClass = $this->resolveEntityListClass($entityName)) { $this->addNewEntityStateToListOrShowPage( 'List', $name.'EntityState', @@ -119,9 +114,7 @@ protected function entityStatePrompt(): void $this->components->info(sprintf('The Entity State was successfully added to the related Entity List (%s).', $entityName.'EntityList')); } - $showClass = $this->getSharpRootNamespace().'\\'.Str::plural($entityName).'\\'.$entityName.'Show'; - - if (class_exists($showClass)) { + if ($showClass = $this->resolveEntityShowClass($entityName)) { $this->addNewEntityStateToListOrShowPage( 'Show', $name.'EntityState', @@ -189,13 +182,7 @@ protected function filterPrompt(): void $this->components->info('Your Filter has been created.'); - $listClass = $this->getSharpRootNamespace().'\\'.Str::plural($entityName).'\\'.$entityName.'EntityList'; - - if (! class_exists($listClass)) { - $listClass = $this->getSharpRootNamespace().'\\'.Str::plural($entityName).'\\'.$entityName.'List'; - } - - if (class_exists($listClass)) { + if ($listClass = $this->resolveEntityListClass($entityName)) { $this->addNewItemToAListOfFilters( $name.'Filter', $this->getSharpRootNamespace().'\\'.$filterPath.'\\', @@ -255,13 +242,7 @@ protected function commandPrompt(): void $this->components->info('Your Command has been created.'); - $listClass = $this->getSharpRootNamespace().'\\'.Str::plural($entityName).'\\'.$entityName.'EntityList'; - - if (! class_exists($listClass)) { - $listClass = $this->getSharpRootNamespace().'\\'.Str::plural($entityName).'\\'.$entityName.'List'; - } - - if (class_exists($listClass)) { + if ($listClass = $this->resolveEntityListClass($entityName)) { $this->addNewItemToAListOfCommands( $commandType, $name.'Command', @@ -272,9 +253,9 @@ protected function commandPrompt(): void $this->components->info(sprintf('The Command has been successfully added to the related Entity List (%s).', $entityName.'EntityList')); } - $showClass = $this->getSharpRootNamespace().'\\'.Str::plural($entityName).'\\'.$entityName.'Show'; + $showClass = $this->resolveEntityShowClass($entityName); - if ($commandType === 'Instance' && class_exists($showClass)) { + if ($commandType === 'Instance' && $showClass) { $this->addNewItemToAListOfCommands( $commandType, $name.'Command', @@ -349,11 +330,7 @@ protected function reorderHandlerPrompt(): void ); $model = $modelNamespace.'\\'.$modelName; - $listClass = $this->getSharpRootNamespace().'\\'.Str::plural($entityName).'\\'.$entityName.'EntityList'; - - if (! class_exists($listClass)) { - $listClass = $this->getSharpRootNamespace().'\\'.Str::plural($entityName).'\\'.$entityName.'List'; - } + $listClass = $this->resolveEntityListClass($entityName); $isSimple = confirm( label: 'Use the simple Eloquent implementation based on a reorder attribute?', @@ -366,7 +343,7 @@ protected function reorderHandlerPrompt(): void required: true, ); - if (class_exists($listClass)) { + if ($listClass) { $this->addNewSimpleEloquentReorderHandlerToList( $reorderAttribute, $modelName, @@ -397,7 +374,7 @@ protected function reorderHandlerPrompt(): void $this->components->info('Your Reorder Handler has been created.'); - if (class_exists($listClass)) { + if ($listClass) { $this->addNewReorderHandlerToList( $name.'Reorder', $this->getSharpRootNamespace().'\\'.$reorderPath.'\\', @@ -718,34 +695,18 @@ private function addNewItemToAListOfCommands(string $commandType, string $comman $classMethodName = sprintf('get%sCommands', $commandType); $reflector = new ReflectionClass($targetClass); - $this->replaceFileContent( - $reflector->getFileName(), - PHP_EOL.'class ', - 'use '.$commandPath.$commandClass.';'.PHP_EOL.PHP_EOL.'class ', - ); - - $this->replaceFileContent( - $reflector->getFileName(), - "$classMethodName(): ?array".PHP_EOL.' {'.PHP_EOL.' return ['.PHP_EOL, - "$classMethodName(): ?array".PHP_EOL.' {'.PHP_EOL.' return ['.PHP_EOL.' '.$commandClass.'::class,'.PHP_EOL, - ); + $editor = new GeneratorFileEditor($reflector->getFileName()); + $editor->ensureUseStatement($commandPath.$commandClass); + $editor->ensureMethodArrayContains("$classMethodName(): ?array", $commandClass); } private function addNewItemToAListOfFilters(string $filterClass, string $filterPath, string $targetClass): void { $reflector = new ReflectionClass($targetClass); - $this->replaceFileContent( - $reflector->getFileName(), - PHP_EOL.'class ', - 'use '.$filterPath.$filterClass.';'.PHP_EOL.PHP_EOL.'class ', - ); - - $this->replaceFileContent( - $reflector->getFileName(), - 'getFilters(): ?array'.PHP_EOL.' {'.PHP_EOL.' return ['.PHP_EOL, - 'getFilters(): ?array'.PHP_EOL.' {'.PHP_EOL.' return ['.PHP_EOL.' '.$filterClass.'::class,'.PHP_EOL, - ); + $editor = new GeneratorFileEditor($reflector->getFileName()); + $editor->ensureUseStatement($filterPath.$filterClass); + $editor->ensureMethodArrayContains('getFilters(): ?array', $filterClass); } private function addNewEntityStateToListOrShowPage(string $targetType, string $entityStateClass, string $entityStatePath, string $targetClass): void @@ -753,16 +714,11 @@ private function addNewEntityStateToListOrShowPage(string $targetType, string $e $classMethodName = sprintf('build%sConfig', $targetType); $reflector = new ReflectionClass($targetClass); - $this->replaceFileContent( - $reflector->getFileName(), - PHP_EOL.'class ', - 'use '.$entityStatePath.$entityStateClass.';'.PHP_EOL.PHP_EOL.'class ', - ); - - $this->replaceFileContent( - $reflector->getFileName(), - "$classMethodName(): void".PHP_EOL.' {'.PHP_EOL.' $this'.PHP_EOL, - "$classMethodName(): void".PHP_EOL.' {'.PHP_EOL.' $this'.PHP_EOL." ->configureEntityState('state',".$entityStateClass.'::class)'.PHP_EOL, + $editor = new GeneratorFileEditor($reflector->getFileName()); + $editor->ensureUseStatement($entityStatePath.$entityStateClass); + $editor->ensureMethodChainContains( + "$classMethodName(): void", + "->configureEntityState('state',".$entityStateClass.'::class)' ); } @@ -770,16 +726,11 @@ private function addNewReorderHandlerToList(string $reorderHandlerClass, string { $reflector = new ReflectionClass($targetClass); - $this->replaceFileContent( - $reflector->getFileName(), - PHP_EOL.'class ', - 'use '.$reorderHandlerPath.$reorderHandlerClass.';'.PHP_EOL.PHP_EOL.'class ', - ); - - $this->replaceFileContent( - $reflector->getFileName(), - 'buildListConfig(): void'.PHP_EOL.' {'.PHP_EOL.' $this'.PHP_EOL, - 'buildListConfig(): void'.PHP_EOL.' {'.PHP_EOL.' $this'.PHP_EOL.' ->configureReorderable('.$reorderHandlerClass.'::class)'.PHP_EOL, + $editor = new GeneratorFileEditor($reflector->getFileName()); + $editor->ensureUseStatement($reorderHandlerPath.$reorderHandlerClass); + $editor->ensureMethodChainContains( + 'buildListConfig(): void', + '->configureReorderable('.$reorderHandlerClass.'::class)' ); } @@ -787,16 +738,15 @@ private function addNewSimpleEloquentReorderHandlerToList(string $reorderAttribu { $reflector = new ReflectionClass($targetClass); - $this->replaceFileContent( - $reflector->getFileName(), - PHP_EOL.'class ', - 'use '.$modelPath.$modelClass.';'.PHP_EOL.'use Code16\Sharp\EntityList\Eloquent\SimpleEloquentReorderHandler;'.PHP_EOL.PHP_EOL.'class ', - ); - - $this->replaceFileContent( - $reflector->getFileName(), - 'buildListConfig(): void'.PHP_EOL.' {'.PHP_EOL.' $this'.PHP_EOL, - 'buildListConfig(): void'.PHP_EOL.' {'.PHP_EOL.' $this'.PHP_EOL.' ->configureReorderable('.PHP_EOL.' (new SimpleEloquentReorderHandler('.$modelClass.'::class))'.PHP_EOL." ->setOrderAttribute('".$reorderAttribute."')".PHP_EOL.' )'.PHP_EOL, + $editor = new GeneratorFileEditor($reflector->getFileName()); + $editor->ensureUseStatement($modelPath.$modelClass); + $editor->ensureUseStatement('Code16\\Sharp\\EntityList\\Eloquent\\SimpleEloquentReorderHandler'); + $editor->ensureMethodChainContains( + 'buildListConfig(): void', + '->configureReorderable('.PHP_EOL + .' (new SimpleEloquentReorderHandler('.$modelClass.'::class))'.PHP_EOL + ." ->setOrderAttribute('".$reorderAttribute."')".PHP_EOL + .' )' ); } @@ -814,4 +764,23 @@ private function namespaceToPath(string $namespace): string { return Str::lcfirst(str_replace('\\', '/', $namespace)); } + + private function resolveEntityListClass(string $entityName): ?string + { + $base = $this->getSharpRootNamespace().'\\'.Str::plural($entityName).'\\'.$entityName; + $listClass = $base.'EntityList'; + + if (! class_exists($listClass)) { + $listClass = $base.'List'; + } + + return class_exists($listClass) ? $listClass : null; + } + + private function resolveEntityShowClass(string $entityName): ?string + { + $showClass = $this->getSharpRootNamespace().'\\'.Str::plural($entityName).'\\'.$entityName.'Show'; + + return class_exists($showClass) ? $showClass : null; + } } diff --git a/src/Console/Utils/GeneratorFileEditor.php b/src/Console/Utils/GeneratorFileEditor.php new file mode 100644 index 000000000..91b4c7fc6 --- /dev/null +++ b/src/Console/Utils/GeneratorFileEditor.php @@ -0,0 +1,63 @@ +contains($useStatement)) { + return; + } + + $this->replace(PHP_EOL.'class ', $useStatement.PHP_EOL.PHP_EOL.'class '); + } + + public function ensureMethodArrayContains(string $methodSignature, string $className): void + { + $needle = $className.'::class'; + if ($this->contains($needle)) { + return; + } + + $search = $methodSignature.PHP_EOL.' {'.PHP_EOL.' return ['.PHP_EOL; + $replace = $search.' '.$className.'::class,'.PHP_EOL; + + $this->replace($search, $replace); + } + + public function ensureMethodChainContains(string $methodSignature, string $chainCall): void + { + if ($this->contains($chainCall)) { + return; + } + + $search = $methodSignature.PHP_EOL.' {'.PHP_EOL.' $this'.PHP_EOL; + $replace = $search.' '.$chainCall.PHP_EOL; + + $this->replace($search, $replace); + } + + private function contains(string $needle): bool + { + return str_contains($this->read(), $needle); + } + + private function read(): string + { + return file_get_contents($this->filePath); + } + + private function replace(string $search, string $replace): void + { + $content = $this->read(); + + file_put_contents( + $this->filePath, + str_replace($search, $replace, $content) + ); + } +} From c9244582b4802f33500cb62744d5290bd24a24ac Mon Sep 17 00:00:00 2001 From: philippe Date: Wed, 4 Feb 2026 09:59:55 +0100 Subject: [PATCH 2/6] Better tests, refactor command prompt --- src/Console/GeneratorCommand.php | 159 +++++++++------ .../{ClosedPeriod.php => UnitTestModel.php} | 2 +- tests/Unit/Console/GeneratorTest.php | 193 +++++++++++++++--- 3 files changed, 264 insertions(+), 90 deletions(-) rename tests/Fixtures/{ClosedPeriod.php => UnitTestModel.php} (78%) diff --git a/src/Console/GeneratorCommand.php b/src/Console/GeneratorCommand.php index 1cedbeb0b..7711240fd 100644 --- a/src/Console/GeneratorCommand.php +++ b/src/Console/GeneratorCommand.php @@ -30,24 +30,15 @@ public function handle() default: 'A new Entity', ); - switch ($wizardType) { - default: - case 'A new Entity': - $this->entityPrompt(); - break; - case 'A Command': - $this->commandPrompt(); - break; - case 'A List Filter': - $this->filterPrompt(); - break; - case 'An Entity State': - $this->entityStatePrompt(); - break; - case 'A Reorder Handler': - $this->reorderHandlerPrompt(); - break; - } + $wizardHandlers = [ + 'A new Entity' => fn () => $this->entityPrompt(), + 'A Command' => fn () => $this->commandPrompt(), + 'A List Filter' => fn () => $this->filterPrompt(), + 'An Entity State' => fn () => $this->entityStatePrompt(), + 'A Reorder Handler' => fn () => $this->reorderHandlerPrompt(), + ]; + + ($wizardHandlers[$wizardType] ?? fn () => $this->entityPrompt())(); return 0; } @@ -62,12 +53,16 @@ protected function entityStatePrompt(): void ); $name = Str::ucfirst(Str::camel($name)); - $entityName = search( - 'Looking for the related Sharp Entity', - fn (string $value) => strlen($value) > 0 - ? $this->getSharpEntitiesList($value) - : [] - ); + if (app()->runningUnitTests()) { + $entityName = 'UnitTestModel'; + } else { + $entityName = search( + 'Looking for the related Sharp Entity', + fn (string $value) => strlen($value) > 0 + ? $this->getSharpEntitiesList($value) + : [] + ); + } $entityStatePath = Str::plural($entityName).'\\States'; $hasModel = confirm( @@ -81,13 +76,17 @@ protected function entityStatePrompt(): void required: true, ); - $model = search( - 'Looking for the related model', - fn (string $value) => strlen($value) > 0 - ? $this->getModelsList(base_path($this->namespaceToPath($modelNamespace)), $value) - : [] - ); - $model = $modelNamespace.'\\'.$model; + if (app()->runningUnitTests()) { + $model = 'Code16\\Sharp\\Tests\\Fixtures\\UnitTestModel'; + } else { + $model = search( + 'Looking for the related model', + fn (string $value) => strlen($value) > 0 + ? $this->getModelsList(base_path($this->namespaceToPath($modelNamespace)), $value) + : [] + ); + $model = $modelNamespace.'\\'.$model; + } if (! class_exists($model)) { $this->components->error(sprintf('Sorry the model class [%s] cannot be found', $model)); @@ -162,12 +161,16 @@ protected function filterPrompt(): void ); $name = Str::ucfirst(Str::camel($name)); - $entityName = search( - 'Looking for the related Sharp Entity', - fn (string $value) => strlen($value) > 0 - ? $this->getSharpEntitiesList($value) - : [] - ); + if (app()->runningUnitTests()) { + $entityName = 'UnitTestModel'; + } else { + $entityName = search( + 'Looking for the related Sharp Entity', + fn (string $value) => strlen($value) > 0 + ? $this->getSharpEntitiesList($value) + : [] + ); + } $filterPath = Str::plural($entityName).'\\Filters'; $this->call('sharp:make:entity-list-filter', [ @@ -222,12 +225,16 @@ protected function commandPrompt(): void ); $name = Str::ucfirst(Str::camel($name)); - $entityName = search( - 'Looking for the related Sharp Entity', - fn (string $value) => strlen($value) > 0 - ? $this->getSharpEntitiesList($value) - : [] - ); + if (app()->runningUnitTests()) { + $entityName = 'UnitTestModel'; + } else { + $entityName = search( + 'Looking for the related Sharp Entity', + fn (string $value) => strlen($value) > 0 + ? $this->getSharpEntitiesList($value) + : [] + ); + } $needsWizard = $needsWizard ?? false; $commandPath = Str::plural($entityName).'\\Commands'; @@ -282,11 +289,30 @@ protected function entityPrompt(): void }; if (confirm(label: 'Do you want to automatically declare this Entity in the Sharp configuration?')) { - $provider = text( - label: 'What is the full name of your Sharp Service Provider?', - default: 'App\\Providers\\SharpServiceProvider', - required: true, - ); + $providerFound = false; + + while (! $providerFound) { + $provider = text( + label: 'What is the full name of your Sharp Service Provider?', + default: 'App\\Providers\\SharpServiceProvider', + required: true, + ); + + if (! class_exists($provider)) { + $this->components->error(sprintf('The class [%s] does not exist.', $provider)); + + if (! confirm(label: 'Do you want to try again?')) { + $this->components->info('Your Entity and all related files have been created.'); + + return; + } + + continue; + } + + $providerFound = true; + } + $reflector = new \ReflectionClass($provider); $this->declareEntityInSharpConfiguration($reflector->getFileName(), $entityPath, $entityKey); @@ -308,12 +334,16 @@ protected function entityPrompt(): void protected function reorderHandlerPrompt(): void { - $entityName = search( - 'Looking for the related Sharp Entity', - fn (string $value) => strlen($value) > 0 - ? $this->getSharpEntitiesList($value) - : [] - ); + if (app()->runningUnitTests()) { + $entityName = 'UnitTestModel'; + } else { + $entityName = search( + 'Looking for the related Sharp Entity', + fn (string $value) => strlen($value) > 0 + ? $this->getSharpEntitiesList($value) + : [] + ); + } $reorderPath = Str::plural($entityName).'\\ReorderHandlers'; $modelNamespace = text( @@ -322,13 +352,18 @@ protected function reorderHandlerPrompt(): void required: true, ); - $modelName = search( - 'Search for the related model', - fn (string $value) => strlen($value) > 0 - ? $this->getModelsList(base_path($this->namespaceToPath($modelNamespace)), $value) - : [] - ); - $model = $modelNamespace.'\\'.$modelName; + if (app()->runningUnitTests()) { + $modelName = 'UnitTestModel'; + $model = 'Code16\\Sharp\\Tests\\Fixtures\\UnitTestModel'; + } else { + $modelName = search( + 'Search for the related model', + fn (string $value) => strlen($value) > 0 + ? $this->getModelsList(base_path($this->namespaceToPath($modelNamespace)), $value) + : [] + ); + $model = $modelNamespace.'\\'.$modelName; + } $listClass = $this->resolveEntityListClass($entityName); @@ -409,7 +444,7 @@ private function generateRegularEntity(): array ); if (app()->runningUnitTests()) { - $model = 'Code16\\Sharp\\Tests\\Fixtures\\ClosedPeriod'; + $model = 'Code16\\Sharp\\Tests\\Fixtures\\UnitTestModel'; } else { $model = search( 'Looking for the related model', diff --git a/tests/Fixtures/ClosedPeriod.php b/tests/Fixtures/UnitTestModel.php similarity index 78% rename from tests/Fixtures/ClosedPeriod.php rename to tests/Fixtures/UnitTestModel.php index 07beaac65..8b3ee3278 100644 --- a/tests/Fixtures/ClosedPeriod.php +++ b/tests/Fixtures/UnitTestModel.php @@ -4,7 +4,7 @@ use Illuminate\Database\Eloquent\Model; -class ClosedPeriod extends Model +class UnitTestModel extends Model { protected $guarded = []; } diff --git a/tests/Unit/Console/GeneratorTest.php b/tests/Unit/Console/GeneratorTest.php index 5f1522563..f621085df 100644 --- a/tests/Unit/Console/GeneratorTest.php +++ b/tests/Unit/Console/GeneratorTest.php @@ -1,7 +1,7 @@ increments('id'); $table->string('my_field'); $table->timestamps(); @@ -20,10 +20,10 @@ $this->artisan('sharp:generator') ->expectsQuestion('What do you need?', 'A new Entity') ->expectsQuestion('What is the type of your Entity?', 'Regular') - ->expectsQuestion('What is the name of your Entity (singular)?', 'ClosedPeriod') + ->expectsQuestion('What is the name of your Entity (singular)?', 'UnitTestModel') ->expectsQuestion('Do you want to attach this Entity to a specific Model?', 'yes') ->expectsQuestion('What is the namespace of your models?', 'App/Models') - ->expectsQuestion('What is the label of your Entity?', 'Fermetures') + ->expectsQuestion('What is the label of your Entity?', 'Unit Test Models') ->expectsQuestion('What do you need with your Entity?', 'Entity List, Form & Show Page') ->expectsConfirmation('Do you need a Policy?', 'yes') ->expectsConfirmation('Do you want to automatically declare this Entity in the Sharp configuration?', 'no') @@ -31,74 +31,74 @@ // Manually add this new Entity to the Sharp config app(SharpConfigBuilder::class) - ->addEntity('closed_periods', '\App\Sharp\Entities\ClosedPeriodEntity'); + ->addEntity('unit_test_models', '\App\Sharp\Entities\UnitTestModelEntity'); $this - ->get(route('code16.sharp.list', ['entityKey' => 'closed_periods'])) + ->get(route('code16.sharp.list', ['entityKey' => 'unit_test_models'])) ->assertOk(); $this ->get(route('code16.sharp.form.create', [ - 'parentUri' => 's-list/closed_periods', - 'entityKey' => 'closed_periods', + 'parentUri' => 's-list/unit_test_models', + 'entityKey' => 'unit_test_models', ])) ->assertOk(); $this ->post(route('code16.sharp.form.store', [ - 'parentUri' => 's-list/closed_periods', - 'entityKey' => 'closed_periods', + 'parentUri' => 's-list/unit_test_models', + 'entityKey' => 'unit_test_models', ]), [ 'my_field' => 'Arnaud', ]) ->assertStatus(302); - $this->assertDatabaseHas('closed_periods', ['my_field' => 'Arnaud']); + $this->assertDatabaseHas('unit_test_models', ['my_field' => 'Arnaud']); - $this->get(route('code16.sharp.list', ['closed_periods'])) + $this->get(route('code16.sharp.list', ['unit_test_models'])) ->assertOk() ->assertSee('Arnaud'); - $closedPeriod = ClosedPeriod::first(); + $unitTestModel = UnitTestModel::first(); $this ->get(route('code16.sharp.show.show', [ - 'parentUri' => 's-list/closed_periods', - 'entityKey' => 'closed_periods', - 'instanceId' => $closedPeriod->id, + 'parentUri' => 's-list/unit_test_models', + 'entityKey' => 'unit_test_models', + 'instanceId' => $unitTestModel->id, ])) ->assertOk() ->assertSee('Arnaud'); $this ->get(route('code16.sharp.form.edit', [ - 'parentUri' => 's-list/closed_periods', - 'entityKey' => 'closed_periods', - 'instanceId' => $closedPeriod->id, + 'parentUri' => 's-list/unit_test_models', + 'entityKey' => 'unit_test_models', + 'instanceId' => $unitTestModel->id, ])) ->assertOk(); $this ->post(route('code16.sharp.form.update', [ - 'parentUri' => 's-list/closed_periods', - 'entityKey' => 'closed_periods', - 'instanceId' => $closedPeriod->id, + 'parentUri' => 's-list/unit_test_models', + 'entityKey' => 'unit_test_models', + 'instanceId' => $unitTestModel->id, ]), [ 'my_field' => 'Benoit', ]) ->assertStatus(302); - $this->assertDatabaseHas('closed_periods', ['my_field' => 'Benoit']); + $this->assertDatabaseHas('unit_test_models', ['my_field' => 'Benoit']); $this ->delete(route('code16.sharp.show.delete', [ - 'parentUri' => 's-list/closed_periods', - 'entityKey' => 'closed_periods', - 'instanceId' => $closedPeriod->id, + 'parentUri' => 's-list/unit_test_models', + 'entityKey' => 'unit_test_models', + 'instanceId' => $unitTestModel->id, ])) ->assertStatus(302); - $this->assertDatabaseMissing('closed_periods', ['id' => $closedPeriod->id]); + $this->assertDatabaseMissing('unit_test_models', ['id' => $unitTestModel->id]); }); it('can generate a new sharp single entity from console', function () { @@ -159,3 +159,142 @@ ->assertSee('My section title') ->assertSee('1234'); }); + +it('can generate a new command from console', function () { + Schema::create('unit_test_models', function (Blueprint $table) { + $table->increments('id'); + $table->string('my_field'); + $table->timestamps(); + }); + + // First create an entity + $this->artisan('sharp:generator') + ->expectsQuestion('What do you need?', 'A new Entity') + ->expectsQuestion('What is the type of your Entity?', 'Regular') + ->expectsQuestion('What is the name of your Entity (singular)?', 'UnitTestModel') + ->expectsConfirmation('Do you want to attach this Entity to a specific Model?', 'yes') + ->expectsQuestion('What is the namespace of your models?', 'App/Models') + ->expectsQuestion('What is the label of your Entity?', 'Unit Test Models') + ->expectsQuestion('What do you need with your Entity?', 'Entity List, Form & Show Page') + ->expectsConfirmation('Do you need a Policy?', 'no') + ->expectsConfirmation('Do you want to automatically declare this Entity in the Sharp configuration?', 'no') + ->assertExitCode(0); + + app(SharpConfigBuilder::class) + ->addEntity('unit_test_models', '\App\Sharp\Entities\UnitTestModelEntity'); + + // Now generate a command + $this->artisan('sharp:generator') + ->expectsQuestion('What do you need?', 'A Command') + ->expectsQuestion('What is the type of the new Command?', 'Instance') + ->expectsConfirmation('Do you need a Form in the Command?', 'no') + ->expectsQuestion('What is the name of your Command?', 'SendEmail') + ->assertExitCode(0); + + expect(File::exists(base_path('app/Sharp/UnitTestModels/Commands/SendEmailCommand.php')))->toBeTrue(); +}); + +it('can generate a new list filter from console', function () { + Schema::create('unit_test_models', function (Blueprint $table) { + $table->increments('id'); + $table->string('my_field'); + $table->timestamps(); + }); + + // First create an entity + $this->artisan('sharp:generator') + ->expectsQuestion('What do you need?', 'A new Entity') + ->expectsQuestion('What is the type of your Entity?', 'Regular') + ->expectsQuestion('What is the name of your Entity (singular)?', 'UnitTestModel') + ->expectsConfirmation('Do you want to attach this Entity to a specific Model?', 'yes') + ->expectsQuestion('What is the namespace of your models?', 'App/Models') + ->expectsQuestion('What is the label of your Entity?', 'Unit Test Models') + ->expectsQuestion('What do you need with your Entity?', 'Entity List') + ->expectsConfirmation('Do you need a Policy?', 'no') + ->expectsConfirmation('Do you want to automatically declare this Entity in the Sharp configuration?', 'no') + ->assertExitCode(0); + + app(SharpConfigBuilder::class) + ->addEntity('unit_test_models', '\App\Sharp\Entities\UnitTestModelEntity'); + + // Now generate a filter + $this->artisan('sharp:generator') + ->expectsQuestion('What do you need?', 'A List Filter') + ->expectsQuestion('What is the type of your Entity Filter?', 'Select') + ->expectsConfirmation('Can the Filter accept multiple values?', 'no') + ->expectsConfirmation('Can the Filter accept empty value?', 'yes') + ->expectsQuestion('What is the name of your Filter?', 'Status') + ->assertExitCode(0); + + expect(File::exists(base_path('app/Sharp/UnitTestModels/Filters/StatusFilter.php')))->toBeTrue(); +}); + +it('can generate a new entity state from console', function () { + Schema::create('unit_test_models', function (Blueprint $table) { + $table->increments('id'); + $table->string('my_field'); + $table->timestamps(); + }); + + // First create an entity with List and Show + $this->artisan('sharp:generator') + ->expectsQuestion('What do you need?', 'A new Entity') + ->expectsQuestion('What is the type of your Entity?', 'Regular') + ->expectsQuestion('What is the name of your Entity (singular)?', 'UnitTestModel') + ->expectsConfirmation('Do you want to attach this Entity to a specific Model?', 'yes') + ->expectsQuestion('What is the namespace of your models?', 'App/Models') + ->expectsQuestion('What is the label of your Entity?', 'Unit Test Models') + ->expectsQuestion('What do you need with your Entity?', 'Entity List & Show Page') + ->expectsConfirmation('Do you need a Policy?', 'no') + ->expectsConfirmation('Do you want to automatically declare this Entity in the Sharp configuration?', 'no') + ->assertExitCode(0); + + app(SharpConfigBuilder::class) + ->addEntity('unit_test_models', '\App\Sharp\Entities\UnitTestModelEntity'); + + // Now generate an entity state + $this->artisan('sharp:generator') + ->expectsQuestion('What do you need?', 'An Entity State') + ->expectsQuestion('What is the name of your Entity State?', 'Approval') + ->expectsConfirmation('Should the Entity State update an instance of an Eloquent model?', 'yes') + ->expectsQuestion('What is the namespace of your models?', 'App/Models') + ->expectsConfirmation('A App\Code16\Sharp\Tests\Fixtures\UnitTestModel model does not exist. Do you want to generate it?', 'no') + ->assertExitCode(0); + + expect(File::exists(base_path('app/Sharp/UnitTestModels/States/ApprovalEntityState.php')))->toBeTrue(); +}); + +it('can generate a new reorder handler from console', function () { + Schema::create('unit_test_models', function (Blueprint $table) { + $table->increments('id'); + $table->string('my_field'); + $table->timestamps(); + }); + + // First create an entity + $this->artisan('sharp:generator') + ->expectsQuestion('What do you need?', 'A new Entity') + ->expectsQuestion('What is the type of your Entity?', 'Regular') + ->expectsQuestion('What is the name of your Entity (singular)?', 'UnitTestModel') + ->expectsConfirmation('Do you want to attach this Entity to a specific Model?', 'yes') + ->expectsQuestion('What is the namespace of your models?', 'App/Models') + ->expectsQuestion('What is the label of your Entity?', 'Unit Test Models') + ->expectsQuestion('What do you need with your Entity?', 'Entity List') + ->expectsConfirmation('Do you need a Policy?', 'no') + ->expectsConfirmation('Do you want to automatically declare this Entity in the Sharp configuration?', 'no') + ->assertExitCode(0); + + app(SharpConfigBuilder::class) + ->addEntity('unit_test_models', '\App\Sharp\Entities\UnitTestModelEntity'); + + // Now generate a reorder handler + $this->artisan('sharp:generator') + ->expectsQuestion('What do you need?', 'A Reorder Handler') + ->expectsQuestion('What is the namespace of your models?', 'App/Models') + ->expectsConfirmation('Use the simple Eloquent implementation based on a reorder attribute?', 'no') + ->expectsQuestion('What is the name of your reorder handler?', 'UnitTestModel') + ->expectsConfirmation('A App\Code16\Sharp\Tests\Fixtures\UnitTestModel model does not exist. Do you want to generate it?', 'no') + ->assertExitCode(0); + + expect(File::exists(base_path('app/Sharp/UnitTestModels/ReorderHandlers/UnitTestModelReorder.php')))->toBeTrue(); +}); From e9939944b14ef4a693884882ddb2d1e228ccb1a7 Mon Sep 17 00:00:00 2001 From: Philippe Lonchampt Date: Wed, 4 Feb 2026 10:10:35 +0100 Subject: [PATCH 3/6] Update src/Console/GeneratorCommand.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Console/GeneratorCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Console/GeneratorCommand.php b/src/Console/GeneratorCommand.php index 7711240fd..2f477b248 100644 --- a/src/Console/GeneratorCommand.php +++ b/src/Console/GeneratorCommand.php @@ -753,7 +753,7 @@ private function addNewEntityStateToListOrShowPage(string $targetType, string $e $editor->ensureUseStatement($entityStatePath.$entityStateClass); $editor->ensureMethodChainContains( "$classMethodName(): void", - "->configureEntityState('state',".$entityStateClass.'::class)' + "->configureEntityState('state', ".$entityStateClass.'::class)' ); } From 2298b2576987a1b294e956457d12bb90c44baed7 Mon Sep 17 00:00:00 2001 From: Philippe Lonchampt Date: Wed, 4 Feb 2026 10:10:45 +0100 Subject: [PATCH 4/6] Update tests/Unit/Console/GeneratorTest.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/Unit/Console/GeneratorTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Unit/Console/GeneratorTest.php b/tests/Unit/Console/GeneratorTest.php index f621085df..d9cdbe07c 100644 --- a/tests/Unit/Console/GeneratorTest.php +++ b/tests/Unit/Console/GeneratorTest.php @@ -3,6 +3,7 @@ use Code16\Sharp\Config\SharpConfigBuilder; use Code16\Sharp\Tests\Fixtures\UnitTestModel; use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\File; beforeEach(function () { login(); From 42ce44b1bab8495eca82d680223c8df814b5b241 Mon Sep 17 00:00:00 2001 From: Philippe Lonchampt Date: Wed, 4 Feb 2026 10:12:20 +0100 Subject: [PATCH 5/6] Update src/Console/Utils/GeneratorFileEditor.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Console/Utils/GeneratorFileEditor.php | 27 ++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/src/Console/Utils/GeneratorFileEditor.php b/src/Console/Utils/GeneratorFileEditor.php index 91b4c7fc6..41f350cb1 100644 --- a/src/Console/Utils/GeneratorFileEditor.php +++ b/src/Console/Utils/GeneratorFileEditor.php @@ -48,16 +48,33 @@ private function contains(string $needle): bool private function read(): string { - return file_get_contents($this->filePath); + if (!is_file($this->filePath) || !is_readable($this->filePath)) { + return ''; + } + + $content = file_get_contents($this->filePath); + + return $content === false ? '' : $content; } private function replace(string $search, string $replace): void { + if (!is_file($this->filePath) || !is_writable($this->filePath)) { + return; + } + $content = $this->read(); - file_put_contents( - $this->filePath, - str_replace($search, $replace, $content) - ); + if ($content === '' || !str_contains($content, $search)) { + return; + } + + $updatedContent = str_replace($search, $replace, $content); + + if ($updatedContent === $content) { + return; + } + + file_put_contents($this->filePath, $updatedContent); } } From 2d59151edb367f7ab8272f97d8975bf4745e4260 Mon Sep 17 00:00:00 2001 From: philippe Date: Wed, 4 Feb 2026 10:13:24 +0100 Subject: [PATCH 6/6] CS --- src/Console/Utils/GeneratorFileEditor.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Console/Utils/GeneratorFileEditor.php b/src/Console/Utils/GeneratorFileEditor.php index 41f350cb1..0638d97d6 100644 --- a/src/Console/Utils/GeneratorFileEditor.php +++ b/src/Console/Utils/GeneratorFileEditor.php @@ -48,7 +48,7 @@ private function contains(string $needle): bool private function read(): string { - if (!is_file($this->filePath) || !is_readable($this->filePath)) { + if (! is_file($this->filePath) || ! is_readable($this->filePath)) { return ''; } @@ -59,13 +59,13 @@ private function read(): string private function replace(string $search, string $replace): void { - if (!is_file($this->filePath) || !is_writable($this->filePath)) { + if (! is_file($this->filePath) || ! is_writable($this->filePath)) { return; } $content = $this->read(); - if ($content === '' || !str_contains($content, $search)) { + if ($content === '' || ! str_contains($content, $search)) { return; }