diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml
new file mode 100644
index 0000000..d63d13d
--- /dev/null
+++ b/.github/workflows/phpstan.yml
@@ -0,0 +1,26 @@
+name: PHPStan
+
+on:
+ push:
+ paths:
+ - '**.php'
+ - 'phpstan.neon.dist'
+
+jobs:
+ phpstan:
+ name: phpstan
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: 8.1
+ coverage: none
+
+ - name: Install composer dependencies
+ uses: ramsey/composer-install@v3
+
+ - name: Run PHPStan
+ run: ./vendor/bin/phpstan --error-format=github
\ No newline at end of file
diff --git a/.github/workflows/pint.yml b/.github/workflows/pint.yml
new file mode 100644
index 0000000..4c5d64c
--- /dev/null
+++ b/.github/workflows/pint.yml
@@ -0,0 +1,25 @@
+name: Laravel Pint
+
+on:
+ push:
+ paths:
+ - '**.php'
+
+jobs:
+ pint:
+ name: pint
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: 8.1
+ coverage: none
+
+ - name: Install composer dependencies
+ uses: ramsey/composer-install@v3
+
+ - name: Run pint
+ run: ./vendor/bin/pint --test
\ No newline at end of file
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 0000000..f24c8f7
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -0,0 +1,57 @@
+name: Run Unit Tests
+
+on:
+ pull_request:
+ push:
+ branches:
+ - main
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ strategy:
+ fail-fast: false
+ matrix:
+ php: [8.0, 8.1, 8.2, 8.3]
+ laravel: [10.*, 11.*]
+ dependency-version: [prefer-lowest, prefer-stable]
+ exclude:
+ - laravel: 10.*
+ php: 8.0
+ - laravel: 11.*
+ php: 8.0
+ - laravel: 11.*
+ php: 8.1
+
+ name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }}
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{matrix.php}}
+
+ - name: Get Composer cache directory
+ id: composer-cache
+ run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
+
+ - name: Cache Composer dependencies
+ uses: actions/cache@v4
+ with:
+ path: ${{ steps.composer-cache.outputs.dir }}
+ # Use composer.json for key, if composer.lock is not committed.
+ key: php-${{ matrix.php }}-lara-${{ matrix.laravel }}-composer-${{ matrix.dependency-version }}-${{ hashFiles('**/composer.json') }}
+ # key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
+ restore-keys: php-${{ matrix.php }}-lara-${{ matrix.laravel }}-composer-${{ matrix.dependency-version }}-
+
+ - name: Install Composer dependencies
+ run: |
+ composer require "laravel/framework:${{ matrix.laravel }}" --no-interaction --no-update
+ composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction
+
+ - name: Run PHPUnit tests
+ run: vendor/bin/phpunit --testdox
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 4f4acd3..6c42583 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,4 @@
+build/
vendor/
-composer.lock
\ No newline at end of file
+composer.lock
+*.cache
\ No newline at end of file
diff --git a/README.md b/README.md
index f8f9b13..911b493 100644
--- a/README.md
+++ b/README.md
@@ -11,11 +11,10 @@ Run the following command from your projects root
```php
composer require norbybaru/modularize
```
-
-For Laravel versions lower than 5.5, this step is important after running above script.
-- Open your `config/app.php` file and add custom service provider:
-```php
-NorbyBaru\Modularize\ModularizeServiceProvider::class
+## Config
+Publish configuration
+```bash
+php artisan vendor:publish --provider="NorbyBaru\Modularize\ModularizeServiceProvider" --tag="modularize-config"
```
## Usage
diff --git a/composer.json b/composer.json
index 42c9146..ee59e07 100644
--- a/composer.json
+++ b/composer.json
@@ -3,12 +3,8 @@
"description": "Generate modular structure files for laravel",
"homepage": "https://github.com/norbybaru/modularize",
"keywords": ["laravel", "modular", "modules", "module", "structure", "modular", "laravel-modular", "modularize"],
- "require": {
- "php": ">=5.6.4",
- "illuminate/support": "^5.6 || ^6.0 || ^7.0"
- },
"license": "MIT",
- "version": "1.2.2",
+ "version": "2.0",
"authors": [
{
"name": "Norby Baruani",
@@ -20,6 +16,11 @@
"NorbyBaru\\Modularize\\": "src/"
}
},
+ "autoload-dev": {
+ "psr-4": {
+ "NorbyBaru\\Modularize\\Tests\\": "tests/"
+ }
+ },
"extra": {
"laravel": {
"providers": [
@@ -27,5 +28,28 @@
]
}
},
- "minimum-stability": "dev"
+ "minimum-stability": "dev",
+ "prefer-stable": true,
+ "scripts": {
+ "analyse": "vendor/bin/phpstan analyse",
+ "fmt": "./vendor/bin/pint -v",
+ "post-autoload-dump": [
+ "@php vendor/bin/testbench package:discover --ansi"
+ ],
+ "test": "phpunit"
+ },
+ "config": {
+ "sort-packages": true
+ },
+ "require": {
+ "php": "^8.1",
+ "illuminate/console": "^10.13|^11.0",
+ "illuminate/support": "^10.13|^11.0"
+ },
+ "require-dev": {
+ "laravel/pint": "^1.10",
+ "nunomaduro/larastan": "^2.0",
+ "orchestra/testbench": "^8.5|^9.0",
+ "phpunit/phpunit": "^9.5|^10.13|^11.0"
+ }
}
diff --git a/config/modularize.php b/config/modularize.php
new file mode 100644
index 0000000..1629b7c
--- /dev/null
+++ b/config/modularize.php
@@ -0,0 +1,20 @@
+ true,
+
+ /**
+ * Define application root directory folder to create modules files
+ */
+ 'root_path' => 'modules',
+
+ /**
+ * Routes created under the Routes/ directory of a module would be autoload to be discovered by the application.
+ * Setting 'autoload_routes => false' will require manually registering module routes through a service provider.
+ */
+ 'autoload_routes' => true,
+];
diff --git a/phpstan.neon.dist b/phpstan.neon.dist
new file mode 100644
index 0000000..e761f99
--- /dev/null
+++ b/phpstan.neon.dist
@@ -0,0 +1,16 @@
+includes:
+ - ./vendor/nunomaduro/larastan/extension.neon
+
+parameters:
+
+ paths:
+ - src
+ - config
+
+ # The level 9 is the highest level
+ level: 5
+
+ tmpDir: build/phpstan
+ checkModelProperties: true
+ checkMissingIterableValueType: true
+ treatPhpDocTypesAsCertain: false
\ No newline at end of file
diff --git a/phpunit.xml b/phpunit.xml
new file mode 100644
index 0000000..388c944
--- /dev/null
+++ b/phpunit.xml
@@ -0,0 +1,23 @@
+
+
+
+
+ ./tests
+
+
+
+
+
+
+
+
diff --git a/pint.json b/pint.json
new file mode 100644
index 0000000..661e522
--- /dev/null
+++ b/pint.json
@@ -0,0 +1,3 @@
+{
+ "preset": "laravel"
+}
\ No newline at end of file
diff --git a/src/Console/Commands/ModuleCommand.php b/src/Console/Commands/ModuleCommand.php
deleted file mode 100644
index 4db44b4..0000000
--- a/src/Console/Commands/ModuleCommand.php
+++ /dev/null
@@ -1,347 +0,0 @@
-
- * @version 1.2.2
- * @since 1.0.0
- */
-class ModuleCommand extends GeneratorCommand
-{
- /**
- * The name and signature of the console command.
- *
- * @var string
- */
- protected $signature = 'module:generate
- {name : Module name}
- {--group= : Optional grouping name}
- {--no-migration : Do not create migration files}
- {--no-request : Do not create module request file}
- {--no-translation : Do not create module translation filesystem}';
-
- /**
- * The console command description.
- *
- * @var string
- */
- protected $description = 'Generate new module';
-
- /**
- * The current stub.
- *
- * @var string
- */
- protected $currentStub;
-
- /**
- * Module group name
- *
- * @var string
- */
- protected $group;
-
- /**
- * The type of class being generated.
- *
- * @var string
- */
- protected $type = 'Module';
-
- /**
- * Laravel version
- *
- * @var string
- */
- protected $version;
-
- /**
- * Execute the console command.
- *
- * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
- */
- public function handle()
- {
- $this->version = (int) str_replace('.', '', app()->version());
-
- $this->group = $this->option('group')
- ? Str::studly($this->option('group'))
- : null;
-
- $name = ($this->group)
- ? $this->group . '/' . Str::studly($this->getNameInput())
- : Str::studly($this->getNameInput()) ;
-
- // check if module exists
- if ($this->files->exists(app_path() . '/Modules/' . $name)) {
- $this->error($this->type.' already exists!');
- return;
- }
-
- // Create Controller
- $this->generate('controller');
-
- // Create Model
- $this->generate('model');
-
- // Create Views folder
- $this->generate('view');
-
- // Create Helper file
- $this->generate('helper');
-
- if ($this->version < 530) {
- // Create Routes file
- $this->generate('routes');
- } else {
- // Create WEB Routes file
- $this->generate('web');
-
- // Create API Routes file
- $this->generate('api');
- }
-
- //Flag for no translation
- if (!$this->option('no-translation')) {
- $this->generate('translation');
- }
-
- //Flag for no request
- if (!$this->option('no-request')) {
- $this->generate('request');
- }
-
- //Flag for no migrations
- if (!$this->option('no-migration')) {
- // without hacky studly_case function
- // foo-bar results in foo-bar and not in foo_bar
- $table = Str::of($this->getNameInput())->plural()->snake()->studly();
- $this->call('make:migration', ['name' => "create_{$table}_table", '--create' => $table]);
- }
-
- $this->info($this->type.' created successfully.');
- }
-
- /**
- * @return mixed
- * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
- */
- public function fire()
- {
- return $this->handle();
- }
-
- /**
- * @param $type
- * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
- *
- */
- protected function generate($type)
- {
- switch ($type) {
- case 'controller':
- $filename = Str::studly($this->getNameInput()).ucfirst($type);
- break;
- case 'request':
- $filename = Str::studly($this->getNameInput()).ucfirst($type);
- break;
- case 'model':
- $filename = Str::studly($this->getNameInput());
- break;
- case 'view':
- $filename = 'index.blade';
- break;
- case 'translation':
- $filename = 'example';
- break;
- case 'routes':
- $filename = 'routes';
- break;
- case 'web':
- $filename = 'web';
- $folder = 'routes\\';
- break;
- case 'api':
- $filename = 'api';
- $folder = 'routes\\';
- break;
- case 'helper':
- $filename = 'Helper';
- break;
- }
-
- if (! isset($folder)) {
- $folder = ($type != 'routes' && $type != 'helper')
- ? ucfirst($type).'s\\'. ($type === 'translation' ? 'en\\':'')
- : '';
- }
-
- $qualifyClass = method_exists($this, 'qualifyClass')
- ? 'qualifyClass'
- : 'parseName';
-
- $module = ($this->group)
- ? $this->group . '\\' . Str::of($this->getNameInput())->studly()->ucfirst()
- : Str::of($this->getNameInput())->studly()->ucfirst();
-
- $name = $this->$qualifyClass('Modules\\'. $module .'\\' . $folder . $filename);
-
- if ($this->files->exists($path = $this->getPath($name))) {
- $this->error($this->type.' already exists!');
- return;
- }
-
- $this->currentStub = __DIR__ . '/templates/' .$type.'.sample';
-
- //Group samples
- if ($this->group && $type == 'routes') {
- $this->currentStub = __DIR__ . '/templates/routesGroup.sample';
- } elseif ($this->group && $type == 'web') {
- $this->currentStub = __DIR__ . '/templates/webGroup.sample';
- }
-
- $this->makeDirectory($path);
- $this->files->put($path, $this->buildClass($name));
- }
- /**
- * Get the full namespace name for a given class.
- *
- * @param string $name
- * @return string
- */
- protected function getNamespace($name)
- {
- $name = str_replace('\\routes\\', '\\', $name);
- return trim(
- implode(
- '\\',
- array_map(
- 'ucfirst',
- array_slice(explode('\\', Str::studly($name)), 0, -1)
- )
- ),
- '\\'
- );
- }
-
- /**
- * Build the class with the given name.
- *
- * @param string $name
- * @return string
- * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
- */
- protected function buildClass($name)
- {
- $stub = $this->files->get($this->getStub());
- return $this->replaceName($stub, $this->getNameInput())
- ->replaceNamespace($stub, $name)
- ->replaceClass($stub, $name);
- }
-
- /**
- * Replace the namespace for the given stub.
- *
- * @param string $stub
- * @param string $name
- * @return $this
- */
- protected function replaceNamespace(&$stub, $name)
- {
- $stub = str_replace(
- ['SampleNamespace', 'SampleRootNamespace', 'NamespacedDummyUserModel'],
- [$this->getNamespace($name), $this->rootNamespace(), config('auth.providers.users.model')],
- $stub
- );
-
- return $this;
- }
-
- /**
- * Replace the name for the given stub.
- *
- * @param string $stub
- * @param string $name
- * @return $this
- */
- protected function replaceName(&$stub, $name)
- {
- $title = ($this->group) ? $this->group . '.' . $name : $name;
-
- $stub = str_replace('SampleTitle', strtolower($name), $stub);
- $stub = str_replace('SampleViewTitle', strtolower(Str::snake($title, '-')), $stub);
- $stub = str_replace('SampleUCtitle', ucfirst(Str::studly($name)), $stub);
-
- $stub = ($this->group)
- ? str_replace('SampleModuleGroup', strtolower($this->group), $stub)
- : $this->removePrefixFromRoutes($stub);
-
- return $this;
- }
-
- /**
- * Remove prefix from routes when there its not a module group
- *
- * @param $stub
- * @return mixed
- */
- private function removePrefixFromRoutes(&$stub)
- {
- return str_replace("'prefix' => 'SampleModuleGroup', ", '', $stub);
- }
-
- /**
- * Replace the class name for the given stub.
- *
- * @param string $stub
- * @param string $name
- * @return string
- */
- protected function replaceClass($stub, $name)
- {
- $class = class_basename($name);
- return str_replace('SampleClass', $class, $stub);
- }
- /**
- * Get the stub file for the generator.
- *
- * @return string
- */
- protected function getStub()
- {
- return $this->currentStub;
- }
-
- /**
- * Get the console command arguments.
- *
- * @return array
- */
- protected function getArguments()
- {
- return array(
- ['name', InputArgument::REQUIRED, 'Module name.'],
- );
- }
- /**
- * Get the console command options.
- *
- * @return array
- */
- protected function getOptions()
- {
- return [
- ['--no-migration', null, InputOption::VALUE_NONE, 'Do not create new migration files.'],
- ['--no-translation', null, InputOption::VALUE_NONE, 'Do not create module translation filesystem.'],
- ];
- }
-}
diff --git a/src/Console/Commands/ModuleMakeComponentCommand.php b/src/Console/Commands/ModuleMakeComponentCommand.php
new file mode 100644
index 0000000..d0a2cf6
--- /dev/null
+++ b/src/Console/Commands/ModuleMakeComponentCommand.php
@@ -0,0 +1,96 @@
+getModuleInput();
+ $filename = Str::studly($this->getNameInput());
+ $folder = $this->getFolderPath();
+
+ $name = $this->qualifyClass($module.'\\'.$folder.'\\'.$filename);
+ $path = $this->getPath($name);
+ if ($this->files->exists($path) && ! $this->option('force')) {
+ $this->logFileExist($name);
+
+ return true;
+ }
+
+ $type = '';
+
+ if ($this->option('inline')) {
+ $type = 'inline.';
+ }
+
+ $this->setStubFile("view-component.{$type}");
+ $this->makeDirectory($path);
+
+ $this->files->put($path, $this->buildClass($name));
+
+ if (! $this->option('inline')) {
+ $this->call(
+ ModuleMakeViewCommand::class,
+ [
+ 'name' => 'Components/'.Str::snake($filename, '-'),
+ '--module' => $module,
+ '--quiet' => true,
+ ]
+ );
+ }
+
+ $this->logFileCreated($name);
+
+ return true;
+ }
+
+ protected function buildClass($name): string
+ {
+ if ($this->option('inline')) {
+ return str_replace(
+ ['{{view}}', '{{ view }}'],
+ "<<<'blade'\n
\n \n
\nblade",
+ parent::buildClass($name)
+ );
+ }
+
+ return parent::buildClass($name);
+ }
+
+ protected function getFolderPath(): string
+ {
+ return 'Components';
+ }
+}
diff --git a/src/Console/Commands/ModuleMakeConsoleCommand.php b/src/Console/Commands/ModuleMakeConsoleCommand.php
new file mode 100644
index 0000000..2ebb90a
--- /dev/null
+++ b/src/Console/Commands/ModuleMakeConsoleCommand.php
@@ -0,0 +1,58 @@
+getModuleInput();
+ $filename = Str::studly($this->getNameInput());
+ $folder = $this->getFolderPath();
+
+ $name = $this->qualifyClass($module.'\\'.$folder.'\\'.$filename);
+
+ if (! $path = $this->getFilePath(name: $name, force: $this->option('force'))) {
+ return true;
+ }
+
+ $this->generateFile($path, $name);
+
+ }
+
+ protected function logFileCreated(string $path, ?string $type = null)
+ {
+ parent::logFileCreated($path, 'Console command');
+ }
+
+ protected function getFolderPath(): string
+ {
+ return 'Console';
+ }
+}
diff --git a/src/Console/Commands/ModuleMakeControllerCommand.php b/src/Console/Commands/ModuleMakeControllerCommand.php
new file mode 100644
index 0000000..1ff3e7b
--- /dev/null
+++ b/src/Console/Commands/ModuleMakeControllerCommand.php
@@ -0,0 +1,93 @@
+getModuleInput();
+ $filename = Str::studly($this->getNameInput());
+ $folder = $this->getFolderPath();
+
+ $type = 'plain.';
+
+ if ($this->option('api')) {
+ $type = 'api.';
+ }
+
+ if ($this->option('invokable')) {
+ $type = 'invokable.';
+ }
+
+ if ($this->option('resource')) {
+ $type = '';
+ }
+
+ $name = $this->qualifyClass($module.'\\'.$folder.'\\'.$filename);
+
+ if ($this->files->exists($path = $this->getPath($name))) {
+ $this->logFileExist($name);
+
+ return true;
+ }
+
+ $this->setStubFile("controller.{$type}");
+ $this->makeDirectory($path);
+ $this->files->put($path, $this->buildClass($name));
+
+ $this->logFileCreated($name);
+ }
+
+ protected function getFolderPath(): string
+ {
+ return 'Controllers';
+ }
+
+ protected function setStubFile(string $file): void
+ {
+ $this->currentStub = $this->currentStub.$file.'sample';
+ }
+
+ /**
+ * Get the console command arguments.
+ *
+ * @return array
+ */
+ protected function getArguments()
+ {
+ return [
+ ['name', InputArgument::REQUIRED, 'The name of the controller'],
+ ];
+ }
+}
diff --git a/src/Console/Commands/ModuleMakeEventCommand.php b/src/Console/Commands/ModuleMakeEventCommand.php
new file mode 100644
index 0000000..804cc78
--- /dev/null
+++ b/src/Console/Commands/ModuleMakeEventCommand.php
@@ -0,0 +1,53 @@
+getModuleInput();
+ $filename = Str::studly($this->getNameInput());
+ $folder = $this->getFolderPath();
+
+ $name = $this->qualifyClass($module.'\\'.$folder.'\\'.$filename);
+
+ if (! $path = $this->getFilePath($name)) {
+ return true;
+ }
+
+ $this->generateFile($path, $name);
+
+ return true;
+ }
+
+ protected function getFolderPath(): string
+ {
+ return 'Events';
+ }
+}
diff --git a/src/Console/Commands/ModuleMakeJobCommand.php b/src/Console/Commands/ModuleMakeJobCommand.php
new file mode 100644
index 0000000..b7000b2
--- /dev/null
+++ b/src/Console/Commands/ModuleMakeJobCommand.php
@@ -0,0 +1,60 @@
+getModuleInput();
+ $filename = Str::studly($this->getNameInput());
+ $folder = $this->getFolderPath();
+
+ $name = $this->qualifyClass($module.'\\'.$folder.'\\'.$filename);
+
+ if (! $path = $this->getFilePath($name)) {
+ return true;
+ }
+
+ $type = '';
+
+ if ($this->option('sync')) {
+ $type = 'sync.';
+ }
+
+ $this->generateFile($path, $name, $type);
+
+ return true;
+ }
+
+ protected function getFolderPath(): string
+ {
+ return 'Jobs';
+ }
+}
diff --git a/src/Console/Commands/ModuleMakeListenerCommand.php b/src/Console/Commands/ModuleMakeListenerCommand.php
new file mode 100644
index 0000000..a1ea301
--- /dev/null
+++ b/src/Console/Commands/ModuleMakeListenerCommand.php
@@ -0,0 +1,77 @@
+getModuleInput();
+ $filename = Str::studly($this->getNameInput());
+ $folder = $this->getFolderPath();
+
+ $name = $this->qualifyClass('Modules\\'.$module.'\\'.$folder.'\\'.$filename);
+
+ if (! $path = $this->getFilePath($name)) {
+ return true;
+ }
+
+ $type = '';
+
+ if ($event = $this->option('event')) {
+ $type = 'event.';
+ $event = $this->qualifyClass($module.'\\'.'Events'.'\\'.$event);
+ }
+
+ if ($this->option('queued')) {
+ $type .= 'queued.';
+ }
+
+ if ($event) {
+ $this->setStubFile(strtolower($this->type).".{$type}");
+ $this->makeDirectory($path);
+
+ $stub = $this->buildClass($name);
+ $this->files->put($path, $this->buildModel($stub, $event));
+ $this->logFileCreated($name);
+
+ return true;
+ }
+
+ $this->generateFile($path, $name, $type);
+
+ return true;
+ }
+
+ protected function getFolderPath(): string
+ {
+ return 'Listeners';
+ }
+}
diff --git a/src/Console/Commands/ModuleMakeMiddlewareCommand.php b/src/Console/Commands/ModuleMakeMiddlewareCommand.php
new file mode 100644
index 0000000..4215aa3
--- /dev/null
+++ b/src/Console/Commands/ModuleMakeMiddlewareCommand.php
@@ -0,0 +1,63 @@
+getModuleInput();
+ $filename = Str::studly($this->getNameInput());
+ $folder = $this->getFolderPath();
+
+ $name = $this->qualifyClass($module.'\\'.$folder.'\\'.$filename);
+
+ if ($this->files->exists($path = $this->getPath($name))) {
+ $this->logFileExist($name);
+
+ return true;
+ }
+
+ $type = '';
+ $this->setStubFile("middleware.{$type}");
+ $this->makeDirectory($path);
+
+ $stub = $this->buildClass($name);
+
+ $this->files->put($path, $stub);
+
+ $this->logFileCreated($name);
+
+ return true;
+ }
+
+ protected function getFolderPath(): string
+ {
+ return 'Middleware';
+ }
+}
diff --git a/src/Console/Commands/ModuleMakeMigrationCommand.php b/src/Console/Commands/ModuleMakeMigrationCommand.php
new file mode 100644
index 0000000..7f0c5cf
--- /dev/null
+++ b/src/Console/Commands/ModuleMakeMigrationCommand.php
@@ -0,0 +1,122 @@
+option('module')) {
+ $module = $this->ask('What is the name of the module?');
+ }
+
+ $module = Str::studly($module);
+
+ $name = Str::studly($this->getNameInput());
+
+ $create = $this->option('create');
+ $update = $this->option('table');
+
+ $path = $this->qualifyClass($module.'\\'.$this->folder);
+ $path = $this->classPath($path);
+
+ $arguments = [
+ 'name' => $name,
+ '--path' => $path,
+ ];
+
+ if ($create) {
+ $arguments['--create'] = $this->getPluralName($create);
+ } elseif ($update) {
+ $arguments['--table'] = $this->getPluralName($update);
+ }
+
+ $this->call(
+ 'make:migration',
+ $arguments
+ );
+
+ }
+
+ protected function getFolderPath(): string
+ {
+ return 'Migrations';
+ }
+
+ /**
+ * Get the destination class path.
+ *
+ * @param string $name
+ * @return string
+ */
+ protected function classPath($name)
+ {
+ return str_replace('\\', '/', $name);
+ }
+
+ /**
+ * Get the stub file for the generator.
+ *
+ * @return string
+ */
+ protected function getStub()
+ {
+ return $this->currentStub;
+ }
+
+ /**
+ * Get the console command arguments.
+ *
+ * @return array
+ */
+ protected function getArguments()
+ {
+ return [
+ ['name', InputArgument::REQUIRED, 'The name of the migration'],
+ ];
+ }
+
+ /**
+ * Get the console command options.
+ *
+ * @return array
+ */
+ protected function getOptions()
+ {
+ return [
+ ['--module', null, InputOption::VALUE_REQUIRED, 'Name of module migration should belong to.'],
+ ];
+ }
+}
diff --git a/src/Console/Commands/ModuleMakeModelCommand.php b/src/Console/Commands/ModuleMakeModelCommand.php
new file mode 100644
index 0000000..e91be82
--- /dev/null
+++ b/src/Console/Commands/ModuleMakeModelCommand.php
@@ -0,0 +1,167 @@
+getModuleInput();
+ $filename = Str::studly($this->getNameInput());
+ $folder = $this->getFolderPath();
+
+ $type = '';
+
+ if ($this->option('pivot')) {
+ $type = 'pivot.';
+ }
+
+ $name = $this->qualifyClass($module.'\\'.$folder.'\\'.$filename);
+
+ if ($this->files->exists($path = $this->getPath($name))) {
+ $this->logFileExist($name);
+
+ return true;
+ }
+
+ $this->setStubFile("model.{$type}");
+ $this->makeDirectory($path);
+ $this->files->put($path, $this->buildClass($name));
+
+ $this->logFileCreated($name);
+
+ if ($this->option('migration')) {
+ $this->makeMigration(name: $filename, module: $module);
+ }
+
+ if ($this->option('controller')) {
+ $this->makeController(name: $filename, module: $module);
+ }
+
+ if ($this->option('policy')) {
+ $this->makePolicy(name: $filename, module: $module);
+ }
+ }
+
+ // private function makeAll()
+ // {
+ // //TODO: makeAll() implementation
+ // }
+
+ private function makeController(string $name, string $module): void
+ {
+ $args = [
+ 'name' => "{$name}Controller",
+ '--module' => $module,
+ ];
+
+ if ($this->option('api')) {
+ $args['--api'] = true;
+ }
+
+ if ($this->option('invokable')) {
+ $args['--invokable'] = true;
+ }
+
+ if ($this->option('resource')) {
+ $args['--resource'] = true;
+ }
+
+ $this->call(
+ command: 'module:make:controller',
+ arguments: $args
+ );
+ }
+
+ private function makeMigration(string $name, string $module): void
+ {
+ $this->call(
+ command: 'module:make:migration',
+ arguments: [
+ 'name' => "create_{$this->getPluralName($name)}_table",
+ '--create' => $name,
+ '--module' => $module,
+ ]
+ );
+ }
+
+ private function makePolicy(string $name, string $module)
+ {
+ $this->call(
+ command: 'module:make:policy',
+ arguments: [
+ 'name' => $name,
+ '--module' => $module,
+ '--model' => $name,
+ ]
+ );
+ }
+
+ // private function makeFactory()
+ // {
+ // //TODO: makeFactory() implementation
+ // }
+
+ // private function makeSeed()
+ // {
+ // //TODO: makeSeed() implementation
+ // }
+
+ protected function getFolderPath(): string
+ {
+ return 'Models';
+ }
+
+ protected function setStubFile(string $file): void
+ {
+ $this->currentStub = $this->currentStub.$file.'sample';
+ }
+
+ /**
+ * Get the console command arguments.
+ *
+ * @return array
+ */
+ protected function getArguments()
+ {
+ return [
+ ['name', InputArgument::REQUIRED, 'The name of the model'],
+ ];
+ }
+}
diff --git a/src/Console/Commands/ModuleMakeNotificationCommand.php b/src/Console/Commands/ModuleMakeNotificationCommand.php
new file mode 100644
index 0000000..58f5a26
--- /dev/null
+++ b/src/Console/Commands/ModuleMakeNotificationCommand.php
@@ -0,0 +1,62 @@
+getModuleInput();
+ $filename = Str::studly($this->getNameInput());
+ $folder = $this->getFolderPath();
+
+ $name = $this->qualifyClass($module.'\\'.$folder.'\\'.$filename);
+
+ if ($this->files->exists($path = $this->getPath($name))) {
+ $this->logFileExist($name);
+
+ return true;
+ }
+
+ $this->setStubFile('notification.');
+ $this->makeDirectory($path);
+
+ $this->files->put($path, $this->buildClass($name));
+
+ $this->logFileCreated($name);
+
+ return true;
+ }
+
+ protected function getFolderPath(): string
+ {
+ return 'Notifications';
+ }
+}
diff --git a/src/Console/Commands/ModuleMakePolicyCommand.php b/src/Console/Commands/ModuleMakePolicyCommand.php
new file mode 100644
index 0000000..b39ec37
--- /dev/null
+++ b/src/Console/Commands/ModuleMakePolicyCommand.php
@@ -0,0 +1,78 @@
+getModuleInput();
+ $filename = Str::studly($this->getNameInput());
+ $folder = $this->getFolderPath();
+
+ $type = '';
+
+ if ($model = $this->option('model')) {
+ $type = 'model.';
+ $model = $this->qualifyClass($module.'\\'.'Models'.'\\'.$model);
+ }
+
+ $name = $this->qualifyClass($module.'\\'.$folder.'\\'.$filename);
+
+ if ($this->files->exists($path = $this->getPath($name))) {
+ $this->logFileExist($name);
+
+ return true;
+ }
+
+ $this->setStubFile("policy.{$type}");
+ $this->makeDirectory($path);
+
+ $stub = $this->buildClass($name);
+
+ if ($model) {
+ $this->files->put($path, $this->buildModel($stub, $model));
+ $this->logFileCreated($name);
+
+ return true;
+ }
+
+ $this->files->put($path, $stub);
+
+ $this->logFileCreated($name);
+
+ return true;
+ }
+
+ protected function getFolderPath(): string
+ {
+ return 'Policies';
+ }
+}
diff --git a/src/Console/Commands/ModuleMakeProviderCommand.php b/src/Console/Commands/ModuleMakeProviderCommand.php
new file mode 100644
index 0000000..145418c
--- /dev/null
+++ b/src/Console/Commands/ModuleMakeProviderCommand.php
@@ -0,0 +1,60 @@
+getModuleInput();
+ $filename = Str::studly($this->getNameInput());
+ $folder = $this->getFolderPath();
+
+ $name = $this->qualifyClass($module.'\\'.$folder.'\\'.$filename);
+
+ if ($this->files->exists($path = $this->getPath($name))) {
+ $this->logFileExist($name);
+
+ return true;
+ }
+
+ $this->setStubFile('provider.');
+ $this->makeDirectory($path);
+
+ $this->files->put($path, $this->buildClass($name));
+
+ $this->logFileCreated($name);
+
+ return true;
+ }
+
+ protected function getFolderPath(): string
+ {
+ return 'Providers';
+ }
+}
diff --git a/src/Console/Commands/ModuleMakeRequestCommand.php b/src/Console/Commands/ModuleMakeRequestCommand.php
new file mode 100644
index 0000000..8d0dec6
--- /dev/null
+++ b/src/Console/Commands/ModuleMakeRequestCommand.php
@@ -0,0 +1,60 @@
+getModuleInput();
+ $filename = Str::studly($this->getNameInput());
+ $folder = $this->getFolderPath();
+
+ $name = $this->qualifyClass($module.'\\'.$folder.'\\'.$filename);
+
+ if ($this->files->exists($path = $this->getPath($name))) {
+ $this->logFileExist($name);
+
+ return true;
+ }
+
+ $this->setStubFile('request.');
+ $this->makeDirectory($path);
+
+ $this->files->put($path, $this->buildClass($name));
+
+ $this->logFileCreated($name);
+
+ return true;
+ }
+
+ protected function getFolderPath(): string
+ {
+ return 'Requests';
+ }
+}
diff --git a/src/Console/Commands/ModuleMakeResourceCommand.php b/src/Console/Commands/ModuleMakeResourceCommand.php
new file mode 100644
index 0000000..320aee8
--- /dev/null
+++ b/src/Console/Commands/ModuleMakeResourceCommand.php
@@ -0,0 +1,66 @@
+getModuleInput();
+ $filename = Str::studly($this->getNameInput());
+ $folder = $this->getFolderPath();
+
+ $name = $this->qualifyClass($module.'\\'.$folder.'\\'.$filename);
+
+ if ($this->files->exists($path = $this->getPath($name))) {
+ $this->logFileExist($name);
+
+ return true;
+ }
+
+ $type = '';
+ if ($this->option('collection')) {
+ $type = 'collection.';
+ }
+
+ $this->setStubFile("resource.{$type}");
+ $this->makeDirectory($path);
+
+ $this->files->put($path, $this->buildClass($name));
+
+ $this->logFileCreated($name);
+
+ return true;
+ }
+
+ protected function getFolderPath(): string
+ {
+ return 'Resources';
+ }
+}
diff --git a/src/Console/Commands/ModuleMakeTestCommand.php b/src/Console/Commands/ModuleMakeTestCommand.php
new file mode 100644
index 0000000..48ca8ab
--- /dev/null
+++ b/src/Console/Commands/ModuleMakeTestCommand.php
@@ -0,0 +1,181 @@
+getModuleInput();
+ $filename = Str::studly($this->getNameInput());
+ $folder = $this->getFolderPath();
+
+ $testType = 'Feature';
+ $prefix = 'test';
+ $type = '';
+
+ if ($this->option('unit')) {
+ $type = 'unit.';
+ $testType = 'Unit';
+ }
+
+ if ($this->option('view')) {
+ $filename = collect(explode('/', $filename))
+ ->map(fn ($name) => ucwords($name))
+ ->join('/');
+
+ $filename = "View/{$filename}Test";
+ $type = 'view.';
+ $testType = 'Feature';
+
+ if ($this->option('pest')) {
+ $prefix = 'pest';
+ }
+ }
+
+ $name = $this->qualifyClass($module.'\\'.$folder.'\\'.$testType.'\\'.$filename);
+
+ if ($this->files->exists($path = $this->getPath($name))) {
+ $this->logFileExist($name);
+
+ return true;
+ }
+
+ if ($this->option('pest')) {
+ $prefix = 'pest';
+ }
+
+ $this->setStubFile("{$prefix}.{$type}");
+ $this->makeDirectory($path);
+
+ $this->files->put($path, $this->buildClass($name));
+
+ $this->logFileCreated($name);
+
+ $this->updatePhpUnitXmlFile();
+ }
+
+ protected function getFolderPath(): string
+ {
+ return 'Tests';
+ }
+
+ /**
+ * Update phpunit.xml file with Module test directories
+ */
+ protected function updatePhpUnitXmlFile()
+ {
+ $path = base_path('phpunit.xml.dist');
+
+ if (! $this->files->exists($path)) {
+ $path = base_path('phpunit.xml');
+
+ if (! $this->files->exists($path)) {
+ return;
+ }
+ }
+
+ /** @var \SimpleXMLElement */
+ $xml = simplexml_load_file($path);
+ $dom = $this->createDomDocument($xml);
+ $rootDirectory = $this->getModuleRootDirectory();
+ $updateDocument = false;
+
+ // Specify the testsuite name and attribute to check
+ $testSuiteName = 'Module Feature';
+ $testSuiteDirectory = "./{$rootDirectory}/**/Tests/Feature";
+ $testSuite = $xml->xpath("//testsuite[@name='{$testSuiteName}']");
+
+ // Check if the attribute already exists for the child element
+ if (! $testSuite || (string) $testSuite[0]->directory != $testSuiteDirectory) {
+ $dom = $this->updateDocument($dom, $testSuiteName, $testSuiteDirectory, $path);
+ $updateDocument = true;
+ }
+
+ $testSuiteName = 'Module Unit';
+ $testSuiteDirectory = "./{$rootDirectory}/**/Tests/Unit";
+ $testSuite = $xml->xpath("//testsuite[@name='{$testSuiteName}']");
+
+ if (! $testSuite || (string) $testSuite[0]->directory != $testSuiteDirectory) {
+ $dom = $this->updateDocument($dom, $testSuiteName, $testSuiteDirectory, $path);
+ $updateDocument = true;
+ }
+
+ if ($updateDocument) {
+ $this->saveDomDocument($dom, $path);
+ $this->components->info(sprintf('[%s] updated successfully.', $path));
+ }
+ }
+
+ private function createDomDocument(\SimpleXMLElement $xml): DOMDocument
+ {
+ // Create a new DOMDocument
+ $dom = new DOMDocument('1.0');
+ $dom->preserveWhiteSpace = false;
+ $dom->formatOutput = true;
+
+ // Import the SimpleXML object into the DOMDocument
+ $dom->loadXML($xml->asXML());
+
+ return $dom;
+ }
+
+ /**
+ * Save the modified DOMDocument back to the phpunit.xml file
+ */
+ private function saveDomDocument(DOMDocument $dom, string $path): void
+ {
+ $dom->save($path);
+ }
+
+ private function updateDocument(DOMDocument $dom, string $testSuiteName, string $testSuiteDirectory, string $path): DOMDocument
+ {
+ // Get the root element ()
+ $testSuites = $dom->getElementsByTagName('testsuites')->item(0);
+ // Create a new element
+ $newTestSuite = $dom->createElement('testsuite');
+ $newTestSuite->setAttribute('name', $testSuiteName);
+
+ // Create a new element inside
+ $newDirectory = $dom->createElement('directory', $testSuiteDirectory);
+ $domAttribute = $dom->createAttribute('suffix');
+ $domAttribute->value = 'Test.php';
+
+ $newDirectory->appendChild($domAttribute);
+ $newTestSuite->appendChild($newDirectory);
+
+ // Append the new to
+ $testSuites->appendChild($newTestSuite);
+
+ return $dom;
+ }
+}
diff --git a/src/Console/Commands/ModuleMakeViewCommand.php b/src/Console/Commands/ModuleMakeViewCommand.php
new file mode 100644
index 0000000..4b6a6ea
--- /dev/null
+++ b/src/Console/Commands/ModuleMakeViewCommand.php
@@ -0,0 +1,87 @@
+getModuleInput();
+ $filename = $this->getNameInput();
+ $folder = $this->getFolderPath();
+
+ $name = $this->qualifyClass($module.'\\'.$folder.'\\'.$filename);
+
+ if ($this->files->exists($path = $this->getPath($name, 'blade.php'))) {
+ $this->logFileExist($name);
+
+ return true;
+ }
+
+ $type = '';
+
+ $this->setStubFile("view.{$type}");
+ $this->makeDirectory($path);
+ $this->files->put($path, $this->buildClass($name));
+
+ $this->logFileCreated($name);
+
+ $folder = 'Tests/Feature';
+ if ($this->option('pest')) {
+ $this->call(
+ ModuleMakeTestCommand::class,
+ [
+ 'name' => $filename,
+ '--module' => $module,
+ '--pest' => true,
+ '--view' => true,
+ ]
+ );
+ $type = 'pest.';
+ }
+
+ if ($this->option('test')) {
+ $this->call(
+ ModuleMakeTestCommand::class,
+ [
+ 'name' => $filename,
+ '--module' => $module,
+ '--view' => true,
+ ]
+ );
+ $type = 'test.';
+ }
+
+ return true;
+ }
+
+ protected function getFolderPath(): string
+ {
+ return 'Views';
+ }
+}
diff --git a/src/Console/Commands/ModuleMakerCommand.php b/src/Console/Commands/ModuleMakerCommand.php
new file mode 100644
index 0000000..412ba0c
--- /dev/null
+++ b/src/Console/Commands/ModuleMakerCommand.php
@@ -0,0 +1,389 @@
+currentStub;
+ }
+
+ private function getTemplatePath(string $file): string
+ {
+ return __DIR__."/templates/{$file}sample";
+ }
+
+ public function getModuleInput(): string
+ {
+ if (! $this->module = $this->option('module')) {
+ $this->module = $this->ask('What is the name of the module?');
+ }
+
+ return $this->module;
+ }
+
+ /**
+ * Build the class with the given name.
+ *
+ * @param string $name
+ * @return string
+ *
+ * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
+ */
+ protected function buildClass($name)
+ {
+ $stub = $this->files->get($this->getStub());
+
+ return $this->replaceName($stub, $this->getNameInput())
+ ->replaceModuleName($stub, $this->module)
+ ->replaceNamespace($stub, $name)
+ ->replaceClass($stub, $name);
+ }
+
+ protected function buildModel(string $stub, string $model): string
+ {
+ return $this->replaceModelName($stub, class_basename($model))
+ ->replaceModelNamespace($stub, $model)
+ ->replaceModelClass($stub, $model);
+ }
+
+ protected function buildClassWithModel(string $name, string $model): string
+ {
+ $stub = $this->files->get($this->getStub());
+
+ $stub = $this->replaceModelName($stub, $this->getNameInput())
+ ->replaceModelNamespace($stub, $model)
+ ->replaceModelClass($stub, $model);
+
+ return $this->replaceName($stub, $this->getNameInput())
+ ->replaceNamespace($stub, $name)
+ ->replaceClass($stub, $name);
+ }
+
+ /**
+ * Replace the namespace for the given stub.
+ *
+ * @param string $stub
+ * @param string $name
+ * @return $this
+ */
+ protected function replaceNamespace(&$stub, $name): self
+ {
+ $stub = str_replace(
+ search: [
+ '{{ namespace }}',
+ '{{namespace}}',
+ 'SampleNamespace',
+ '{{ rootNamespace }}',
+ '{{rootNamespace}}',
+ 'SampleRootNamespace',
+ 'NamespacedDummyUserModel',
+ '{{ namespacedUserModel }}',
+ '{{namespacedUserModel}}',
+ ],
+ replace: [
+ $this->getNamespace($name),
+ $this->getNamespace($name),
+ $this->getNamespace($name),
+ $this->rootNamespace(),
+ $this->rootNamespace(),
+ $this->rootNamespace(),
+ $this->userProviderModel(),
+ $this->userProviderModel(),
+ $this->userProviderModel(),
+ ],
+ subject: $stub
+ );
+
+ return $this;
+ }
+
+ /**
+ * Replace the name for the given stub.
+ *
+ * @param string $stub
+ * @param string $name
+ * @return $this
+ */
+ protected function replaceName(&$stub, $name)
+ {
+ $title = $name;
+
+ $stub = str_replace(
+ search: [
+ 'SampleTitle',
+ 'SampleViewTitle',
+ 'SampleUCtitle',
+ '{{viewFile}}',
+ '{{command}}',
+ ],
+ replace: [
+ strtolower($name),
+ strtolower(Str::snake($title, '-')),
+ ucfirst(Str::studly($name)),
+ strtolower(Str::snake(str_replace(search: '/', replace: '.', subject: $title), '-')),
+ strtolower(str_replace(search: '/-', replace: ':', subject: Str::snake($title, '-'))),
+ ],
+ subject: $stub
+ );
+
+ $stub = $this->removePrefixFromRoutes($stub);
+
+ return $this;
+ }
+
+ protected function replaceModuleName(&$stub, $name)
+ {
+ $stub = str_replace(
+ search: [
+ '{{ moduleName }}',
+ '{{moduleName}}',
+ 'moduleName',
+ ],
+ replace: strtolower($name),
+ subject: $stub
+ );
+
+ return $this;
+ }
+
+ /**
+ * Remove prefix from routes when there its not a module group
+ *
+ * @return mixed
+ */
+ private function removePrefixFromRoutes(&$stub)
+ {
+ return str_replace(
+ search: "'prefix' => 'SampleModuleGroup', ",
+ replace: '',
+ subject: $stub
+ );
+ }
+
+ /**
+ * Replace the class name for the given stub.
+ *
+ * @param string $stub
+ * @param string $name
+ * @return string
+ */
+ protected function replaceClass($stub, $name)
+ {
+ $class = class_basename($name);
+
+ return str_replace(
+ search: [
+ '{{ class }}',
+ '{{class}}',
+ 'SampleClass',
+ ],
+ replace: [
+ $class,
+ $class,
+ $class,
+ ],
+ subject: $stub
+ );
+ }
+
+ protected function replaceModelClass($stub, $name): string
+ {
+ $class = class_basename($name);
+ $user = class_basename($this->userProviderModel());
+
+ return str_replace(
+ search: [
+ '{{ model }}',
+ '{{model}}',
+ 'model',
+ '{{ event }}',
+ '{{event}}',
+ '{{ user }}',
+ '{{user}}',
+ ],
+ replace: [
+ $class,
+ $class,
+ $class,
+ $class,
+ $class,
+ $user,
+ $user,
+ ],
+ subject: $stub
+ );
+ }
+
+ protected function replaceModelNamespace(&$stub, $name)
+ {
+ $stub = str_replace(
+ search: [
+ '{{ namespacedModel }}',
+ '{{namespacedModel}}',
+ '{{ eventNamespace }}',
+ '{{eventNamespace}}',
+ ],
+ replace: $this->getModelNamespace($name),
+ subject: $stub
+ );
+
+ return $this;
+ }
+
+ protected function replaceModelName(&$stub, $name): self
+ {
+ $stub = str_replace(
+ search: [
+ '{{ modelVariable }}',
+ '{{modelVariable}}',
+ ],
+ replace: [
+ Str::of($name)->lower(),
+ Str::of($name)->lower(),
+ ],
+ subject: $stub
+ );
+
+ return $this;
+ }
+
+ /**
+ * Get the full namespace name for a given class.
+ *
+ * @param string $name
+ * @return string
+ */
+ protected function getNamespace($name)
+ {
+ $name = str_replace(search: '\\routes\\', replace: '\\', subject: $name);
+
+ return trim(
+ implode(
+ '\\',
+ array_map(
+ 'ucfirst',
+ array_slice(explode('\\', Str::studly($name)), 0, -1)
+ )
+ ),
+ '\\'
+ );
+ }
+
+ protected function getModelNamespace($name)
+ {
+ return trim(
+ implode(
+ '\\',
+ array_map(
+ 'ucfirst',
+ array_slice(explode('\\', Str::studly($name)), 0)
+ )
+ ),
+ '\\'
+ );
+ }
+
+ protected function getPluralName(string $name): string
+ {
+ return Str::of($name)
+ ->plural()
+ ->snake();
+ }
+
+ protected function getFilePath(string $name, bool $force = false): ?string
+ {
+ if ($this->files->exists($path = $this->getPath($name)) && ! $force) {
+ $this->logFileExist($name);
+
+ return null;
+ }
+
+ return $path;
+ }
+
+ protected function generateFile(string $path, string $filename, string $stubType = ''): void
+ {
+ $stubPrefix = strtolower($this->type);
+ $this->setStubFile("{$stubPrefix}.{$stubType}");
+ $this->makeDirectory($path);
+
+ $stub = $this->buildClass($filename);
+
+ $this->files->put($path, $stub);
+
+ $this->logFileCreated($filename);
+ }
+
+ protected function setStubFile(string $file): void
+ {
+ $this->currentStub = $this->getTemplatePath($file);
+ }
+
+ protected function logFileCreated(string $path, ?string $type = null)
+ {
+ if (! $this->hasOption('quiet') || ! $this->option('quiet')) {
+ $this->components->info(sprintf('%s [%s] created successfully.', $type ?? $this->type, $path));
+ }
+ }
+
+ protected function logFileExist(string $path)
+ {
+ if (! $this->hasOption('quiet') || ! $this->option('quiet')) {
+ $this->components->error(sprintf('%s [%s] already exist.', $this->type, $path));
+ }
+ }
+
+ /**
+ * Get the destination class path.
+ *
+ * @param string $name
+ * @return string
+ */
+ protected function getPath($name, string $fileExtension = 'php')
+ {
+ $name = Str::replaceFirst($this->rootNamespace(), '', $name);
+
+ return $this->getModuleRootPath().'/'.str_replace('\\', '/', $name).".{$fileExtension}";
+ }
+
+ protected function getModuleRootPath(): string
+ {
+ return base_path(config('modularize.root_path'));
+ }
+
+ protected function getModuleRootDirectory(): string
+ {
+ return config('modularize.root_path');
+ }
+
+ /**
+ * Get the root namespace for the class.
+ *
+ * @return string
+ */
+ protected function rootNamespace()
+ {
+ return config('modularize.root_path').'\\';
+ }
+
+ abstract protected function getFolderPath(): string;
+}
diff --git a/src/Console/Commands/templates/console.sample b/src/Console/Commands/templates/console.sample
new file mode 100644
index 0000000..dac6fe4
--- /dev/null
+++ b/src/Console/Commands/templates/console.sample
@@ -0,0 +1,30 @@
+
+ */
+ public function broadcastOn(): array
+ {
+ return [
+ new PrivateChannel('channel-name'),
+ ];
+ }
+}
diff --git a/src/Console/Commands/templates/job.sample b/src/Console/Commands/templates/job.sample
new file mode 100644
index 0000000..bc67adc
--- /dev/null
+++ b/src/Console/Commands/templates/job.sample
@@ -0,0 +1,31 @@
+id();
+ $table->timestamps();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('{{ table }}');
+ }
+};
diff --git a/src/Console/Commands/templates/migration.sample b/src/Console/Commands/templates/migration.sample
new file mode 100644
index 0000000..168c622
--- /dev/null
+++ b/src/Console/Commands/templates/migration.sample
@@ -0,0 +1,27 @@
+id();
+ $table->timestamps();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('{{ table }}');
+ }
+};
diff --git a/src/Console/Commands/templates/migration.update.sample b/src/Console/Commands/templates/migration.update.sample
new file mode 100644
index 0000000..0f08705
--- /dev/null
+++ b/src/Console/Commands/templates/migration.update.sample
@@ -0,0 +1,25 @@
+
+ */
+ public function via(object $notifiable): array
+ {
+ return ['mail'];
+ }
+
+ /**
+ * Get the mail representation of the notification.
+ */
+ public function toMail(object $notifiable): MailMessage
+ {
+ return (new MailMessage)
+ ->line('The introduction to the notification.')
+ ->action('Notification Action', url('/'))
+ ->line('Thank you for using our application!');
+ }
+
+ /**
+ * Get the array representation of the notification.
+ *
+ * @return array
+ */
+ public function toArray(object $notifiable): array
+ {
+ return [
+ //
+ ];
+ }
+}
diff --git a/src/Console/Commands/templates/pest.sample b/src/Console/Commands/templates/pest.sample
new file mode 100644
index 0000000..b46239f
--- /dev/null
+++ b/src/Console/Commands/templates/pest.sample
@@ -0,0 +1,7 @@
+get('/');
+
+ $response->assertStatus(200);
+});
diff --git a/src/Console/Commands/templates/pest.unit.sample b/src/Console/Commands/templates/pest.unit.sample
new file mode 100644
index 0000000..61cd84c
--- /dev/null
+++ b/src/Console/Commands/templates/pest.unit.sample
@@ -0,0 +1,5 @@
+toBeTrue();
+});
diff --git a/src/Console/Commands/templates/pest.view.sample b/src/Console/Commands/templates/pest.view.sample
new file mode 100644
index 0000000..303772c
--- /dev/null
+++ b/src/Console/Commands/templates/pest.view.sample
@@ -0,0 +1,9 @@
+view('{{moduleName}}::{{viewFile}}', [
+ //
+ ]);
+
+ $contents->assertSee('');
+});
diff --git a/src/Console/Commands/templates/policy.model.sample b/src/Console/Commands/templates/policy.model.sample
new file mode 100644
index 0000000..ffe9ce1
--- /dev/null
+++ b/src/Console/Commands/templates/policy.model.sample
@@ -0,0 +1,66 @@
+
*/
- public function rules()
+ public function rules(): array
{
return [
//
diff --git a/src/Console/Commands/templates/resource.collection.sample b/src/Console/Commands/templates/resource.collection.sample
new file mode 100644
index 0000000..f75e481
--- /dev/null
+++ b/src/Console/Commands/templates/resource.collection.sample
@@ -0,0 +1,19 @@
+
+ */
+ public function toArray(Request $request): array
+ {
+ return parent::toArray($request);
+ }
+}
diff --git a/src/Console/Commands/templates/resource.sample b/src/Console/Commands/templates/resource.sample
new file mode 100644
index 0000000..ce8ece4
--- /dev/null
+++ b/src/Console/Commands/templates/resource.sample
@@ -0,0 +1,19 @@
+
+ */
+ public function toArray(Request $request): array
+ {
+ return parent::toArray($request);
+ }
+}
diff --git a/src/Console/Commands/templates/test.sample b/src/Console/Commands/templates/test.sample
new file mode 100644
index 0000000..f6f79fd
--- /dev/null
+++ b/src/Console/Commands/templates/test.sample
@@ -0,0 +1,20 @@
+get('/');
+
+ $response->assertStatus(200);
+ }
+}
diff --git a/src/Console/Commands/templates/test.unit.sample b/src/Console/Commands/templates/test.unit.sample
new file mode 100644
index 0000000..7accf88
--- /dev/null
+++ b/src/Console/Commands/templates/test.unit.sample
@@ -0,0 +1,16 @@
+assertTrue(true);
+ }
+}
diff --git a/src/Console/Commands/templates/test.view.sample b/src/Console/Commands/templates/test.view.sample
new file mode 100644
index 0000000..8656db5
--- /dev/null
+++ b/src/Console/Commands/templates/test.view.sample
@@ -0,0 +1,20 @@
+view('{{moduleName}}::{{viewFile}}', [
+ //
+ ]);
+
+ $contents->assertSee('');
+ }
+}
diff --git a/src/Console/Commands/templates/view-component.inline.sample b/src/Console/Commands/templates/view-component.inline.sample
new file mode 100644
index 0000000..e5ecb65
--- /dev/null
+++ b/src/Console/Commands/templates/view-component.inline.sample
@@ -0,0 +1,26 @@
+
+
+
+
+ {{-- {{ __('{module}::messages.welcome') }} --}}
+
\ No newline at end of file
diff --git a/src/MigrationMaker.php b/src/MigrationMaker.php
new file mode 100644
index 0000000..f6225f6
--- /dev/null
+++ b/src/MigrationMaker.php
@@ -0,0 +1,141 @@
+files = $files;
+ $this->customStubPath = $customStubPath;
+ }
+
+ public static function make(Filesystem $files, string $customStubPath): self
+ {
+ return new self($files, $customStubPath);
+
+ }
+
+ public function create(string $table, string $destinationPath, bool $create = false)
+ {
+ $table = $this->getName($table);
+ $stub = self::getStub($table, $create);
+ $path = self::getPath($table, $this->classPath($destinationPath));
+
+ $this->files->put(
+ $path, $this->populateStub($stub, $table)
+ );
+
+ return $path;
+ }
+
+ /**
+ * Get the migration stub file.
+ *
+ * @param string|null $table
+ * @param bool $create
+ * @return string
+ */
+ protected function getStub($table, $create)
+ {
+ if (is_null($table)) {
+ $stub = $this->files->exists($customPath = $this->customStubPath.'/migration.sample')
+ ? $customPath
+ : $this->stubPath().'/migration.stub';
+ } elseif ($create) {
+ $stub = $this->files->exists($customPath = $this->customStubPath.'/migration.create.sample')
+ ? $customPath
+ : $this->stubPath().'/migration.create.stub';
+ } else {
+ $stub = $this->files->exists($customPath = $this->customStubPath.'/migration.update.sample')
+ ? $customPath
+ : $this->stubPath().'/migration.update.sample';
+ }
+
+ return $this->files->get($stub);
+ }
+
+ /**
+ * Populate the place-holders in the migration stub.
+ *
+ * @param string $stub
+ * @param string|null $table
+ * @return string
+ */
+ protected function populateStub($stub, $table)
+ {
+ // Here we will replace the table place-holders with the table specified by
+ // the developer, which is useful for quickly creating a tables creation
+ // or update migration from the console instead of typing it manually.
+ if (! is_null($table)) {
+ $stub = str_replace(
+ ['DummyTable', '{{ table }}', '{{table}}'],
+ $table, $stub
+ );
+ }
+
+ return $stub;
+ }
+
+ private function getName(string $name): string
+ {
+ return Str::of($name)
+ ->plural()
+ ->snake();
+ }
+
+ /**
+ * Get the full path to the migration.
+ *
+ * @param string $name
+ * @param string $path
+ * @return string
+ */
+ protected function getPath($name, $path)
+ {
+ return $path.$this->getDatePrefix().'_create_'.$name.'_table.php';
+ }
+
+ /**
+ * Get the destination class path.
+ *
+ * @param string $name
+ * @return string
+ */
+ protected function classPath($name)
+ {
+ $name = Str::replaceFirst('App\\', '', $name);
+
+ return app_path().'/'.str_replace('\\', '/', $name);
+ }
+
+ /**
+ * Get the date prefix for the migration.
+ *
+ * @return string
+ */
+ protected function getDatePrefix()
+ {
+ return date('Y_m_d_His');
+ }
+
+ /**
+ * Get the path to the stubs.
+ *
+ * @return string
+ */
+ public function stubPath()
+ {
+ return __DIR__.'/stubs';
+ }
+}
diff --git a/src/ModularizeServiceProvider.php b/src/ModularizeServiceProvider.php
index d53cd22..9d822bc 100644
--- a/src/ModularizeServiceProvider.php
+++ b/src/ModularizeServiceProvider.php
@@ -1,25 +1,42 @@
-
- * @package Norbybaru\Modularize
- * @version 1.2.2
- * @since 1.0.0
- */
+use NorbyBaru\Modularize\Console\Commands\ModuleMakeComponentCommand;
+use NorbyBaru\Modularize\Console\Commands\ModuleMakeConsoleCommand;
+use NorbyBaru\Modularize\Console\Commands\ModuleMakeControllerCommand;
+use NorbyBaru\Modularize\Console\Commands\ModuleMakeEventCommand;
+use NorbyBaru\Modularize\Console\Commands\ModuleMakeJobCommand;
+use NorbyBaru\Modularize\Console\Commands\ModuleMakeListenerCommand;
+use NorbyBaru\Modularize\Console\Commands\ModuleMakeMiddlewareCommand;
+use NorbyBaru\Modularize\Console\Commands\ModuleMakeMigrationCommand;
+use NorbyBaru\Modularize\Console\Commands\ModuleMakeModelCommand;
+use NorbyBaru\Modularize\Console\Commands\ModuleMakeNotificationCommand;
+use NorbyBaru\Modularize\Console\Commands\ModuleMakePolicyCommand;
+use NorbyBaru\Modularize\Console\Commands\ModuleMakeProviderCommand;
+use NorbyBaru\Modularize\Console\Commands\ModuleMakeRequestCommand;
+use NorbyBaru\Modularize\Console\Commands\ModuleMakeResourceCommand;
+use NorbyBaru\Modularize\Console\Commands\ModuleMakeTestCommand;
+use NorbyBaru\Modularize\Console\Commands\ModuleMakeViewCommand;
+use ReflectionClass;
+use Symfony\Component\Finder\Finder;
+
class ModularizeServiceProvider extends ServiceProvider
{
/** @var Filesystem */
protected $files;
+ protected string $moduleRootPath;
+
+ protected string $rootNamespace = 'Modules\\';
+
/**
* Bootstrap the application services.
*
@@ -27,64 +44,31 @@ class ModularizeServiceProvider extends ServiceProvider
*/
public function boot()
{
+ $this->publishConfig();
- if (is_dir(app_path().'/Modules/')) {
- $modules = config("modules.enable")
- ?: array_map(
- 'class_basename',
- $this->files->directories(app_path().'/Modules/')
- );
-
- foreach ($modules as $key => $module) {
- if (!$this->files->exists(app_path() . '/Modules/' . $module . '/Controllers')) {
- unset($modules[$key]);
-
- $directories = array_map(
- 'class_basename',
- $this->files->directories(app_path().'/Modules/' . $module)
- );
-
- foreach ($directories as $directory) {
- array_push($modules, $module . '/' . $directory);
- }
- }
+ if (is_dir($this->moduleRootPath = base_path(config('modularize.root_path')))) {
+ if (! config('modularize.enable')) {
+ return;
}
- foreach ($modules as $module) {
- // Allow routes to be cached
- if (!$this->app->routesAreCached()) {
- $route_files = [
- app_path() . '/Modules/' . $module . '/routes.php',
- app_path() . '/Modules/' . $module . '/routes/web.php',
- app_path() . '/Modules/' . $module . '/routes/api.php',
- ];
-
- foreach ($route_files as $route_file) {
- if ($this->files->exists($route_file)) {
- include $route_file;
- }
- }
- }
-
- $helper = app_path() . '/Modules/' . $module . '/helper.php';
- $views = app_path() . '/Modules/' . $module . '/Views';
- $trans = app_path() . '/Modules/' . $module . '/Translations';
+ $modules = array_map(
+ 'class_basename',
+ $this->files->directories($this->moduleRootPath)
+ );
- if ($this->files->exists($helper)) {
- include_once $helper;
- }
-
- //Load views
- if ($this->files->isDirectory($views)) {
- $this->loadViewsFrom($views, strtolower(str_replace('.-', '.', Str::snake(str_replace('/', '.', $module), '-'))));
- }
-
- //Load translations
- if ($this->files->isDirectory($trans)) {
- $this->loadTranslationsFrom($trans, strtolower(str_replace('.-', '.', Str::snake(str_replace('/', '.', $module), '-'))));
- }
+ foreach ($modules as $module) {
+ $this->autoloadServiceProvider($this->moduleRootPath, $module);
+ $this->autoloadConfig($this->moduleRootPath, $module);
+ $this->autoloadConsoleCommands($this->moduleRootPath, $module);
+ $this->autoloadMigration($this->moduleRootPath, $module);
+ $this->autoloadRoutes($this->moduleRootPath, $module);
+ $this->autoloadHelper($this->moduleRootPath, $module);
+ $this->autoloadViews($this->moduleRootPath, $module);
+ $this->autoloadTranslations($this->moduleRootPath, $module);
+ $this->autoloadViewComponents($module);
}
}
+
}
/**
@@ -95,20 +79,228 @@ public function boot()
public function register()
{
$this->files = new Filesystem;
- $this->registerMakeCommand();
+
+ $this->mergeConfigFrom($this->configPath(), 'modularize');
+
+ if ($this->app->runningInConsole()) {
+ $this->registerMakeCommand();
+ }
}
/**
- * Register module" console command.
+ * Return config file.
*
+ * @return string
*/
- protected function registerMakeCommand()
+ protected function configPath()
{
- $this->commands('modules.make');
+ return __DIR__.'/../config/modularize.php';
+ }
- $bind_method = method_exists($this->app, 'bindShared') ? 'bindShared' : 'singleton';
- $this->app->{$bind_method}('modules.make', function () {
- return new ModuleCommand($this->files);
+ /**
+ * Publish config file.
+ */
+ protected function publishConfig()
+ {
+ if ($this->app->runningInConsole()) {
+ $this->publishes([
+ $this->configPath() => config_path('modularize.php'),
+ ], 'modularize-config');
+ }
+ }
+
+ private function getModuleNamespace(string $name): string
+ {
+ return Str::of($name)
+ ->replace(search: '/', replace: '.')
+ ->snake('-')
+ ->replace(search: '.-', replace: '.')
+ ->lower();
+ }
+
+ /**
+ * Load module migration files
+ */
+ private function autoloadMigration(string $moduleRootPath, string $module)
+ {
+ $this->loadMigrationsFrom("{$moduleRootPath}/{$module}/Database/migrations");
+ }
+
+ /**
+ * Load module console commands
+ */
+ private function autoloadConsoleCommands(string $moduleRootPath, string $module): void
+ {
+ if (! $this->app->runningInConsole()) {
+ return;
+ }
+
+ $path = "{$moduleRootPath}/{$module}/Console";
+
+ $paths = array_unique(Arr::wrap($path));
+
+ $paths = array_filter($paths, function ($path) {
+ return is_dir($path);
});
+
+ if (empty($paths)) {
+ return;
+ }
+
+ $namespace = $this->rootNamespace;
+
+ foreach ((new Finder)->in($paths)->files() as $command) {
+ $command = $namespace.str_replace(
+ ['/', '.php'],
+ ['\\', ''],
+ Str::after($command->getRealPath(), realpath($this->moduleRootPath).DIRECTORY_SEPARATOR)
+ );
+
+ if (
+ is_subclass_of($command, Command::class)
+ && ! (new ReflectionClass($command))->isAbstract()
+ ) {
+ $this->commands($command);
+ }
+ }
+ }
+
+ /**
+ * Load module config.php file
+ */
+ private function autoloadConfig(string $moduleRootPath, string $module)
+ {
+ $config = "{$moduleRootPath}/{$module}/config.php";
+
+ if ($this->files->exists($config)) {
+ $this->mergeConfigFrom($config, Str::slug($module));
+ }
+ }
+
+ /**
+ * Load and register module service provider
+ */
+ private function autoloadServiceProvider(string $moduleRootPath, string $module)
+ {
+ $provider = "{$module}/Providers/{$module}ServiceProvider.php";
+ $file = "{$moduleRootPath}/$provider";
+ if ($this->files->exists($file)) {
+ $providerNamespace = $this->rootNamespace.str_replace(
+ ['/', '.php'],
+ ['\\', ''],
+ $provider
+ );
+
+ if (
+ is_subclass_of($providerNamespace, ServiceProvider::class)
+ && ! (new ReflectionClass($providerNamespace))->isAbstract()
+ ) {
+ $this->app->register($providerNamespace);
+ }
+ }
+ }
+
+ private function autoloadHelper(string $moduleRootPath, string $module): void
+ {
+ $path = "{$moduleRootPath}/$module/helper.php";
+
+ if ($this->files->exists($path)) {
+ include_once $path;
+ }
+ }
+
+ private function autoloadRoutes(string $moduleRootPath, string $module): void
+ {
+ if (! config('modularize.autoload_routes')) {
+ return;
+ }
+
+ $path = "{$moduleRootPath}/{$module}";
+ if (! ($this->app instanceof CachesRoutes && $this->app->routesAreCached())) {
+ $routeFiles = [
+ $path.'/routes.php',
+ $path.'/Routes/web.php',
+ $path.'/Routes/api.php',
+ ];
+
+ foreach ($routeFiles as $path) {
+ if ($this->files->isDirectory(directory: $path)) {
+ foreach ($this->files->allFiles(directory: $path) as $file) {
+ include $file->getPathname();
+ }
+ } else {
+ if ($this->files->exists(path: $path)) {
+ include $path;
+ }
+ }
+ }
+ }
+ }
+
+ private function autoloadViews(string $moduleRootPath, string $module): void
+ {
+ $path = "{$moduleRootPath}/{$module}/Views";
+
+ if ($this->files->isDirectory(directory: $path)) {
+ $this->loadViewsFrom(
+ path: $path,
+ namespace: $this->getModuleNamespace(name: $module),
+ );
+ }
+ }
+
+ private function autoloadViewComponents(string $module): void
+ {
+ Blade::componentNamespace(
+ "Modules\\{$module}\\Components",
+ $this->getModuleNamespace(name: $module)
+ );
+ }
+
+ private function autoloadTranslations(string $moduleRootPath, string $module): void
+ {
+ $path = "{$moduleRootPath}/{$module}/Lang";
+
+ if ($this->files->isDirectory(directory: $path)) {
+ $this->loadTranslationsFrom(
+ path: $path,
+ namespace: $this->getModuleNamespace(name: $module),
+ );
+ }
+ }
+
+ // protected function loadSeeders($seed_list)
+ // {
+ // $this->callAfterResolving(DatabaseSeeder::class, function ($seeder) use ($seed_list) {
+ // foreach ((array) $seed_list as $path) {
+ // $seeder->call($seed_list);
+ // // here goes the code that will print out in console that the migration was succesful
+ // }
+ // });
+ // }
+
+ /**
+ * Register module" console command.
+ */
+ protected function registerMakeCommand()
+ {
+ $this->commands([
+ ModuleMakeComponentCommand::class,
+ ModuleMakeConsoleCommand::class,
+ ModuleMakeControllerCommand::class,
+ ModuleMakeEventCommand::class,
+ ModuleMakeJobCommand::class,
+ ModuleMakeListenerCommand::class,
+ ModuleMakeModelCommand::class,
+ ModuleMakeMiddlewareCommand::class,
+ ModuleMakeMigrationCommand::class,
+ ModuleMakeNotificationCommand::class,
+ ModuleMakeProviderCommand::class,
+ ModuleMakePolicyCommand::class,
+ ModuleMakeResourceCommand::class,
+ ModuleMakeRequestCommand::class,
+ ModuleMakeTestCommand::class,
+ ModuleMakeViewCommand::class,
+ ]);
}
}
diff --git a/tests/Commands/MakeConsoleCommandTest.php b/tests/Commands/MakeConsoleCommandTest.php
new file mode 100644
index 0000000..66232bc
--- /dev/null
+++ b/tests/Commands/MakeConsoleCommandTest.php
@@ -0,0 +1,68 @@
+artisan(
+ command: 'module:make:console',
+ parameters: [
+ 'name' => 'SendEmails',
+ '--module' => $this->moduleName,
+ ]
+ )->assertSuccessful();
+
+ $this->assertFileExists(filename: $this->getModulePath().'/Console/SendEmails.php');
+ }
+
+ public function test_it_should_create_a_console_command_with_force_option()
+ {
+ $this->artisan(
+ command: 'module:make:console',
+ parameters: [
+ 'name' => 'SendEmails',
+ '--module' => $this->moduleName,
+ ]
+ )->assertSuccessful();
+
+ $this->assertFileExists(filename: $this->getModulePath().'/Console/SendEmails.php');
+
+ $this->artisan(
+ command: 'module:make:console',
+ parameters: [
+ 'name' => 'SendEmails',
+ '--module' => $this->moduleName,
+ '--force' => true,
+ ]
+ )->assertSuccessful();
+
+ $this->assertFileExists(filename: $this->getModulePath().'/Console/SendEmails.php');
+ }
+
+ public function test_it_should_fail_to_create_console_command_on_duplicate_filename()
+ {
+ $this->artisan(
+ command: 'module:make:console',
+ parameters: [
+ 'name' => 'SendEmails',
+ '--module' => $this->moduleName,
+ ]
+ )->assertSuccessful();
+
+ $this->assertFileExists(filename: $this->getModulePath().'/Console/SendEmails.php');
+
+ $this->artisan(
+ command: 'module:make:console',
+ parameters: [
+ 'name' => 'SendEmails',
+ '--module' => $this->moduleName,
+ ]
+ )->assertFailed();
+
+ $this->assertFileExists(filename: $this->getModulePath().'/Console/SendEmails.php');
+ }
+}
diff --git a/tests/Commands/MakeControllerCommandTest.php b/tests/Commands/MakeControllerCommandTest.php
new file mode 100644
index 0000000..bea554e
--- /dev/null
+++ b/tests/Commands/MakeControllerCommandTest.php
@@ -0,0 +1,134 @@
+artisan(
+ command: 'module:make:controller',
+ parameters: [
+ 'name' => 'PostController',
+ '--module' => $this->moduleName,
+ ]
+ )
+ ->assertSuccessful();
+
+ $this->assertFileExists(filename: $this->getModulePath().'/Controllers/PostController.php');
+ }
+
+ public function test_it_should_create_api_controller()
+ {
+ $this->artisan(
+ command: 'module:make:controller',
+ parameters: [
+ 'name' => 'PostController',
+ '--module' => $this->moduleName,
+ '--api' => true,
+ ]
+ )
+ ->assertSuccessful();
+
+ $this->assertFileExists(filename: $this->getModulePath().'/Controllers/PostController.php');
+
+ $controller = $this->files->get(path: $this->getModulePath().'/Controllers/PostController.php');
+
+ $methods = $this->getGeneratedClassMethods($controller);
+
+ $this->assertStringContainsString(needle: 'function index()', haystack: $methods[0]);
+ $this->assertStringContainsString(needle: 'function show(string $id)', haystack: $methods[1]);
+ $this->assertStringContainsString(needle: 'function store(Request $request)', haystack: $methods[2]);
+ $this->assertStringContainsString(needle: 'function update(Request $request, $id)', haystack: $methods[3]);
+ $this->assertStringContainsString(needle: 'function destroy($id)', haystack: $methods[4]);
+ }
+
+ public function test_it_should_create_invokable_controller()
+ {
+ $this->artisan(
+ command: 'module:make:controller',
+ parameters: [
+ 'name' => 'PostController',
+ '--module' => $this->moduleName,
+ '--invokable' => true,
+ ]
+ )
+ ->assertSuccessful();
+
+ $this->assertFileExists(filename: $this->getModulePath().'/Controllers/PostController.php');
+
+ $methods = $this->getGeneratedClassMethods(
+ subjectFile: $this->files
+ ->get(path: $this->getModulePath().'/Controllers/PostController.php')
+ );
+
+ $this->assertStringContainsString(needle: 'function __invoke(Request $request)', haystack: $methods[0]);
+ }
+
+ public function test_it_should_create_resourceful_controller()
+ {
+ $this->artisan(
+ command: 'module:make:controller',
+ parameters: [
+ 'name' => 'PostController',
+ '--module' => $this->moduleName,
+ '--resource' => true,
+ ]
+ )
+ ->assertSuccessful();
+
+ $this->assertFileExists(filename: $this->getModulePath().'/Controllers/PostController.php');
+
+ $methods = $this->getGeneratedClassMethods(
+ subjectFile: $this->files
+ ->get(path: $this->getModulePath().'/Controllers/PostController.php')
+ );
+
+ $this->assertStringContainsString(needle: 'function index()', haystack: $methods[0]);
+ $this->assertStringContainsString(needle: 'function create()', haystack: $methods[1]);
+ $this->assertStringContainsString(needle: 'function store(Request $request)', haystack: $methods[2]);
+ $this->assertStringContainsString(needle: 'function show($id)', haystack: $methods[3]);
+ $this->assertStringContainsString(needle: 'function edit($id)', haystack: $methods[4]);
+ $this->assertStringContainsString(needle: 'function update(Request $request, $id)', haystack: $methods[5]);
+ $this->assertStringContainsString(needle: 'function destroy($id)', haystack: $methods[6]);
+ }
+
+ public function test_it_should_create_controller_in_sub_directory()
+ {
+ $this->artisan(
+ command: 'module:make:controller',
+ parameters: [
+ 'name' => 'Api/CreatePostController',
+ '--module' => $this->moduleName,
+ ]
+ )
+ ->assertSuccessful();
+
+ $this->assertFileExists(filename: $this->getModulePath().'/Controllers/Api/CreatePostController.php');
+ }
+
+ public function test_it_should_not_create_controller_with_duplicate_name()
+ {
+ $this->artisan(
+ command: 'module:make:controller',
+ parameters: [
+ 'name' => 'PostController',
+ '--module' => $this->moduleName,
+ ]
+ )
+ ->assertSuccessful();
+
+ $this->assertFileExists(filename: $this->getModulePath().'/Controllers/PostController.php');
+
+ $this->artisan(
+ command: 'module:make:controller',
+ parameters: [
+ 'name' => 'PostController',
+ '--module' => $this->moduleName,
+ ]
+ )
+ ->assertFailed();
+ }
+}
diff --git a/tests/Commands/MakeMigrationCommandTest.php b/tests/Commands/MakeMigrationCommandTest.php
new file mode 100644
index 0000000..d5c9a6f
--- /dev/null
+++ b/tests/Commands/MakeMigrationCommandTest.php
@@ -0,0 +1,52 @@
+artisan(
+ command: 'module:make:migration',
+ parameters: [
+ 'name' => 'create_posts_table',
+ '--module' => $this->moduleName,
+ ]
+ )
+ ->assertSuccessful();
+
+ $this->assertMigrationFile(module: $this->moduleName, migrationFilename: 'create_posts_table.php');
+ }
+
+ public function test_it_should_create_migration_file_with_create_option()
+ {
+ $this->artisan(
+ command: 'module:make:migration',
+ parameters: [
+ 'name' => 'create_posts_table',
+ '--module' => $this->moduleName,
+ '--create' => 'posts',
+ ]
+ )
+ ->assertSuccessful();
+
+ $this->assertMigrationFile(module: $this->moduleName, migrationFilename: 'create_posts_table.php');
+ }
+
+ public function test_it_should_create_migration_file_with_table_option()
+ {
+ $this->artisan(
+ command: 'module:make:migration',
+ parameters: [
+ 'name' => 'add_title_to_posts_table',
+ '--module' => $this->moduleName,
+ '--table' => 'posts',
+ ]
+ )
+ ->assertSuccessful();
+
+ $this->assertMigrationFile(module: $this->moduleName, migrationFilename: 'add_title_to_posts_table.php');
+ }
+}
diff --git a/tests/Commands/MakeModelCommandTest.php b/tests/Commands/MakeModelCommandTest.php
new file mode 100644
index 0000000..6dbbf8c
--- /dev/null
+++ b/tests/Commands/MakeModelCommandTest.php
@@ -0,0 +1,94 @@
+artisan(
+ command: 'module:make:model',
+ parameters: [
+ 'name' => 'Post',
+ '--module' => $this->moduleName,
+ ]
+ )
+ ->assertSuccessful();
+
+ $this->assertFileExists(filename: $this->getModulePath().'/Models/Post.php');
+ }
+
+ public function test_it_creates_a_model_with_migration()
+ {
+ $this->artisan(
+ command: 'module:make:model',
+ parameters: [
+ 'name' => 'Video',
+ '--module' => $this->moduleName,
+ '--migration' => true,
+ ]
+ )
+ ->assertExitCode(exitCode: 0);
+
+ $this->assertFileExists(filename: $this->getModulePath().'/Models/Video.php');
+
+ $this->assertMigrationFile(module: $this->moduleName, migrationFilename: 'create_videos_table.php');
+ }
+
+ public function test_it_creates_a_model_with_factory()
+ {
+ $this->artisan(
+ command: 'module:make:model',
+ parameters: [
+ 'name' => 'Post',
+ '--module' => $this->moduleName,
+ //'--factory' => true
+ ]
+ )
+ ->assertExitCode(exitCode: 0);
+
+ $this->assertFileExists(filename: $this->getModulePath().'/Models/Post.php');
+ //$this->assertFileExists($this->getModulePath($this->moduleName).'/Database/Factories/PostFactory.php');
+ }
+
+ public function test_it_creates_a_model_with_migration_and_factory()
+ {
+ $module = 'Search';
+ $this->artisan(
+ command: 'module:make:model',
+ parameters: [
+ 'name' => 'Listing',
+ '--module' => $module,
+ '--migration' => true,
+ //'--factory' => true
+ ]
+ )
+ ->assertExitCode(exitCode: 0);
+
+ $this->assertMigrationFile(module: $module, migrationFilename: 'create_listings_table.php');
+
+ $this->assertFileExists(
+ filename: $this->getModulePath($module).'/Models/Listing.php'
+ );
+
+ // TODO: Fix timestamp issue in test
+ //$this->assertFileExists($this->getModulePath($this->moduleName).'/Database/factories/PostFactory.php');
+ }
+
+ public function test_it_creates_a_model_with_migration_and_factory_and_test()
+ {
+ $this->artisan(
+ command: 'module:make:model',
+ parameters: [
+ 'name' => 'Post',
+ '--module' => $this->moduleName,
+ '--migration' => true,
+ '--factory' => true,
+ //'--test' => true
+ ])
+ ->assertExitCode(exitCode: 0);
+
+ }
+}
diff --git a/tests/MakeCommandTestCase.php b/tests/MakeCommandTestCase.php
new file mode 100644
index 0000000..a0abc29
--- /dev/null
+++ b/tests/MakeCommandTestCase.php
@@ -0,0 +1,77 @@
+files = new Filesystem;
+ $this->now = Carbon::now();
+ Carbon::setTestNow(testNow: $this->now);
+ $this->cleanUp();
+ }
+
+ public function teardown(): void
+ {
+ $this->cleanUp();
+ parent::tearDown();
+ }
+
+ public function getModulePath(?string $module = null): string
+ {
+ $module = $module ?? $this->moduleName;
+
+ return parent::getModulePath($module);
+ }
+
+ public function cleanUp(): void
+ {
+ $this->files->deleteDirectory(base_path(config('modularize.root_path')));
+ }
+
+ protected function assertMigrationFile(string $module, string $migrationFilename): void
+ {
+ $migrations = $this->files->allFiles(directory: $this->getModulePath($module).'/Database/migrations');
+ $this->assertNotEmpty(actual: $migrations);
+ $this->assertEquals(
+ expected: 1,
+ actual: count($migrations)
+ );
+
+ /** @var \Symfony\Component\Finder\SplFileInfo */
+ $migrationFile = $migrations[0];
+ $this->assertStringContainsString(
+ needle: $migrationFilename,
+ haystack: $migrationFile->getFilename()
+ );
+ }
+
+ protected function getGeneratedClassMethods(string $subjectFile): array
+ {
+ // Define the regex pattern to match the entire function signature, excluding the opening {
+ $pattern = '/function\s+([a-zA-Z_]\w*)\s*\(([^)]*)\)\s*(?=\{)/';
+ preg_match_all(pattern: $pattern, subject: $subjectFile, matches: $match);
+
+ if (empty($match[0])) {
+ $this->fail('No function found in controller');
+ }
+
+ foreach ($match[0] as $function) {
+ $methods[] = trim($function);
+ }
+
+ return $methods;
+ }
+}
diff --git a/tests/TestCase.php b/tests/TestCase.php
new file mode 100644
index 0000000..fef0127
--- /dev/null
+++ b/tests/TestCase.php
@@ -0,0 +1,21 @@
+