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!');
+});