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.phpNothing 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.
{
"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
}| Field | Required | Description |
|---|---|---|
name | Yes | Human-readable display name |
slug | Yes | Lowercase, hyphenated identifier. Used as view namespace and config key |
version | Yes | Semver string |
provider | Yes | Fully-qualified class name of the service provider |
description | No | Shown in the admin module list |
author | No | Shown in the admin module list |
module_requires | No | Array of module slugs that must be active before this module boots (e.g. ["ai-agents-core"]) |
enabled | No | Fallback 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
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 runs | Before any boot | After all providers are registered |
| DB available? | Not guaranteed | Yes (for web requests) |
| Use for | Config merging, service bindings | Everything 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
$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)
$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 AiAgentsCoreArtisan commands
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 :::
// 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:
config('my-plugin.some_key');
setting('my-plugin.some_key'); // if stored in the settings tablePublic 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.jsWhen the module is installed via the admin UI, ChargePanda creates a symlink at public/modules/MyPlugin/ pointing to modules/MyPlugin/public/. Reference assets with:
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:
{
"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.