diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 9873b0aa..56e4ce38 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -32,6 +32,10 @@ jobs: run: | composer install --no-interaction --prefer-dist --no-progress --no-suggest + - name: Check code formatting with PHP CS Fixer + run: | + vendor/bin/php-cs-fixer fix --dry-run --diff --ansi + - name: Check quality code with PHPInsights run: | vendor/bin/phpinsights -n --ansi --format=github-action diff --git a/src/Console/Commands/MakeCollection.php b/src/Console/Commands/MakeCollection.php new file mode 100644 index 00000000..c1799641 --- /dev/null +++ b/src/Console/Commands/MakeCollection.php @@ -0,0 +1,50 @@ +setHelp('This command allows you to create a new collection.'); + + $this->addArgument('name', InputArgument::REQUIRED, 'The collection name'); + + $this->addOption('force', 'f', InputOption::VALUE_NONE, 'Force to create collections'); + } + + protected function outputDirectory(): string + { + return 'app' . DIRECTORY_SEPARATOR . 'Collections'; + } + + protected function stub(): string + { + return 'collection.stub'; + } + + protected function commonName(): string + { + return 'Collection'; + } +} diff --git a/src/Console/Commands/MakeModel.php b/src/Console/Commands/MakeModel.php new file mode 100644 index 00000000..37a4dc44 --- /dev/null +++ b/src/Console/Commands/MakeModel.php @@ -0,0 +1,129 @@ +addOption('collection', 'cn', InputOption::VALUE_NONE, 'Create a collection for the model'); + + $this->addOption('query', 'qb', InputOption::VALUE_NONE, 'Create a query builder for the model'); + + $this->addOption('all', 'a', InputOption::VALUE_NONE, 'Create a model with custom query builder and collection'); + } + + protected function outputDirectory(): string + { + return 'app' . DIRECTORY_SEPARATOR . 'Models'; + } + + protected function stub(): string + { + $stub = 'model.stub'; + + if ($this->input->getOption('all')) { + $stub = 'model.all.stub'; + } elseif ($this->input->getOption('collection')) { + $stub = 'model.collection.stub'; + } elseif ($this->input->getOption('query')) { + $stub = 'model.query.stub'; + } + + return $stub; + } + + protected function commonName(): string + { + return 'Model'; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->input = $input; + + $search = parent::SEARCH; + + $name = $this->input->getArgument('name'); + $force = $this->input->getOption('force'); + + $namespace = explode(DIRECTORY_SEPARATOR, $name); + $className = array_pop($namespace); + $fileName = $this->getCustomFileName() ?? $className; + + $filePath = $this->preparePath($namespace) . DIRECTORY_SEPARATOR . "{$fileName}.php"; + $namespace = $this->prepareNamespace($namespace); + + $replace = [$namespace, $className]; + + if (File::exists($filePath) && ! $force) { + $output->writeln(["{$this->commonName()} already exists!", self::EMPTY_LINE]); + + return parent::SUCCESS; + } + + $application = $this->getApplication(); + + if ($input->getOption('collection') || $input->getOption('all')) { + $command = $application->find('make:collection'); + $collectionName = "{$name}Collection"; + + $arguments = new ArrayInput([ + 'name' => $collectionName, + ]); + + $command->run($arguments, $output); + + $search[] = '{collection_name}'; + $replace[] = $collectionName; + } + + if ($input->getOption('query') || $input->getOption('all')) { + $command = $application->find('make:query'); + $queryName = "{$name}Query"; + + $arguments = new ArrayInput([ + 'name' => $queryName, + ]); + + $command->run($arguments, $output); + + $search[] = '{query_name}'; + $replace[] = $queryName; + } + + $stub = $this->getStubContent(); + $stub = str_replace($search, $replace, $stub); + + File::put($filePath, $stub); + + $output->writeln(["{$this->commonName()} successfully generated!", self::EMPTY_LINE]); + + + return parent::SUCCESS; + } +} diff --git a/src/Console/Commands/MakeQuery.php b/src/Console/Commands/MakeQuery.php new file mode 100644 index 00000000..ce10970f --- /dev/null +++ b/src/Console/Commands/MakeQuery.php @@ -0,0 +1,50 @@ +setHelp('This command allows you to create a new query.'); + + $this->addArgument('name', InputArgument::REQUIRED, 'The query name'); + + $this->addOption('force', 'f', InputOption::VALUE_NONE, 'Force to create queries'); + } + + protected function outputDirectory(): string + { + return 'app' . DIRECTORY_SEPARATOR . 'Queries'; + } + + protected function stub(): string + { + return 'query.stub'; + } + + protected function commonName(): string + { + return 'Query'; + } +} diff --git a/src/Providers/CommandsServiceProvider.php b/src/Providers/CommandsServiceProvider.php index 4779e945..6b940c2c 100644 --- a/src/Providers/CommandsServiceProvider.php +++ b/src/Providers/CommandsServiceProvider.php @@ -4,8 +4,11 @@ namespace Phenix\Providers; +use Phenix\Console\Commands\MakeCollection; use Phenix\Console\Commands\MakeController; use Phenix\Console\Commands\MakeMiddleware; +use Phenix\Console\Commands\MakeModel; +use Phenix\Console\Commands\MakeQuery; use Phenix\Console\Commands\MakeRequest; use Phenix\Console\Commands\MakeServiceProvider; use Phenix\Console\Commands\MakeTest; @@ -19,6 +22,9 @@ public function boot(): void MakeRequest::class, MakeController::class, MakeMiddleware::class, + MakeModel::class, + MakeCollection::class, + MakeQuery::class, MakeServiceProvider::class, ]); } diff --git a/src/stubs/collection.stub b/src/stubs/collection.stub new file mode 100644 index 00000000..fc0c98cd --- /dev/null +++ b/src/stubs/collection.stub @@ -0,0 +1,12 @@ +expect( + exists: fn (string $path) => false, + get: function (string $path): string { + return file_get_contents($path); + }, + put: fn (string $path) => true, + createDirectory: function (string $path): void { + // .. + } + ); + + $this->app->swap(File::class, $mock); + + /** @var CommandTester $command */ + $command = $this->phenix('make:collection', [ + 'name' => 'User', + ]); + + $command->assertCommandIsSuccessful(); + + expect($command->getDisplay())->toContain('Collection successfully generated!'); +}); + +it('does not create the collection 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:collection', [ + 'name' => 'User', + ]); + + /** @var CommandTester $command */ + $command = $this->phenix('make:collection', [ + 'name' => 'User', + ]); + + $command->assertCommandIsSuccessful(); + + expect($command->getDisplay())->toContain('Collection already exists!'); +}); + +it('creates collection successfully with force option', function () { + $tempDir = sys_get_temp_dir(); + $tempPath = $tempDir . DIRECTORY_SEPARATOR . 'User.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 CommandTester $command */ + $command = $this->phenix('make:collection', [ + 'name' => 'User', + '--force' => true, + ]); + + $command->assertCommandIsSuccessful(); + + expect($command->getDisplay())->toContain('Collection successfully generated!'); + expect('new content')->toBe(file_get_contents($tempPath)); +}); + +it('creates collection successfully in nested namespace', function () { + $mock = Mock::of(File::class)->expect( + exists: fn (string $path) => false, + get: fn (string $path) => '', + put: fn (string $path) => true, + createDirectory: function (string $path): void { + // .. + } + ); + + $this->app->swap(File::class, $mock); + + /** @var \Symfony\Component\Console\Tester\CommandTester $command */ + $command = $this->phenix('make:collection', [ + 'name' => 'Admin/User', + ]); + + $command->assertCommandIsSuccessful(); + + expect($command->getDisplay())->toContain('Collection successfully generated!'); +}); diff --git a/tests/Unit/Console/MakeModelCommandTest.php b/tests/Unit/Console/MakeModelCommandTest.php new file mode 100644 index 00000000..8eec306b --- /dev/null +++ b/tests/Unit/Console/MakeModelCommandTest.php @@ -0,0 +1,229 @@ +expect( + exists: fn (string $path) => false, + get: function (string $path): string { + return file_get_contents($path); + }, + put: fn (string $path) => true, + createDirectory: function (string $path): void { + // .. + } + ); + + $this->app->swap(File::class, $mock); + + /** @var CommandTester $command */ + $command = $this->phenix('make:model', [ + 'name' => 'User', + ]); + + $command->assertCommandIsSuccessful(); + + expect($command->getDisplay())->toContain('Model successfully generated!'); +}); + +it('does not create the model 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:model', [ + 'name' => 'User', + ]); + + /** @var CommandTester $command */ + $command = $this->phenix('make:model', [ + 'name' => 'User', + ]); + + $command->assertCommandIsSuccessful(); + + expect($command->getDisplay())->toContain('Model already exists!'); +}); + +it('creates model successfully with force option', function () { + $tempDir = sys_get_temp_dir(); + $tempPath = $tempDir . DIRECTORY_SEPARATOR . 'User.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 CommandTester $command */ + $command = $this->phenix('make:model', [ + 'name' => 'User', + '--force' => true, + ]); + + $command->assertCommandIsSuccessful(); + + expect($command->getDisplay())->toContain('Model successfully generated!'); + expect('new content')->toBe(file_get_contents($tempPath)); +}); + +it('creates model successfully in nested namespace', function () { + $mock = Mock::of(File::class)->expect( + exists: fn (string $path) => false, + get: fn (string $path) => '', + put: fn (string $path) => true, + createDirectory: function (string $path): void { + // .. + } + ); + + $this->app->swap(File::class, $mock); + + /** @var \Symfony\Component\Console\Tester\CommandTester $command */ + $command = $this->phenix('make:model', [ + 'name' => 'Admin/User', + ]); + + $command->assertCommandIsSuccessful(); + + expect($command->getDisplay())->toContain('Model successfully generated!'); +}); + +it('creates model with custom collection', function () { + $mock = Mock::of(File::class)->expect( + exists: fn (string $path): bool => false, + get: function (string $path): string { + return file_get_contents($path); + }, + put: function (string $path, string $content): bool { + if (str_ends_with($path, 'UserCollection.php')) { + expect($content)->toContain('namespace App\Collections;'); + expect($content)->toContain('class UserCollection extends Collection'); + } + + if (str_ends_with($path, 'User.php')) { + expect($content)->toContain('use App\Collections\UserCollection;'); + expect($content)->toContain('class User extends DatabaseModel'); + expect($content)->toContain('public function newCollection(): UserCollection'); + } + + return true; + }, + createDirectory: function (string $path): void { + // .. + } + ); + + $this->app->swap(File::class, $mock); + + /** @var CommandTester $command */ + $command = $this->phenix('make:model', [ + 'name' => 'User', + '--collection' => true, + ]); + + $command->assertCommandIsSuccessful(); + + expect($command->getDisplay())->toContain('Model successfully generated!'); + expect($command->getDisplay())->toContain('Collection successfully generated!'); +}); + +it('creates model with custom query builder', function () { + $mock = Mock::of(File::class)->expect( + exists: fn (string $path): bool => false, + get: function (string $path): string { + return file_get_contents($path); + }, + put: function (string $path, string $content): bool { + if (str_ends_with($path, 'UserQuery.php')) { + expect($content)->toContain('namespace App\Queries;'); + expect($content)->toContain('class UserQuery extends DatabaseQueryBuilder'); + } + + if (str_ends_with($path, 'User.php')) { + expect($content)->toContain('use App\Queries\UserQuery;'); + expect($content)->toContain('class User extends DatabaseModel'); + expect($content)->toContain('protected static function newQueryBuilder(): UserQuery'); + } + + return true; + }, + createDirectory: function (string $path): void { + // .. + } + ); + + $this->app->swap(File::class, $mock); + + /** @var CommandTester $command */ + $command = $this->phenix('make:model', [ + 'name' => 'User', + '--query' => true, + ]); + + $command->assertCommandIsSuccessful(); + + expect($command->getDisplay())->toContain('Model successfully generated!'); + expect($command->getDisplay())->toContain('Query successfully generated!'); +}); + +it('creates model with all', function () { + $mock = Mock::of(File::class)->expect( + exists: fn (string $path): bool => false, + get: function (string $path): string { + return file_get_contents($path); + }, + put: function (string $path, string $content): bool { + if (str_ends_with($path, 'UserCollection.php')) { + expect($content)->toContain('namespace App\Collections;'); + expect($content)->toContain('class UserCollection extends Collection'); + } + + if (str_ends_with($path, 'UserQuery.php')) { + expect($content)->toContain('namespace App\Queries;'); + expect($content)->toContain('class UserQuery extends DatabaseQueryBuilder'); + } + + if (str_ends_with($path, 'User.php')) { + expect($content)->toContain('use App\Queries\UserQuery;'); + expect($content)->toContain('class User extends DatabaseModel'); + expect($content)->toContain('protected static function newQueryBuilder(): UserQuery'); + expect($content)->toContain('public function newCollection(): UserCollection'); + } + + return true; + }, + createDirectory: function (string $path): void { + // .. + } + ); + + $this->app->swap(File::class, $mock); + + /** @var CommandTester $command */ + $command = $this->phenix('make:model', [ + 'name' => 'User', + '--all' => true, + ]); + + $command->assertCommandIsSuccessful(); + + expect($command->getDisplay())->toContain('Model successfully generated!'); + expect($command->getDisplay())->toContain('Query successfully generated!'); + expect($command->getDisplay())->toContain('Collection successfully generated!'); +}); diff --git a/tests/Unit/Console/MakeQueryCommandTest.php b/tests/Unit/Console/MakeQueryCommandTest.php new file mode 100644 index 00000000..d2003397 --- /dev/null +++ b/tests/Unit/Console/MakeQueryCommandTest.php @@ -0,0 +1,105 @@ +expect( + exists: fn (string $path) => false, + get: function (string $path): string { + return file_get_contents($path); + }, + put: fn (string $path) => true, + createDirectory: function (string $path): void { + // .. + } + ); + + $this->app->swap(File::class, $mock); + + /** @var CommandTester $command */ + $command = $this->phenix('make:query', [ + 'name' => 'UserQuery', + ]); + + $command->assertCommandIsSuccessful(); + + expect($command->getDisplay())->toContain('Query successfully generated!'); +}); + +it('does not create the query 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:query', [ + 'name' => 'UserQuery', + ]); + + /** @var CommandTester $command */ + $command = $this->phenix('make:query', [ + 'name' => 'UserQuery', + ]); + + $command->assertCommandIsSuccessful(); + + expect($command->getDisplay())->toContain('Query already exists!'); +}); + +it('creates query successfully with force option', function () { + $tempDir = sys_get_temp_dir(); + $tempPath = $tempDir . DIRECTORY_SEPARATOR . 'User.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 CommandTester $command */ + $command = $this->phenix('make:query', [ + 'name' => 'UserQuery', + '--force' => true, + ]); + + $command->assertCommandIsSuccessful(); + + expect($command->getDisplay())->toContain('Query successfully generated!'); + expect('new content')->toBe(file_get_contents($tempPath)); +}); + +it('creates query successfully in nested namespace', function () { + $mock = Mock::of(File::class)->expect( + exists: fn (string $path) => false, + get: fn (string $path) => '', + put: fn (string $path) => true, + createDirectory: function (string $path): void { + // .. + } + ); + + $this->app->swap(File::class, $mock); + + /** @var \Symfony\Component\Console\Tester\CommandTester $command */ + $command = $this->phenix('make:query', [ + 'name' => 'Domain/UserQuery', + ]); + + $command->assertCommandIsSuccessful(); + + expect($command->getDisplay())->toContain('Query successfully generated!'); +});