# Build your own module generator

On this page

The default modules are fine to get started but often you'll have your own structure that you'll use from module to module. It's nice to be able to use your structure as a template for all future modules to be created from.

There is an external package for generating Laravel Modules from a template https://github.com/dcblogdev/laravel-module-generator

There's 2 options here. Edit the stub files or create your own base module and custom artisan command.

Lets explore both options

Stub files

By default, the config/modules.php file has a stubs path that points to the laravel-modules package vendor:

'stubs' => [
    'enabled' => false,
    'path' => base_path() . '/vendor/nwidart/laravel-modules/src/Commands/stubs',

You can change this path to one were you can edit the files. You should never edit files within the vendor folder so instead lets take a copy of the vendor/nwidart/laravel-modules/src/Commands/stubs folder to this path stubs/module

If you do not have a stubs folder then first publish Laravel's stubs:

php artisan stub:publish

This will create a stubs folder containing all the stub files Laravel used when creating classes. Make a folder called module inside stubs.

Ensure you've copied the contents of vendor/nwidart/laravel-modules/src/Commands/stubs into stubs/module now open config/modules.php and edit the path for stubs:

'stubs' => [
   'enabled' => false,
   'path' => base_path() . '/stubs/module',

Now you can edit any of the stubs for instance I like to remove all the docblocks from controllers by default the controllers.stub file contains:

<?php

namespace $CLASS_NAMESPACE$;

use Illuminate\Contracts\Support\Renderable;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;

class $CLASS$ extends Controller
{
    /**
     * Display a listing of the resource.
     * @return Renderable
     */
    public function index()
    {
        return view('$LOWER_NAME$::index');
    }

    /**
     * Show the form for creating a new resource.
     * @return Renderable
     */
    public function create()
    {
        return view('$LOWER_NAME$::create');
    }

    /**
     * Store a newly created resource in storage.
     * @param Request $request
     * @return Renderable
     */
    public function store(Request $request)
    {
        //
    }

    /**
     * Show the specified resource.
     * @param int $id
     * @return Renderable
     */
    public function show($id)
    {
        return view('$LOWER_NAME$::show');
    }

    /**
     * Show the form for editing the specified resource.
     * @param int $id
     * @return Renderable
     */
    public function edit($id)
    {
        return view('$LOWER_NAME$::edit');
    }

    /**
     * Update the specified resource in storage.
     * @param Request $request
     * @param int $id
     * @return Renderable
     */
    public function update(Request $request, $id)
    {
        //
    }

    /**
     * Remove the specified resource from storage.
     * @param int $id
     * @return Renderable
     */
    public function destroy($id)
    {
        //
    }
}

I always delete the docblocks so lets edit the stub for this file, open stubs/module/controllers.stub

<?php

namespace $CLASS_NAMESPACE$;

use Illuminate\Contracts\Support\Renderable;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;

class $CLASS$ extends Controller
{
    public function index()
    {
        return view('$LOWER_NAME$::index');
    }

    public function create()
    {
        return view('$LOWER_NAME$::create');
    }

    public function store(Request $request)
    {
        //
    }

    public function show($id)
    {
        return view('$LOWER_NAME$::show');
    }

    public function edit($id)
    {
        return view('$LOWER_NAME$::edit');
    }

    public function update(Request $request, $id)
    {
        //
    }

    public function destroy($id)
    {
        //
    }
}

Now when you create a module or create a controller:

php artisan module:make-controller CustomerController Customers

The stubs for controllers.stub will be used.

As you can see it's simple to edit the default files that are created. Now you could modify lots of files to have a structure you want by default but these stubs are used for both creating modules and files. Meaning if you modify the files to your defaults then generating additional classes they will also have the set structure. You may not always want this.

Often I want a starting structure for an entire module and bare-bones boilerplate when generating classes.

Base Module & custom Artisan command

What we're going to do is create an artisan command that will take a copy of a module and rename its files and placeholder words inside the module as a new module.

To generate a new module, you will want to create a module normally first ensure it has everything you want as a base. Keep it simple for instance for a CRUD (Create Read Update and Delete) module you'll want routes to list, add, edit and delete their controller methods views and tests.

Base Module

Once you have a module you're happy as a starting point, you're ready to convert this to a base module. Copy the module to a location. I will be using stubs/base-module as the location.

stubs/
    base-module
        app/Http
        app/Models
        app/Providers
        config
        database
        resources
        routes
        tests
        composer.json
        module.json
        package.json
        vite.config.js

Every reference to a module would need to be changed when making a new module, for instance you have a module called contacts, it has a model called contact everywhere in the module that uses the model would use Contact:: that would need to be changed to the name of the new module when creating a new module.

Instead of manually finding and copying all references to controllers, model, namespaces, views etc you would use placeholders.

Placeholders

These are the placeholders I use:

{module_}      

Module name all lowercase seperate spaces with underscores.

{module-}  

Module name all lowercase seperate spaces with hypens.

{Module}  

Module name in CamelCase.

{module} 

Module name all in lowercase.

{Model}

Model name in CamelCase.

{model} 

Model name all in lowercase.

These placeholders can then be used throughout the module for example a controller:

namespace Modules\{Module}\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Modules\{Module}\Models\{Model};

class {Module}Controller extends Controller
{
    public function index()
    {
        ${module} = {Model}::get();

        return view('{module}::index', compact('{module}'));
    }
}

When the placeholders are swapped out look like

namespace Modules\Contacts\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Modules\Contacts\Models\Contact;

class {Module}Controller extends Controller
{
    public function index()
    {
        $contacts = Contact::get();

        return view('contacts::index', compact('contacts'));
    }
}

This allows for great customisation. You can create any structure you need and later are able to swap out the placeholder for the actual values.

As I've said you would first need to create a base module using these placeholders. In addition, you will want to edit filename such as Contact.php for a model to be Model.php These would be renamed automatically to the new module name.

Base Module source code

You can find a complete module boilerplate at https://github.com/modularlaravel/base-module

Let's build a base module here for completeness.

The module will have this structure:

base-module
  config
    config.php
  app/Http
    Controllers
        ModuleController.php
    app/Models
        Model.php
    app/Providers
        ModuleServiceProvider.php
        RouteServiceProvider.php
  database
    factories
        ModelFactory.php
    migrations
        create_module_table.php
    seeders
        ModelDatabaseSeeder.php
  resources
    assets
        js
            app.js
        sass
    views
        create.blade.php
        edit.blade.php
        index.blade.php
  routes
    api.php
    web.php
  tests
    Feature
        ModuleTest.php
    Unit
  composer.json
  module.json
  package.json
  vite.config.js

Config.php

Will hold the name of the module such as Contacts

<?php

return [
    'name' => '{Module}'
];

ModelFactory.php

<?php
namespace Modules\{Module}\Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;
use Modules\{Module}\Models\{Model};

class {Model}Factory extends Factory
{
    protected $model = {Model}::class;

    public function definition(): array
    {
        return [
            'name' => $this->faker->name()
        ];
    }
}

create_model_table.php

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class Create{Module}Table extends Migration
{
    public function up()
    {
        Schema::create('{module}', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('{module}');
    }
}

ModuleDatabaseSeeder.php

<?php

namespace Modules\{Module}\Database\Seeders;

use Illuminate\Database\Seeder;
use Illuminate\Database\Eloquent\Model;

class {Module}DatabaseSeeder extends Seeder
{
    public function run()
    {
        Model::unguard();

        // $this->call("OthersTableSeeder");
    }
}

ModuleController.php

<?php

namespace Modules\{Module}\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Modules\{Module}\Models\{Model};

class {Module}Controller extends Controller
{
    public function index()
    {
        ${module} = {Model}::get();

        return view('{module}::index', compact('{module}'));
    }

    public function create()
    {
        return view('{module}::create');
    }

    public function store(Request $request)
    {
        $request->validate([
            'name' => 'required|string'
        ]);

        {Model}::create([
            'name' => $request->input('name')
        ]);

        return redirect(route('app.{module}.index'));
    }

    public function edit($id)
    {
        ${model} = {Model}::findOrFail($id);

        return view('{module}::edit', compact('{model}'));
    }

    public function update(Request $request, $id)
    {
        $request->validate([
            'name' => 'required|string'
        ]);

        {Model}::findOrFail($id)->update([
            'name' => $request->input('name')
        ]);

        return redirect(route('app.{module}.index'));
    }

    public function destroy($id)
    {
        {Model}::findOrFail($id)->delete();

        return redirect(route('app.{module}.index'));
    }
}

Model.php

<?php

namespace Modules\{Module}\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Modules\{Module}\Database\Factories\{Model}Factory;

class {Model} extends Model
{
    use HasFactory;

    protected $fillable = ['name'];
    
    protected static function newFactory()
    {
        return {Model}Factory::new();
    }
}

ModuleServiceProvider.php

<?php

namespace Modules\{Module}\Providers;

use Illuminate\Support\ServiceProvider;

class {Module}ServiceProvider extends ServiceProvider
{
    protected $moduleName = '{Module}';
    protected $moduleNameLower = '{module}';

    public function boot()
    {
        $this->registerTranslations();
        $this->registerConfig();
        $this->registerViews();
        $this->loadMigrationsFrom(module_path($this->moduleName, 'database/migrations'));
    }

    public function register()
    {
        $this->app->register(RouteServiceProvider::class);
    }

    protected function registerConfig()
    {
        $this->publishes([
            module_path($this->moduleName, 'config/config.php') => config_path($this->moduleNameLower . '.php'),
        ], 'config');
        $this->mergeConfigFrom(
            module_path($this->moduleName, 'config/config.php'), $this->moduleNameLower
        );
    }

    public function registerViews()
    {
        $viewPath = resource_path('views/modules/' . $this->moduleNameLower);

        $sourcePath = module_path($this->moduleName, 'resources/views');

        $this->publishes([
            $sourcePath => $viewPath
        ], ['views', $this->moduleNameLower . '-module-views']);

        $this->loadViewsFrom(array_merge($this->getPublishableViewPaths(), [$sourcePath]), $this->moduleNameLower);
    }

    public function registerTranslations()
    {
        $langPath = resource_path('lang/modules/' . $this->moduleNameLower);

        if (is_dir($langPath)) {
            $this->loadJsonTranslationsFrom($langPath, $this->moduleNameLower);
        } else {
            $this->loadJsonTranslationsFrom(module_path($this->moduleName, 'lang'), $this->moduleNameLower);
        }
    }

    public function provides()
    {
        return [];
    }

    private function getPublishableViewPaths(): array
    {
        $paths = [];
        foreach (\Config::get('view.paths') as $path) {
            if (is_dir($path . '/modules/' . $this->moduleNameLower)) {
                $paths[] = $path . '/modules/' . $this->moduleNameLower;
            }
        }
        return $paths;
    }
}

RouteServiceProvider.php

<?php

namespace Modules\{Module}\Providers;

use Illuminate\Support\Facades\Route;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;

class RouteServiceProvider extends ServiceProvider
{
    protected $moduleNamespace = '';

    public function boot()
    {
        parent::boot();
    }

    public function map()
    {
        $this->mapApiRoutes();
        $this->mapWebRoutes();
    }

    protected function mapWebRoutes()
    {
        Route::middleware('web')
            ->namespace($this->moduleNamespace)
            ->group(module_path('{Module}', '/Routes/web.php'));
    }

    protected function mapApiRoutes()
    {
        Route::prefix('api')
            ->middleware('api')
            ->namespace($this->moduleNamespace)
            ->group(module_path('{Module}', '/Routes/api.php'));
    }
}

create.blade.php

@extends('layouts.app')

@section('content')
    <div class="card">
        <h1>Add {Model}</h1>

        <x-form action="{{ route('app.{module}.create') }}">
            <x-form.input name="name" />
            <x-form.button>Submit</x-form.button>
        </x-form>
    </div>
@endsection

edit.blade.php

@extends('layouts.app')

@section('content')
    <div class="card">
        <h1>Edit {Model}</h1>

        <x-form action="{{ route('app.{module}.update', ${model}->id) }}" method="patch">
            <x-form.input name="name">{{ ${model}->name }}</x-form.input>
            <x-form.button>Update</x-form.button>
        </x-form>
    </div>
@endsection

index.blade.php

@extends('layouts.app')

@section('content')
    <div class="card">
    <h1>{Module}</h1>

    <p><a href="{{ route('app.{module}.create') }}">Add {Model}</a> </p>

    <table>
        <tr>
            <td>Name</td>
            <td>Action</td>
        </tr>
        @foreach(${module} as ${model})
            <tr>
                <td>{{ ${model}->name }}</td>
                <td>
                    <a href="{{ route('app.{module}.edit', ${model}->id) }}">Edit</a>

                    <a href="#" onclick="event.preventDefault(); document.getElementById('delete-form').submit();">Delete</a>
                    <x-form id="delete-form" method="delete" action="{{ route('app.{module}.delete', ${model}->id) }}" />
                </td>
            </tr>
        @endforeach
    </table>
    </div>
@endsection

api.php

<?php

use Illuminate\Http\Request;

Route::middleware('auth:api')->get('/{module}', function (Request $request) {
    return $request->user();
});

web.php

<?php

use Modules\{Module}\Http\Controllers\{Module}Controller;

Route::middleware('auth')->prefix('app/{module}')->group(function() {
    Route::get('/', [{Module}Controller::class, 'index'])->name('app.{module}.index');
    Route::get('create', [{Module}Controller::class, 'create'])->name('app.{module}.create');
    Route::post('create', [{Module}Controller::class, 'store'])->name('app.{module}.store');
    Route::get('edit/{id}', [{Module}Controller::class, 'edit'])->name('app.{module}.edit');
    Route::patch('edit/{id}', [{Module}Controller::class, 'update'])->name('app.{module}.update');
    Route::delete('delete/{id}', [{Module}Controller::class, 'destroy'])->name('app.{module}.delete');
});

ModuleTest.php

<?php

use Modules\{Module}\Models\{Model};

uses(Tests\TestCase::class);

test('can see {model} list', function() {
    $this->authenticate();
   $this->get(route('app.{module}.index'))->assertOk();
});

test('can see {model} create page', function() {
    $this->authenticate();
   $this->get(route('app.{module}.create'))->assertOk();
});

test('can create {model}', function() {
    $this->authenticate();
   $this->post(route('app.{module}.store', [
       'name' => 'Joe'
   ]))->assertRedirect(route('app.{module}.index'));

   $this->assertDatabaseCount('{module}', 1);
});

test('can see {model} edit page', function() {
    $this->authenticate();
    ${model} = {Model}::factory()->create();
    $this->get(route('app.{module}.edit', ${model}->id))->assertOk();
});

test('can update {model}', function() {
    $this->authenticate();
    ${model} = {Model}::factory()->create();
    $this->patch(route('app.{module}.update', ${model}->id), [
        'name' => 'Joe Smith'
    ])->assertRedirect(route('app.{module}.index'));

    $this->assertDatabaseHas('{module}', ['name' => 'Joe Smith']);
});

test('can delete {model}', function() {
    $this->authenticate();
    ${model} = {Model}::factory()->create();
    $this->delete(route('app.{module}.delete', ${model}->id))->assertRedirect(route('app.{module}.index'));

    $this->assertDatabaseCount('{module}', 0);
});

composer.json

Ensure you edit the author details here, all future modules will use these details.

{
    "name": "dcblogdev/{module}",
    "description": "",
    "authors": [
        {
            "name": "David Carr",
            "email": "dave@dcblog.dev"
        }
    ],
    "extra": {
        "laravel": {
            "providers": [],
            "aliases": {

            }
        }
    },
    "autoload": {
        "psr-4": {
            "Modules\\{Module}\\": "app/",
            "Modules\\{Module}\\Database\\Factories\\": "database/factories/",
            "Modules\\{Module}\\Database\\Seeders\\": "database/seeders/"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "Modules\\{Module}\\Tests\\": "tests/"
        }
    }
}

module.json

{
    "name": "{Module}",
    "label": "{Module}",
    "alias": "{module}",
    "description": "manage all {module}",
    "keywords": ["{module}"],
    "priority": 0,
    "providers": [
        "Modules\\{Module}\\Providers\\{Module}ServiceProvider"
    ],
    "files": []
}

package.json

{
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build"
  },
  "devDependencies": {
    "axios": "^1.1.2",
    "laravel-vite-plugin": "^0.7.5",
    "sass": "^1.69.5",
    "postcss": "^8.3.7",
    "vite": "^4.0.0"
  }
}

vite.config.js

export const paths = [
   'Modules/{Module}/resources/assets/sass/app.scss',
   'Modules/{Module}/resources/assets/js/app.js',
];

Artisan make:module command

Now we have a base module and its placeholders in place its time to write an artisan command that will take this as blueprints to create a new module from.

create a new command with this command:

php artisan make:command MakeModuleCommand

This will generate a new command class inside app/Console/Commands/MakeModuleCommand.php

<?php

declare(strict_types=1);

namespace App\Console\Commands;

use Illuminate\Console\Command;

class MakeModuleCommand extends Command
{
    protected $signature = 'command:name';
    protected $description = 'Command description';

    public function __construct()
    {
        parent::__construct();
    }

    public function handle()
    {
        return 0;
    }
}

We want to call the command using make:module, replace the signature and description:

protected $signature = 'make:module';
protected $description = 'Create starter CRUD module';

Import Str and Symfony Filesystem classes:

use Illuminate\Support\Str;
use Symfony\Component\Filesystem\Filesystem as SymfonyFilesystem;

For the Filesystem you would need to install the class via composer:

composer require symfony/filesystem

Inside the handle method we want the command to ask for a name of the module, we can use $this->ask for this.

Ensure the module name is not empty with validation.

When the name of the module is provided, we will try to guess the name of the model and then ask for confirmation if the model name is correct.

If the name is correct confirmation of the module name and model will be printed and ask for final confirmation before moving to the next step.

If confirmation fails the process will restart.

Once the final confirmation has occurred a method called generate will be called.

public function handle()
{
  $this->container['name'] = ucwords($this->ask('Please enter the name of the Module'));

  if (strlen($this->container['name']) == 0) {
      $this->error("\nModule name cannot be empty.");
  } else {
      $this->container['model'] = ucwords(Str::singular($this->container['name']));

      if ($this->confirm("Is '{$this->container['model']}' the correct name for the Model?", 'yes')) {
          $this->comment('You have provided the following information:');
          $this->comment('Name:  ' . $this->container['name']);
          $this->comment('Model: ' . $this->container['model']);

          if ($this->confirm('Do you wish to continue?', 'yes')) {
              $this->comment('Success!');
              $this->generate();
          } else {
              return false;
          }

          return true;
      } else {
          $this->handle();
      }
  }
  
  $this->info('Starter '.$this->container['name'].' module installed successfully.');
}

Now we need to add a generate method, this will need to add a few other methods let's add these first.

Add a method called rename that accepts a path, target and a type. This is used to rename file and save the renamed file into a new $target location.

If the $type is set to migration then create a timestamp that will be added to the filename and prefixed to the migration file. Otherwise do a direct rename.

protected function rename($path, $target, $type = null)
{
    $filesystem = new SymfonyFilesystem;
    if ($filesystem->exists($path)) {
        if ($type == 'migration') {
            $timestamp = date('Y_m_d_his_');
            $target = str_replace("create", $timestamp."create", $target);
            $filesystem->rename($path, $target, true);
            $this->replaceInFile($target);
        } else {
            $filesystem->rename($path, $target, true);
        }
    }
}

Next we want to be able to copy an entire folder and into contents to a new location.

protected function copy($path, $target)
{
    $filesystem = new SymfonyFilesystem;
    if ($filesystem->exists($path)) {
        $filesystem->mirror($path, $target);
    }
}

The final helper method will be to replace all the placeholders from the files.

Inside this method is where we define the placeholders and their values.

The types defined all the type of placeholders this is case and character sensitive, next each type is looped over. If the key from the type is module_ then all names with spaces will be switched to be underscored separated, likewise for the module- will be hyphened separated for spaces.

Finally, the file's contents will be replaced with the values of the placeholders name of model contents.

protected function replaceInFile($path)
{
    $name = $this->container['name'];
    $model = $this->container['model'];
    $types = [
        '{module_}' => null,
        '{module-}' => null,
        '{Module}' => $name,
        '{module}' => strtolower($name),
        '{Model}' => $model,
        '{model}' => strtolower($model)
    ];

    foreach($types as $key => $value) {
        if (file_exists($path)) {

            if ($key == "module_") {
                $parts = preg_split('/(?=[A-Z])/', $name, -1, PREG_SPLIT_NO_EMPTY);
                $parts = array_map('strtolower', $parts);
                $value = implode('_', $parts);
            }

            if ($key == 'module-') {
                $parts = preg_split('/(?=[A-Z])/', $name, -1, PREG_SPLIT_NO_EMPTY);
                $parts = array_map('strtolower', $parts);
                $value = implode('-', $parts);
            }

            file_put_contents($path, str_replace($key, $value, file_get_contents($path)));
        }
    }
}

Now we have these helpers methods we can create the generate method.

First we create local variables of name and model for easy referencing. The $targetPath variable stores the final module path.

Next we need to copy the base module into Modules folder.

The new module at this point is in the modules folder with all the placeholders and temporary file names, now we need to list all files that need the contents examining to place the placeholders.

The last chunk lists all filename that need to be renamed.

protected function generate()
{
    $module     = $this->container['name'];
    $model      = $this->container['model'];
    $Module     = $module;
    $module     = strtolower($module);
    $Model      = $model;
    $targetPath = base_path('Modules/'.$Module);

    //copy folders
    $this->copy(base_path('stubs/base-module'), $targetPath);

    //replace contents
    $this->replaceInFile($targetPath.'/config/config.php');
    $this->replaceInFile($targetPath.'/database/factories/ModelFactory.php');
    $this->replaceInFile($targetPath.'/database/migrations/create_module_table.php');
    $this->replaceInFile($targetPath.'/database/seeders/ModelDatabaseSeeder.php');
    $this->replaceInFile($targetPath.'/app/Http/Controllers/ModuleController.php');
    $this->replaceInFile($targetPath.'/app/Models/Model.php');
    $this->replaceInFile($targetPath.'/app/Providers/ModuleServiceProvider.php');
    $this->replaceInFile($targetPath.'/app/Providers/RouteServiceProvider.php');
    $this->replaceInFile($targetPath.'/resources/views/create.blade.php');
    $this->replaceInFile($targetPath.'/resources/views/edit.blade.php');
    $this->replaceInFile($targetPath.'/resources/views/index.blade.php');
    $this->replaceInFile($targetPath.'/routes/api.php');
    $this->replaceInFile($targetPath.'/routes/web.php');
    $this->replaceInFile($targetPath.'/tests/Feature/ModuleTest.php');
    $this->replaceInFile($targetPath.'/composer.json');
    $this->replaceInFile($targetPath.'/module.json');
    $this->replaceInFile($targetPath.'/vite.config.js');

    //rename
    $this->rename($targetPath.'/database/factories/ModelFactory.php', $targetPath.'/database/factories/'.$Model.'Factory.php');
    $this->rename($targetPath.'/database/migrations/create_module_table.php', $targetPath.'/database/migrations/create_'.$module.'_table.php', 'migration');
    $this->rename($targetPath.'/database/seeders/ModelDatabaseSeeder.php', $targetPath.'/database/seeders/'.$Module.'DatabaseSeeder.php');
    $this->rename($targetPath.'/app/Http/Controllers/ModuleController.php', $targetPath.'/app/Http/Controllers/'.$Module.'Controller.php');
    $this->rename($targetPath.'/app/Models/Model.php', $targetPath.'/Models/'.$Model.'.php');
    $this->rename($targetPath.'/app/Providers/ModuleServiceProvider.php', $targetPath.'/app/Providers/'.$Module.'ServiceProvider.php');
    $this->rename($targetPath.'/tests/Feature/ModuleTest.php', $targetPath.'/tests/Feature/'.$Module.'Test.php');
}

Putting it all together:

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Str;
use Symfony\Component\Filesystem\Filesystem as SymfonyFilesystem;

class MakeModuleCommand extends Command
{
    protected $signature = 'make:module';
    protected $description = 'Create starter CRUD module';

    public function handle()
    {
        $this->container['name'] = ucwords($this->ask('Please enter the name of the Module'));

        if (strlen($this->container['name']) == 0) {
            $this->error("\nModule name cannot be empty.");
        } else {
            $this->container['model'] = ucwords(Str::singular($this->container['name']));

            if ($this->confirm("Is '{$this->container['model']}' the correct name for the Model?", 'yes')) {
                $this->comment('You have provided the following information:');
                $this->comment('Name:  ' . $this->container['name']);
                $this->comment('Model: ' . $this->container['model']);

                if ($this->confirm('Do you wish to continue?', 'yes')) {
                    $this->comment('Success!');
                    $this->generate();
                } else {
                    return false;
                }

                return true;
            } else {
                $this->handle();
            }
        }

        $this->info('Starter '.$this->container['name'].' module installed successfully.');
    }

    protected function generate()
    {
        $module     = $this->container['name'];
        $model      = $this->container['model'];
        $Module     = $module;
        $module     = strtolower($module);
        $Model      = $model;
        $targetPath = base_path('Modules/'.$Module);

        //copy folders
        $this->copy(base_path('stubs/base-module'), $targetPath);

        //replace contents
        $this->replaceInFile($targetPath.'/config/config.php');
        $this->replaceInFile($targetPath.'/database/factories/ModelFactory.php');
        $this->replaceInFile($targetPath.'/database/migrations/create_module_table.php');
        $this->replaceInFile($targetPath.'/database/seeders/ModelDatabaseSeeder.php');
        $this->replaceInFile($targetPath.'/app/Http/Controllers/ModuleController.php');
        $this->replaceInFile($targetPath.'/app/Models/Model.php');
        $this->replaceInFile($targetPath.'/app/Providers/ModuleServiceProvider.php');
        $this->replaceInFile($targetPath.'/app/Providers/RouteServiceProvider.php');
        $this->replaceInFile($targetPath.'/resources/views/create.blade.php');
        $this->replaceInFile($targetPath.'/resources/views/edit.blade.php');
        $this->replaceInFile($targetPath.'/resources/views/index.blade.php');
        $this->replaceInFile($targetPath.'/routes/api.php');
        $this->replaceInFile($targetPath.'/routes/web.php');
        $this->replaceInFile($targetPath.'/tests/Feature/ModuleTest.php');
        $this->replaceInFile($targetPath.'/composer.json');
        $this->replaceInFile($targetPath.'/module.json');
        $this->replaceInFile($targetPath.'/vite.config.js');

        //rename
        $this->rename($targetPath.'/database/factories/ModelFactory.php', $targetPath.'/database/factories/'.$Model.'Factory.php');
        $this->rename($targetPath.'/database/migrations/create_module_table.php', $targetPath.'/database/migrations/create_'.$module.'_table.php', 'migration');
        $this->rename($targetPath.'/database/seeders/ModelDatabaseSeeder.php', $targetPath.'/database/seeders/'.$Module.'DatabaseSeeder.php');
        $this->rename($targetPath.'/app/Http/Controllers/ModuleController.php', $targetPath.'/app/Http/Controllers/'.$Module.'Controller.php');
        $this->rename($targetPath.'/app/Models/Model.php', $targetPath.'/Models/'.$Model.'.php');
        $this->rename($targetPath.'/app/Providers/ModuleServiceProvider.php', $targetPath.'/app/Providers/'.$Module.'ServiceProvider.php');
        $this->rename($targetPath.'/tests/Feature/ModuleTest.php', $targetPath.'/tests/Feature/'.$Module.'Test.php');
    }

    protected function rename($path, $target, $type = null)
    {
        $filesystem = new SymfonyFilesystem;
        if ($filesystem->exists($path)) {
            if ($type == 'migration') {
                $timestamp = date('Y_m_d_his_');
                $target = str_replace("create", $timestamp."create", $target);
                $filesystem->rename($path, $target, true);
                $this->replaceInFile($target);
            } else {
                $filesystem->rename($path, $target, true);
            }
        }
    }

    protected function copy($path, $target)
    {
        $filesystem = new SymfonyFilesystem;
        if ($filesystem->exists($path)) {
            $filesystem->mirror($path, $target);
        }
    }

    protected function replaceInFile($path)
    {
        $name = $this->container['name'];
        $model = $this->container['model'];
        $types = [
            '{module_}' => null,
            '{module-}' => null,
            '{Module}' => $name,
            '{module}' => strtolower($name),
            '{Model}' => $model,
            '{model}' => strtolower($model)
        ];

        foreach($types as $key => $value) {
            if (file_exists($path)) {

                if ($key == "module_") {
                    $parts = preg_split('/(?=[A-Z])/', $name, -1, PREG_SPLIT_NO_EMPTY);
                    $parts = array_map('strtolower', $parts);
                    $value = implode('_', $parts);
                }

                if ($key == 'module-') {
                    $parts = preg_split('/(?=[A-Z])/', $name, -1, PREG_SPLIT_NO_EMPTY);
                    $parts = array_map('strtolower', $parts);
                    $value = implode('-', $parts);
                }

                file_put_contents($path, str_replace($key, $value, file_get_contents($path)));
            }
        }
    }
}

This seems like a lot of work but this gives you complete control over what a module will contain when generated. For simple CRUD modules you can build an application incredibly quickly with this approach.


Laravel Package built by Nicolas Widart.
Maintained by David Carr follow on X @dcblogdev