From ca735f3f9a43156b100f80a83e0a697c6b9591a7 Mon Sep 17 00:00:00 2001 From: stancobridge Date: Mon, 30 May 2022 01:17:40 +0000 Subject: [PATCH] [feature] added modular system --- Modules/BaseController.php | 27 +++ Modules/BaseRepository.php | 11 ++ Modules/BaseValidator.php | 18 ++ Modules/Commands/CommandTrait.php | 42 ++++ Modules/Commands/MakeController.php | 59 ++++++ Modules/Commands/MakeMigration.php | 62 ++++++ Modules/Commands/MakeRequest.php | 59 ++++++ Modules/Commands/ModuleCommands.php | 201 +++++++++++++++++++ Modules/TransformResponse.php | 10 + Modules/stubs/ApiRoutesStub.stub | 28 +++ Modules/stubs/ControllerStub.stub | 57 ++++++ Modules/stubs/CreateMigration.stub | 31 +++ Modules/stubs/Migration.stub | 28 +++ Modules/stubs/Model.stub | 14 ++ Modules/stubs/Repository.stub | 56 ++++++ Modules/stubs/RequestsStub.stub | 30 +++ Modules/stubs/UpdateMigration.stub | 32 +++ Modules/stubs/WebRoutesStub.stub | 18 ++ Modules/stubs/serviceProvider.stub | 51 +++++ README.md | 35 +++- app/Console/Commands/AppName.php | 297 ++++++++++++++++++++++++++++ app/Console/Kernel.php | 14 +- composer.json | 3 +- 23 files changed, 1180 insertions(+), 3 deletions(-) create mode 100644 Modules/BaseController.php create mode 100644 Modules/BaseRepository.php create mode 100644 Modules/BaseValidator.php create mode 100644 Modules/Commands/CommandTrait.php create mode 100644 Modules/Commands/MakeController.php create mode 100644 Modules/Commands/MakeMigration.php create mode 100644 Modules/Commands/MakeRequest.php create mode 100644 Modules/Commands/ModuleCommands.php create mode 100644 Modules/TransformResponse.php create mode 100644 Modules/stubs/ApiRoutesStub.stub create mode 100644 Modules/stubs/ControllerStub.stub create mode 100755 Modules/stubs/CreateMigration.stub create mode 100755 Modules/stubs/Migration.stub create mode 100644 Modules/stubs/Model.stub create mode 100644 Modules/stubs/Repository.stub create mode 100644 Modules/stubs/RequestsStub.stub create mode 100755 Modules/stubs/UpdateMigration.stub create mode 100644 Modules/stubs/WebRoutesStub.stub create mode 100644 Modules/stubs/serviceProvider.stub create mode 100644 app/Console/Commands/AppName.php diff --git a/Modules/BaseController.php b/Modules/BaseController.php new file mode 100644 index 0000000..186a75f --- /dev/null +++ b/Modules/BaseController.php @@ -0,0 +1,27 @@ +json((object)[ + + 'statusCode' => $statusCode, + 'message' => $message, + 'data' => $data, + + ], $statusCode); + } +} diff --git a/Modules/BaseRepository.php b/Modules/BaseRepository.php new file mode 100644 index 0000000..db73e3d --- /dev/null +++ b/Modules/BaseRepository.php @@ -0,0 +1,11 @@ +validated(); + } +} diff --git a/Modules/Commands/CommandTrait.php b/Modules/Commands/CommandTrait.php new file mode 100644 index 0000000..2f46cf8 --- /dev/null +++ b/Modules/Commands/CommandTrait.php @@ -0,0 +1,42 @@ +module; + $module = $module ?? $controllerName; + $controllerStub = File::get(base_path("Modules/stubs/ControllerStub.stub")); + $controllerPath = base_path($this->fromModule("Http/Controllers/{$controllerName}Controller.php")); + $replaceModule = str_replace('{{module}}', $module, $controllerStub); + $replaceClass = str_replace('{{class}}', $controllerName, $replaceModule); + $replaceRepositoryName = str_replace('{{repoName}}', lcfirst($this->module), $replaceClass); + $ucModuleReplace = str_replace('{{uCmodule}}', $ucModule, $replaceRepositoryName); + $underScoreModuleReplace = str_replace('{{underscoreModule}}', strtolower($underScoredName), $ucModuleReplace); + File::put($controllerPath, $underScoreModuleReplace); + } + + + public function fromModule($path) + { + return "Modules/{$this->module}/" . $path; + } + + public function generateRequest($controllerName, $module = null) + { + $controllerName = $controllerName ?? $this->module; + $module = $module ?? $controllerName; + + $requestStub = File::get(base_path("Modules/stubs/RequestsStub.stub")); + $requestPath = base_path($this->fromModule("Http/Requests/{$controllerName}Request.php")); + + $replaceModule = str_replace('{{module}}', $module, $requestStub); + $replaceClass = str_replace('{{class}}', $controllerName, $replaceModule); + + File::put($requestPath, $replaceClass); + } +} diff --git a/Modules/Commands/MakeController.php b/Modules/Commands/MakeController.php new file mode 100644 index 0000000..8d150b6 --- /dev/null +++ b/Modules/Commands/MakeController.php @@ -0,0 +1,59 @@ +module = trim($this->option('module'), "="); + if (!$this->module) { + $this->error("No module selected use -m or --module option to set module"); + return; + } + $className = $this->argument('className'); + + $this->generateControllerFile($className, $this->module); + $this->info("Controller created successfully"); + } +} diff --git a/Modules/Commands/MakeMigration.php b/Modules/Commands/MakeMigration.php new file mode 100644 index 0000000..6585630 --- /dev/null +++ b/Modules/Commands/MakeMigration.php @@ -0,0 +1,62 @@ +option('module'), "="); + if (!$module) { + $this->error("No module selected use -m or --module option to set module"); + return; + } + $className = $this->argument('name'); + $command = "make:migration {$className} --path=Modules/${module}/database/migrations"; + + Artisan::call($command); + + $this->info($command); + } +} diff --git a/Modules/Commands/MakeRequest.php b/Modules/Commands/MakeRequest.php new file mode 100644 index 0000000..4cdac97 --- /dev/null +++ b/Modules/Commands/MakeRequest.php @@ -0,0 +1,59 @@ +module = trim($this->option('module'), "="); + if (!$this->module) { + $this->error("No module selected use -m or --module option to set module"); + return; + } + $className = $this->argument('className'); + + $this->generateRequest($className, $this->module); + $this->info("Controller created successfully"); + } +} diff --git a/Modules/Commands/ModuleCommands.php b/Modules/Commands/ModuleCommands.php new file mode 100644 index 0000000..58e3a83 --- /dev/null +++ b/Modules/Commands/ModuleCommands.php @@ -0,0 +1,201 @@ +ucModule = + + // $this->moduleName = $this->argument('moduleName'); + // $this->namespace = 'Modules\\'; + } + + /** + * Execute the console command. + * + * @return int + */ + public function handle() + { + + $this->module = $this->argument('moduleName'); + $this->ucModule = lcfirst($this->argument('moduleName')); + $this->lcModule = strtolower($this->argument('moduleName')); + + // create undescored names + $this->underScoredName = preg_replace('/([A-Z])/', "_$1", $this->ucModule); + + + // Create module folder + File::makeDirectory("Modules/{$this->module}"); + + $this->modulePath = $this->fromModule(""); + $this->generateServiceProvider(); + $this->generateModel(); + $this->generateDatabaseDir(); + $this->generateHttpDir(); + $this->generateRepository(); + $this->generateControllerFile($this->module, null, $this->ucModule, $this->underScoredName); + $this->generateResources(); + $this->generateRoutes(); + $this->info($this->module . " extracted \n"); + return true; + } + + public function generateServiceProvider() + { + + // Get service provider stub + $serviceProviderStub = base_path('Modules/stubs/serviceProvider.stub'); + // get stub contents + $serviceProvider = File::get($serviceProviderStub); + File::makeDirectory("Modules/{$this->module}/Providers"); + $this->servicePath = $this->getStubPath($this->module); + File::put($this->servicePath, str_replace('{{module}}', $this->module, $serviceProvider)); + + return true; + } + + public function generateModel() + { + + // Get service provider stub + $serviceProviderStub = base_path('Modules/stubs/Model.stub'); + // get stub contents + $model = File::get($serviceProviderStub); + File::makeDirectory("Modules/{$this->module}/Models"); + $this->servicePath = $this->modelPath($this->module); + File::put($this->servicePath, str_replace('{{module}}', $this->module, $model)); + + return true; + } + + protected function modelPath() + { + return base_path("Modules/{$this->module}/Models/{$this->module}.php"); + } + + public function generateRepository() + { + + // Get service provider stub + $repositoryStub = base_path('Modules/stubs/Repository.stub'); + // get stub contents + $repository = File::get($repositoryStub); + // File::makeDirectory("Modules/{$this->module}/Http"); + File::makeDirectory("Modules/{$this->module}/Http/Repositories"); + $this->servicePath = $this->repositoryPath($this->module); + // name all module as variables + $replaceRepovars = str_replace('{{uCmodule}}', $this->ucModule, $repository); + // create all permission + + $underScoredName = str_replace('{{underscoreModule}}', strtolower($this->underScoredName), $replaceRepovars); + + $replacePermissions = str_replace('{{lcModule}}', $this->lcModule, $underScoredName); + File::put($this->servicePath, str_replace('{{module}}', $this->module, $replacePermissions)); + + return true; + } + + protected function repositoryPath() + { + return base_path("Modules/{$this->module}/Http/Repositories/{$this->module}Repository.php"); + } + + protected function getStubPath() + { + return base_path("Modules/{$this->module}/Providers/{$this->module}ServiceProvider.php"); + } + + + protected function generateDatabaseDir() + { + File::makeDirectory(base_path($this->fromModule("database"))); + File::makeDirectory(base_path($this->fromModule("database/migrations"))); + File::makeDirectory(base_path($this->fromModule("database/seeders"))); + File::makeDirectory(base_path($this->fromModule("database/fakers"))); + } + + protected function generateHttpDir() + { + File::makeDirectory(base_path($this->fromModule("Http"))); + File::makeDirectory(base_path($this->fromModule("Http/Requests"))); + File::makeDirectory(base_path($this->fromModule("Http/Controllers"))); + } + + protected function generateModelDir() + { + File::makeDirectory(base_path($this->fromModule("Model"))); + } + + public function generateResources() + { + File::makeDirectory(base_path("Modules/{$this->module}/views")); + } + public function generateRoutes() + { + File::makeDirectory(base_path($this->fromModule("routes"))); + $apiRoutePath = $this->getRouthPath('api'); + + + $apiRouteStub = $this->getRouteStub('Api'); + $replaceModule = str_replace('{{module}}', $this->module, $apiRouteStub); + $replaceLcModule = str_replace('{{lcModule}}', $this->lcModule, $replaceModule); + + + File::put($apiRoutePath, $replaceLcModule); + + $apiRoutePath = $this->getRouthPath('web'); + File::put($apiRoutePath, str_replace('{{module}}', $this->module, $this->getRouteStub('Web'))); + } + + public function getRouthPath($type) + { + return base_path($this->fromModule("/routes/{$type}.php")); + } + + public function getRouteStub($type) + { + return File::get(base_path("Modules/stubs/{$type}RoutesStub.stub")); + } +} diff --git a/Modules/TransformResponse.php b/Modules/TransformResponse.php new file mode 100644 index 0000000..43f45ee --- /dev/null +++ b/Modules/TransformResponse.php @@ -0,0 +1,10 @@ + ['auth:sanctum', /**"role:SuperAdmin"*/], + 'prefix' => 'api', + "namespace" => "{{lcModule}}" + ], function () { + + Route::post('/{{lcModule}}s', [{{module}}Controller::class, 'create{{module}}']); + Route::get('/{{lcModule}}s', [{{module}}Controller::class, 'get{{module}}s']); + Route::get('/{{lcModule}}s/{{{lcModule}}Id}', [{{module}}Controller::class, 'get{{module}}']); + Route::post('/{{lcModule}}s/edit', [{{module}}Controller::class, 'edit{{module}}']); + Route::delete('/{{lcModule}}s/{{{lcModule}}Id}', [{{module}}Controller::class, 'delete{{module}}']); + +}); diff --git a/Modules/stubs/ControllerStub.stub b/Modules/stubs/ControllerStub.stub new file mode 100644 index 0000000..5c8dc4c --- /dev/null +++ b/Modules/stubs/ControllerStub.stub @@ -0,0 +1,57 @@ +validate([ + // 'name' => "required|string" + ]); + + $created{{module}} = $this->{{repoName}}Repository->create{{module}}($request->all()); + + return $this->transformResponse('{{module}} created Successfully', ["{{repoName}}" => $created{{module}}]); + + } + + public function get{{module}}s(Request $request){ + + ${{repoName}}s = $this->{{repoName}}Repository->get{{module}}s($request->all()); + + return $this->transformResponse('{{module}}s Fetched Successfully', ["{{repoName}}" => ${{repoName}}s]); + + } + + public function get{{module}}(${{repoName}}Id){ + + ${{repoName}} = $this->{{repoName}}Repository->get{{module}}(${{repoName}}Id); + + return $this->transformResponse('{{module}} Fetched Successfully', ["{{repoName}}" => ${{repoName}}]); + + } + + public function edit{{module}}(Request $request){ + $request->validate([ + "{{underscoreModule}}_id" => "required|exists:table,id" + ]); + + ${{module}} = $this->{{repoName}}Repository->edit{{module}}($request->all()); + + return $this->transformResponse('{{module}} edited Successfully', ["{{repoName}}" => ${{module}}]); + } + + public function delete{{module}}(${{uCmodule}}Id){ + + ${{module}} = $this->{{repoName}}Repository->delete{{module}}(${{uCmodule}}Id); + + return $this->transformResponse('{{module}} deleted Successfully', ["{{repoName}}" => ${{module}}]); + } +} diff --git a/Modules/stubs/CreateMigration.stub b/Modules/stubs/CreateMigration.stub new file mode 100755 index 0000000..f4a56a0 --- /dev/null +++ b/Modules/stubs/CreateMigration.stub @@ -0,0 +1,31 @@ +id(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('{{ table }}'); + } +} diff --git a/Modules/stubs/Migration.stub b/Modules/stubs/Migration.stub new file mode 100755 index 0000000..fd0e437 --- /dev/null +++ b/Modules/stubs/Migration.stub @@ -0,0 +1,28 @@ + $data['name'], + ]); + + return ${{uCmodule}}; + } + + public function get{{module}}s($data){ + + if (!empty($data['search'])) { + ${{uCmodule}}s = {{module}}::where('name', 'like', "%{$data['search']}%")->orderBy('id', 'desc')->paginate(10); + } else { + ${{uCmodule}}s = {{module}}::orderBy('id', 'desc')->paginate(10); + } + + return ${{uCmodule}}s; + + } + + + public function get{{module}}(int ${{uCmodule}}Id) + { + ${{uCmodule}} = {{module}}::findOrFail(${{uCmodule}}Id); + + return ${{uCmodule}}; + } + + + public function edit{{module}}($data){ + + ${{uCmodule}} = {{module}}::findOrFail($data['{{underscoreModule}}_id']); + + ${{uCmodule}}->save(); + return ${{uCmodule}}; + + } + + public function delete{{module}}(${{underscoreModule}}Id){ + ${{uCmodule}} = {{module}}::findOrFail(${{underscoreModule}}Id); + ${{uCmodule}}->delete(); + return ${{uCmodule}}; + + } + +} diff --git a/Modules/stubs/RequestsStub.stub b/Modules/stubs/RequestsStub.stub new file mode 100644 index 0000000..2ec028d --- /dev/null +++ b/Modules/stubs/RequestsStub.stub @@ -0,0 +1,30 @@ +loadMigrationsFrom(__DIR__."/../database/migrations"); + + // Load Views + + $this->loadViewsFrom(__DIR__."/../views", strtolower('{{module}}')); + + // // Load Routets + $this->loadRoutesFrom(__DIR__."/../routes/web.php"); + $this->loadRoutesFrom(__DIR__ . "/../routes/api.php"); + + } + + /** + * Register any application services. + * + * @return void + */ + public function register() + { + } +} diff --git a/README.md b/README.md index 066a11c..8cd8acd 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![CircleCI](https://circleci.com/gh/hasinhayder/hydra/tree/master.svg?style=svg)](https://circleci.com/gh/hasinhayder/hydra/tree/master) ![GitHub](https://img.shields.io/github/license/hasinhayder/hydra?label=License&style=flat-square) -Hydra is a zero-config API boilerplate with Laravel Sanctum and comes with excellent user and role management API out of the box. Start your next big API project with Hydra, focus on building business logic, and save countless hours of writing boring user and role management API again and again. +Hydra is a zero-config API boilerplate with Laravel Sanctum and comes with excellent user and role management API out of the box. Hydra is has Optional Modular System in-built, which you can use to bootstrap modules faster. Start your next big API project with Hydra, focus on building business logic, and save countless hours of writing boring user and role management API again and again. - [Hydra - Zero Config API Boilerplate with Laravel Sanctum](#hydra---zero-config-api-boilerplate-with-laravel-sanctum) - [Getting Started](#getting-started) @@ -149,6 +149,39 @@ run the following command >>> Role::all() ``` +## Module Documentation +You can easily bootstrap a module, and encapsulate your logic using the Modular Design Pattern, this helps to abstract logic for each sub-system of the project into smaller parts + +to create a module run the following command +### make a module +```shell + php artisan module:make Inventory +``` +this will generate route, controller,model, repository, migration folder and more inside the project `Modules` folder + +create migration for this module + +### create a controller inside a module +```shell + //ModuleName is the name of the module to attach the created controller + php artisan module:controller Inventory -m=ModuleName +``` + +### create a request inside a module +```shell + //ModuleName is the name of the module to attach the created request + php artisan module:request Inventory -m=ModuleName +``` + +### create a migration inside a module +```shell + //ModuleName is the name of the module to attach the created migration + php artisan module:migration create_inventories_table -m=ModuleName +``` + +Finally, open `config/app.php` inside the `providers` array section add the Provider for the new module you created. +You can find the provider for each module inside the `Providers` folder of that particular module, for example, provider for `Shop` will be found inside `Modules/Shop/ShopServiceProvider` + ## Routes Documentation Let's have a look at what Hydra has to offer. Before experimenting with the following API endpoints, run your Hydra project using `php artisan serve` command. For the next part of this documentation, we assumed that Hydra is listening at http://localhost:8000 diff --git a/app/Console/Commands/AppName.php b/app/Console/Commands/AppName.php new file mode 100644 index 0000000..bb53b22 --- /dev/null +++ b/app/Console/Commands/AppName.php @@ -0,0 +1,297 @@ +files = $files; + $this->composer = $composer; + } + + /** + * Execute the console command. + * + * @return void + */ + public function handle() + { + $this->currentRoot = trim($this->laravel->getNamespace(), '\\'); + + $this->setAppDirectoryNamespace(); + $this->setBootstrapNamespaces(); + $this->setConfigNamespaces(); + $this->setComposerNamespace(); + $this->setDatabaseFactoryNamespaces(); + + $this->info('Application namespace set!'); + + $this->composer->dumpAutoloads(); + + $this->call('optimize:clear'); + } + + /** + * Set the namespace on the files in the app directory. + * + * @return void + */ + protected function setAppDirectoryNamespace() + { + $files = Finder::create() + ->in($this->laravel['path']) + ->contains($this->currentRoot) + ->name('*.php'); + + foreach ($files as $file) { + $this->replaceNamespace($file->getRealPath()); + } + } + + /** + * Replace the App namespace at the given path. + * + * @param string $path + * @return void + */ + protected function replaceNamespace($path) + { + $search = [ + 'namespace ' . $this->currentRoot . ';', + $this->currentRoot . '\\', + ]; + + $replace = [ + 'namespace ' . $this->argument('name') . ';', + $this->argument('name') . '\\', + ]; + + $this->replaceIn($path, $search, $replace); + } + + /** + * Set the bootstrap namespaces. + * + * @return void + */ + protected function setBootstrapNamespaces() + { + $search = [ + $this->currentRoot . '\\Http', + $this->currentRoot . '\\Console', + $this->currentRoot . '\\Exceptions', + ]; + + $replace = [ + $this->argument('name') . '\\Http', + $this->argument('name') . '\\Console', + $this->argument('name') . '\\Exceptions', + ]; + + $this->replaceIn($this->getBootstrapPath(), $search, $replace); + } + + /** + * Set the namespace in the appropriate configuration files. + * + * @return void + */ + protected function setConfigNamespaces() + { + $this->setAppConfigNamespaces(); + $this->setAuthConfigNamespace(); + $this->setServicesConfigNamespace(); + } + + /** + * Set the application provider namespaces. + * + * @return void + */ + protected function setAppConfigNamespaces() + { + $search = [ + $this->currentRoot . '\\Providers', + $this->currentRoot . '\\Http\\Controllers\\', + ]; + + $replace = [ + $this->argument('name') . '\\Providers', + $this->argument('name') . '\\Http\\Controllers\\', + ]; + + $this->replaceIn($this->getConfigPath('app'), $search, $replace); + } + + /** + * Set the authentication User namespace. + * + * @return void + */ + protected function setAuthConfigNamespace() + { + $this->replaceIn( + $this->getConfigPath('auth'), + $this->currentRoot . '\\User', + $this->argument('name') . '\\User' + ); + } + + /** + * Set the services User namespace. + * + * @return void + */ + protected function setServicesConfigNamespace() + { + $this->replaceIn( + $this->getConfigPath('services'), + $this->currentRoot . '\\User', + $this->argument('name') . '\\User' + ); + } + + /** + * Set the PSR-4 namespace in the Composer file. + * + * @return void + */ + protected function setComposerNamespace() + { + $this->replaceIn( + $this->getComposerPath(), + str_replace('\\', '\\\\', $this->currentRoot) . '\\\\', + str_replace('\\', '\\\\', $this->argument('name')) . '\\\\' + ); + } + + /** + * Set the namespace in database factory files. + * + * @return void + */ + protected function setDatabaseFactoryNamespaces() + { + $files = Finder::create() + ->in(database_path('factories')) + ->contains($this->currentRoot) + ->name('*.php'); + + foreach ($files as $file) { + $this->replaceIn( + $file->getRealPath(), + $this->currentRoot, + $this->argument('name') + ); + } + } + + /** + * Replace the given string in the given file. + * + * @param string $path + * @param string|array $search + * @param string|array $replace + * @return void + */ + protected function replaceIn($path, $search, $replace) + { + if ($this->files->exists($path)) { + $this->files->put($path, str_replace($search, $replace, $this->files->get($path))); + } + } + + /** + * Get the path to the bootstrap/app.php file. + * + * @return string + */ + protected function getBootstrapPath() + { + return $this->laravel->bootstrapPath() . '/app.php'; + } + + /** + * Get the path to the Composer.json file. + * + * @return string + */ + protected function getComposerPath() + { + return base_path('composer.json'); + } + + /** + * Get the path to the given configuration file. + * + * @param string $name + * @return string + */ + protected function getConfigPath($name) + { + return $this->laravel['path.config'] . '/' . $name . '.php'; + } + + /** + * Get the console command arguments. + * + * @return array + */ + protected function getArguments() + { + return [ + ['name', InputArgument::REQUIRED, 'The desired namespace'], + ]; + } +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index d8bc1d2..ec7d96a 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -7,6 +7,18 @@ use Illuminate\Foundation\Console\Kernel as ConsoleKernel; class Kernel extends ConsoleKernel { + /** + * The Artisan commands provided by your application. + * + * @var array + */ + protected $commands = [ + // + \Modules\Commands\ModuleCommands::class, + \Modules\Commands\MakeController::class, + \Modules\Commands\MakeRequest::class, + \Modules\Commands\MakeMigration::class, + ]; /** * Define the application's command schedule. * @@ -25,7 +37,7 @@ class Kernel extends ConsoleKernel */ protected function commands() { - $this->load(__DIR__.'/Commands'); + $this->load(__DIR__ . '/Commands'); require base_path('routes/console.php'); } diff --git a/composer.json b/composer.json index 164c94b..d4596c7 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,8 @@ }, "autoload-dev": { "psr-4": { - "Tests\\": "tests/" + "Tests\\": "tests/", + "Modules\\": "Modules/" } }, "scripts": {