Skip to content

Module Structure

v1.0


Directory layout

modules/MyPlugin/
├── module.json                        ← metadata & requirements
├── ModuleServiceProvider.php          ← entry point: registers everything

├── Http/
│   ├── Controllers/
│   │   ├── Admin/                     ← admin-facing controllers
│   │   └── MyPluginController.php     ← frontend/account controllers
│   └── Middleware/

├── Models/
├── Services/
├── Listeners/
├── Mail/

├── database/
│   ├── migrations/
│   └── seeders/

├── resources/
│   ├── views/
│   │   ├── admin/
│   │   └── account/
│   └── js/                            ← optional: module JS files

├── config/
│   └── my-plugin.php                  ← optional: module config

└── routes/
    ├── web.php
    └── api.php

Nothing in this structure is mandatory except module.json and ModuleServiceProvider.php. Add only what your module needs.


module.json

Every module must have a module.json at its root.

json
{
    "name": "My Plugin",
    "slug": "my-plugin",
    "version": "1.0.0",
    "description": "A short description of what this module does.",
    "author": "Your Name",
    "provider": "Modules\\MyPlugin\\ModuleServiceProvider",
    "module_requires": [],
    "enabled": true
}
FieldRequiredDescription
nameYesHuman-readable display name
slugYesLowercase, hyphenated identifier. Used as view namespace and config key
versionYesSemver string
providerYesFully-qualified class name of the service provider
descriptionNoShown in the admin module list
authorNoShown in the admin module list
module_requiresNoArray of module slugs that must be active before this module boots (e.g. ["ai-agents-core"])
enabledNoFallback flag (DB record takes precedence once installed)

ModuleServiceProvider

Your service provider is the module's single entry point. It must extend AbstractModuleServiceProvider and implement two methods:

php
<?php

namespace Modules\MyPlugin;

use App\Support\AbstractModuleServiceProvider;

class ModuleServiceProvider extends AbstractModuleServiceProvider
{
    protected function modulePath(): string
    {
        return __DIR__;
    }

    protected function moduleNamespace(): string
    {
        return 'Modules\\MyPlugin';
    }

    public function register(): void
    {
        // Bind services, merge config.
        // Runs before boot() and before the DB is guaranteed available.
        $this->loadModuleConfig();
    }

    public function boot(): void
    {
        // Load routes, views, migrations.
        // Register middleware aliases, menu items, event listeners.
        $this->loadMigrations();
        $this->loadWebRoutes();
        $this->loadViews();
    }
}

register() vs boot()

register()boot()
When it runsBefore any bootAfter all providers are registered
DB available?Not guaranteedYes (for web requests)
Use forConfig merging, service bindingsEverything else

Always load config in register(), everything else in boot().


Available helpers

All helpers are methods on AbstractModuleServiceProvider. Call them from register() or boot() as appropriate.

Loading resources

php
$this->loadMigrations();        // loads database/migrations/
$this->loadWebRoutes();         // loads routes/web.php with ['middleware' => 'web']
$this->loadApiRoutes();         // loads routes/api.php with ['middleware' => 'api']
$this->loadViews();             // loads resources/views/ as namespace "my-plugin"
$this->loadModuleConfig();      // merges config/my-plugin.php into app config
$this->loadCommands();          // registers $this->commands[] (console only)

Injecting into the platform (see dedicated pages for each)

php
$this->registerAdminMenuItem(...)       // v1.2 — admin sidebar link
$this->registerAdminSettingsLink(...)   // v1.2 — admin settings tab
$this->registerAccountMenuItem(...)     // v1.4 — customer account sidebar link
$this->registerAccountNavOverride(...)  // v1.4 — replace account sidebar nav entirely
$this->registerHiddenNavItems(...)      // v1.4 — hide core account nav items conditionally
$this->registerPricingModel(...)        // v1.1 — custom billing model
$this->registerFrontendScript(...)      // v1.1 — JS injected on product page
$this->registerPlanTab(...)             // v1.5 — tab injected into the plan edit modal
$this->registerAgent(...)              // — register an AI agent with AiAgentsCore

Artisan commands

php
protected array $commands = [
    \Modules\MyPlugin\Console\Commands\MyCommand::class,
];

Then call $this->loadCommands() in boot().


Autoloading

The module loader registers a PSR-4 autoloader for Modules\\modules/ at application boot. You never need to run composer dump-autoload when adding new classes inside a module.

The autoloader is registered in AppServiceProvider::register(), which runs before the DB is available — so module classes are always resolvable, even in console commands.


View namespaces

loadViews() registers your resources/views/ directory under the module slug as a namespace. Reference views with :::

php
// slug = "my-plugin"
return view('my-plugin::account.index');
return view('my-plugin::admin.dashboard');

Config

If your module has a config/my-plugin.php, call loadModuleConfig() in register(). Values are merged under the slug key and accessible via:

php
config('my-plugin.some_key');
setting('my-plugin.some_key');   // if stored in the settings table

Public assets

If your module ships browser-accessible files (JS, CSS, images), place them in a public/ directory at the module root:

modules/MyPlugin/
└── public/
    └── js/
        └── my-plugin.js

When the module is installed via the admin UI, ChargePanda creates a symlink at public/modules/MyPlugin/ pointing to modules/MyPlugin/public/. Reference assets with:

php
asset('modules/MyPlugin/js/my-plugin.js')

On Windows servers without symlink privileges, a directory junction is created automatically instead.


Module dependencies (module_requires)

If your module depends on another module being active, declare it in module.json:

json
{
    "module_requires": ["ai-agents-core"]
}

The module loader topologically sorts all active modules and boots dependencies first. If a required module is not active, your module is skipped with a warning in the application log — it will not cause an error for other modules or crash the application.

When bundling multiple modules in a single ZIP for distribution, include all dependencies in the same archive. The installer resolves install order automatically.


Bundling multiple modules

You can distribute a paid module together with its free dependencies in a single ZIP. The archive should contain one subdirectory per module, each with its own module.json:

my-bundle.zip
├── AiAgentsCore/
│   ├── module.json
│   └── ...
└── MyPaidModule/
    ├── module.json   ← declares "module_requires": ["ai-agents-core"]
    └── ...

ChargePanda detects all module directories in the ZIP, sorts them by dependency order, and installs each one. Modules already installed at the same or higher version are skipped automatically.

Released under the Commercial License.