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 @@ +