From 9f6764b2ddfd5662688136b62eb6b468f6390ffc Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Tue, 7 Oct 2025 18:38:39 -0500 Subject: [PATCH 1/4] feat(validation): add MakeRule and MakeType commands with corresponding stubs and tests --- src/Validation/Console/MakeRule.php | 49 ++++++++ src/Validation/Console/MakeType.php | 49 ++++++++ src/Validation/ValidationServiceProvider.php | 20 ++++ src/stubs/rule.stub | 21 ++++ src/stubs/type.stub | 18 +++ .../Console/MakeRuleCommandTest.php | 110 ++++++++++++++++++ .../Console/MakeTypeCommandTest.php | 110 ++++++++++++++++++ tests/fixtures/application/config/app.php | 1 + 8 files changed, 378 insertions(+) create mode 100644 src/Validation/Console/MakeRule.php create mode 100644 src/Validation/Console/MakeType.php create mode 100644 src/Validation/ValidationServiceProvider.php create mode 100644 src/stubs/rule.stub create mode 100644 src/stubs/type.stub create mode 100644 tests/Unit/Validation/Console/MakeRuleCommandTest.php create mode 100644 tests/Unit/Validation/Console/MakeTypeCommandTest.php diff --git a/src/Validation/Console/MakeRule.php b/src/Validation/Console/MakeRule.php new file mode 100644 index 00000000..d6186c91 --- /dev/null +++ b/src/Validation/Console/MakeRule.php @@ -0,0 +1,49 @@ +setHelp('This command allows you to create a new validation rule.'); + + $this->addArgument('name', InputArgument::REQUIRED, 'The rule class name'); + $this->addOption('force', 'f', InputOption::VALUE_NONE, 'Force to create rule'); + } + + protected function outputDirectory(): string + { + return 'app' . DIRECTORY_SEPARATOR . 'Validation' . DIRECTORY_SEPARATOR . 'Rules'; + } + + protected function stub(): string + { + return 'rule.stub'; + } + + protected function commonName(): string + { + return 'Rule'; + } +} diff --git a/src/Validation/Console/MakeType.php b/src/Validation/Console/MakeType.php new file mode 100644 index 00000000..ef7541bf --- /dev/null +++ b/src/Validation/Console/MakeType.php @@ -0,0 +1,49 @@ +setHelp('This command allows you to create a new validation type.'); + + $this->addArgument('name', InputArgument::REQUIRED, 'The type class name'); + $this->addOption('force', 'f', InputOption::VALUE_NONE, 'Force to create type'); + } + + protected function outputDirectory(): string + { + return 'app' . DIRECTORY_SEPARATOR . 'Validation' . DIRECTORY_SEPARATOR . 'Types'; + } + + protected function stub(): string + { + return 'type.stub'; + } + + protected function commonName(): string + { + return 'Type'; + } +} diff --git a/src/Validation/ValidationServiceProvider.php b/src/Validation/ValidationServiceProvider.php new file mode 100644 index 00000000..412f42f8 --- /dev/null +++ b/src/Validation/ValidationServiceProvider.php @@ -0,0 +1,20 @@ +commands([ + MakeRule::class, + MakeType::class, + ]); + } +} diff --git a/src/stubs/rule.stub b/src/stubs/rule.stub new file mode 100644 index 00000000..137c91a3 --- /dev/null +++ b/src/stubs/rule.stub @@ -0,0 +1,21 @@ + $this->getFieldForHumans()]); + } +} diff --git a/src/stubs/type.stub b/src/stubs/type.stub new file mode 100644 index 00000000..30d1a6fc --- /dev/null +++ b/src/stubs/type.stub @@ -0,0 +1,18 @@ +expect( + exists: fn (string $path) => false, + get: fn (string $path) => '', + put: function (string $path) { + expect($path)->toBe(base_path('app/Validation/Rules/AwesomeRule.php')); + + return true; + }, + createDirectory: function (string $path): void { + // .. + } + ); + + $this->app->swap(File::class, $mock); + + /** @var \Symfony\Component\Console\Tester\CommandTester $command */ + $command = $this->phenix('make:rule', [ + 'name' => 'AwesomeRule', + ]); + + $command->assertCommandIsSuccessful(); + + expect($command->getDisplay())->toContain('Rule [app/Validation/Rules/AwesomeRule.php] successfully generated!'); +}); + +it('does not create the rule because it already exists', function () { + $mock = Mock::of(File::class)->expect( + exists: fn (string $path) => true, + ); + + $this->app->swap(File::class, $mock); + + $this->phenix('make:rule', [ + 'name' => 'TestRule', + ]); + + /** @var \Symfony\Component\Console\Tester\CommandTester $command */ + $command = $this->phenix('make:rule', [ + 'name' => 'TestRule', + ]); + + $command->assertCommandIsSuccessful(); + + expect($command->getDisplay())->toContain('Rule already exists!'); +}); + +it('creates rule successfully with force option', function () { + $tempDir = sys_get_temp_dir(); + $tempPath = $tempDir . DIRECTORY_SEPARATOR . 'TestRule.php'; + + file_put_contents($tempPath, 'old content'); + + $this->assertEquals('old content', file_get_contents($tempPath)); + + $mock = Mock::of(File::class)->expect( + exists: fn (string $path) => false, + get: fn (string $path) => 'new content', + put: fn (string $path, string $content) => file_put_contents($tempPath, $content), + createDirectory: function (string $path): void { + // .. + } + ); + + $this->app->swap(File::class, $mock); + + /** @var \Symfony\Component\Console\Tester\CommandTester $command */ + $command = $this->phenix('make:rule', [ + 'name' => 'TestRule', + '--force' => true, + ]); + + $command->assertCommandIsSuccessful(); + + expect($command->getDisplay())->toContain('Rule [app/Validation/Rules/TestRule.php] successfully generated!'); + expect('new content')->toBe(file_get_contents($tempPath)); +}); + +it('creates rule successfully in nested namespace', function () { + $mock = Mock::of(File::class)->expect( + exists: fn (string $path) => false, + get: fn (string $path) => '', + put: function (string $path) { + expect($path)->toBe(base_path('app/Validation/Rules/Admin/TestRule.php')); + + return true; + }, + createDirectory: function (string $path): void { + // .. + } + ); + + $this->app->swap(File::class, $mock); + + /** @var \Symfony\Component\Console\Tester\CommandTester $command */ + $command = $this->phenix('make:rule', [ + 'name' => 'Admin/TestRule', + ]); + + $command->assertCommandIsSuccessful(); + + expect($command->getDisplay())->toContain('Rule [app/Validation/Rules/Admin/TestRule.php] successfully generated!'); +}); diff --git a/tests/Unit/Validation/Console/MakeTypeCommandTest.php b/tests/Unit/Validation/Console/MakeTypeCommandTest.php new file mode 100644 index 00000000..d9845dd9 --- /dev/null +++ b/tests/Unit/Validation/Console/MakeTypeCommandTest.php @@ -0,0 +1,110 @@ +expect( + exists: fn (string $path) => false, + get: fn (string $path) => '', + put: function (string $path) { + expect($path)->toBe(base_path('app/Validation/Types/AwesomeType.php')); + + return true; + }, + createDirectory: function (string $path): void { + // .. + } + ); + + $this->app->swap(File::class, $mock); + + /** @var \Symfony\Component\Console\Tester\CommandTester $command */ + $command = $this->phenix('make:type', [ + 'name' => 'AwesomeType', + ]); + + $command->assertCommandIsSuccessful(); + + expect($command->getDisplay())->toContain('Type [app/Validation/Types/AwesomeType.php] successfully generated!'); +}); + +it('does not create the type because it already exists', function () { + $mock = Mock::of(File::class)->expect( + exists: fn (string $path) => true, + ); + + $this->app->swap(File::class, $mock); + + $this->phenix('make:type', [ + 'name' => 'TestType', + ]); + + /** @var \Symfony\Component\Console\Tester\CommandTester $command */ + $command = $this->phenix('make:type', [ + 'name' => 'TestType', + ]); + + $command->assertCommandIsSuccessful(); + + expect($command->getDisplay())->toContain('Type already exists!'); +}); + +it('creates type successfully with force option', function () { + $tempDir = sys_get_temp_dir(); + $tempPath = $tempDir . DIRECTORY_SEPARATOR . 'TestType.php'; + + file_put_contents($tempPath, 'old content'); + + $this->assertEquals('old content', file_get_contents($tempPath)); + + $mock = Mock::of(File::class)->expect( + exists: fn (string $path) => false, + get: fn (string $path) => 'new content', + put: fn (string $path, string $content) => file_put_contents($tempPath, $content), + createDirectory: function (string $path): void { + // .. + } + ); + + $this->app->swap(File::class, $mock); + + /** @var \Symfony\Component\Console\Tester\CommandTester $command */ + $command = $this->phenix('make:type', [ + 'name' => 'TestType', + '--force' => true, + ]); + + $command->assertCommandIsSuccessful(); + + expect($command->getDisplay())->toContain('Type [app/Validation/Types/TestType.php] successfully generated!'); + expect('new content')->toBe(file_get_contents($tempPath)); +}); + +it('creates type successfully in nested namespace', function () { + $mock = Mock::of(File::class)->expect( + exists: fn (string $path) => false, + get: fn (string $path) => '', + put: function (string $path) { + expect($path)->toBe(base_path('app/Validation/Types/Admin/TestType.php')); + + return true; + }, + createDirectory: function (string $path): void { + // .. + } + ); + + $this->app->swap(File::class, $mock); + + /** @var \Symfony\Component\Console\Tester\CommandTester $command */ + $command = $this->phenix('make:type', [ + 'name' => 'Admin/TestType', + ]); + + $command->assertCommandIsSuccessful(); + + expect($command->getDisplay())->toContain('Type [app/Validation/Types/Admin/TestType.php] successfully generated!'); +}); diff --git a/tests/fixtures/application/config/app.php b/tests/fixtures/application/config/app.php index d0b5a581..732528da 100644 --- a/tests/fixtures/application/config/app.php +++ b/tests/fixtures/application/config/app.php @@ -31,5 +31,6 @@ \Phenix\Queue\QueueServiceProvider::class, \Phenix\Events\EventServiceProvider::class, \Phenix\Translation\TranslationServiceProvider::class, + \Phenix\Validation\ValidationServiceProvider::class, ], ]; From 7d3d12cf5837b4351a9a4b6631ecb7f69117ee3d Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Wed, 8 Oct 2025 08:20:59 -0500 Subject: [PATCH 2/4] chore: change code order --- src/Validation/Console/MakeType.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Validation/Console/MakeType.php b/src/Validation/Console/MakeType.php index ef7541bf..a7dec5c8 100644 --- a/src/Validation/Console/MakeType.php +++ b/src/Validation/Console/MakeType.php @@ -22,19 +22,19 @@ class MakeType extends Maker * * @phpcsSuppress SlevomatCodingStandard.TypeHints.PropertyTypeHint */ - protected static $defaultDescription = 'Creates a new validation type.'; + protected static $defaultDescription = 'Creates a new data type for validation.'; protected function configure(): void { - $this->setHelp('This command allows you to create a new validation type.'); - $this->addArgument('name', InputArgument::REQUIRED, 'The type class name'); $this->addOption('force', 'f', InputOption::VALUE_NONE, 'Force to create type'); + + $this->setHelp('This command allows you to create a new data type for validation.'); } - protected function outputDirectory(): string + protected function commonName(): string { - return 'app' . DIRECTORY_SEPARATOR . 'Validation' . DIRECTORY_SEPARATOR . 'Types'; + return 'Type'; } protected function stub(): string @@ -42,8 +42,8 @@ protected function stub(): string return 'type.stub'; } - protected function commonName(): string + protected function outputDirectory(): string { - return 'Type'; + return 'app' . DIRECTORY_SEPARATOR . 'Validation' . DIRECTORY_SEPARATOR . 'Types'; } } From 8cfd24a7be43e2b83a1cb88777bb4a4ee158d270 Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Wed, 8 Oct 2025 08:42:18 -0500 Subject: [PATCH 3/4] chore(rule.stub): remove placeholder comment from passes method [skip ci] --- src/stubs/rule.stub | 1 - 1 file changed, 1 deletion(-) diff --git a/src/stubs/rule.stub b/src/stubs/rule.stub index 137c91a3..d054192f 100644 --- a/src/stubs/rule.stub +++ b/src/stubs/rule.stub @@ -10,7 +10,6 @@ class {name} extends Rule { public function passes(): bool { - // Implement validation logic return true; } From e7a9f153b349c78cb7e7cdfd5d68b3671aa0f7a1 Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Wed, 8 Oct 2025 08:44:07 -0500 Subject: [PATCH 4/4] chore: update type.stub file structure and organization [skip ci] --- src/stubs/type.stub | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/stubs/type.stub b/src/stubs/type.stub index 30d1a6fc..3ccb0506 100644 --- a/src/stubs/type.stub +++ b/src/stubs/type.stub @@ -4,7 +4,7 @@ declare(strict_types=1); namespace {namespace}; -use Phenix\Validation\Rules\IsString; // Change to desired base rule +use Phenix\Validation\Rules\IsString; use Phenix\Validation\Rules\TypeRule; use Phenix\Validation\Types\Scalar; @@ -12,7 +12,6 @@ class {name} extends Scalar { protected function defineType(): TypeRule { - // Use a base type rule that defines the primitive type of this custom type return IsString::new(); } }