The Module System
Every integration is a module: a directory under a type folder. The platform discovers it by name, loads it on demand, and hands it to you through one factory.
Overview
A module is never registered: no manifest to edit, no container entry, no install routine. The registry reads the file system. A directory whose name matches the class file inside it is a module the moment it exists on disk, and the panel lists it on the next request.
The directory one level up is the module's type, and the type is the whole contract. It decides which methods the core calls, which admin screen lists the module, and whether a base class exists to extend. Eight of the sixteen types have one. The other eight are plain classes whose contract is whatever the core looks for with method_exists.
Sample sandbox modules that ship as templates.
Servers, Payment, Registrars). It is a string in every registry call, so a typo gives a silent empty result rather than an error.
Structure
The Sixteen Types
Counts include the sandbox modules whose names start with Sample. Those exist to be read and copied, not enabled in production.
| Type | Base class | What the core calls it for | Present |
|---|---|---|---|
| Servers | ServerModule | Provisioning and managing a hosting account, a virtual machine or a game server on a control panel | 50 |
| Payment | PaymentGatewayModule | Taking money: the payment screen, the capture, the callback and the settlement | 164 |
| Registrars | RegistrarModule | Registering, renewing and transferring a domain name at a registrar | 21 |
| Product | ProductModule, SslProductModule | A product that is provisioned without a server. All four non-sample modules here are SSL certificate products | 7 |
| Addons | AddonModule | A feature bolted onto the panel itself: its own settings page, privileges and hooks | 9 |
| SMS | none, plain class | Sending a text message through a provider | 11 |
| none, plain class | Delivering outgoing mail, by SMTP or through a provider API | 4 | |
| Authentication | none, plain class | A second factor at login: mail code, SMS code or an authenticator app | 3 |
| Pipe | none, plain class | Pulling mail from a mailbox and turning it into support tickets | 3 |
| Imports | none, plain class | Migrating clients, services and invoices in from another platform | 3 |
| Fraud | FraudModule | Scoring an order or a signup and recording what was found | 2 |
| SocialAuth | SocialAuthProvider | Social login: the authorisation redirect, the code exchange and the identity token check | 3 |
| Captcha | none, plain class | Challenging a public form before it is accepted | 4 |
| Currency | none, plain class | Fetching exchange rates for the configured currencies | 7 |
| IP | none, plain class | Resolving an address to a country and a network, used for locale and risk | 3 |
| Storage | StorageModule, CloudStorageModule | A remote destination backups are uploaded to and restored from | 6 |
Discovery and Class Resolution
Loading is two separate things. Configuration and the language pack are include calls into a static cache. The class file is included only when an instance is wanted, and the loader's third argument controls that split.
coremio/modules/{Type}/{Name}/
├── {Name}.php # included only when an instance is built (nominc = false)
├── config.php # returns an array, cached under Modules::$modules[Type][Name]['config']
└── lang/
├── en.php # fallback, always tried when the active language file is missing
└── {lang}.php # cached under ['lang']
Three class names are tried in this order, and the first that exists wins. That is why the global namespace and the type namespace both work.
$classList = [
$name . "_Module", // legacy suffix form
$name, // global namespace
"WISECP\\Modules\\" . ucfirst($type) . "\\" . $name, // the form new modules use
];
Reference
The Registry
All static, all in one class. The exact signatures:
// Build. Returns null when no class of any candidate name exists.
public static function getInstance(string $type, string $name, array $params = []): ?object;
// Load configuration and language into the static cache. Returns the loaded record(s),
// or false when a named module directory does not exist.
public static function Load($type = '', $name = '', $nominc = false, $status = '');
public static function add($file, $type, $nominc = false, $status = '');
// Read back from the cache. Config() does NOT load; Lang() does.
public static function Config($type, $module);
public static function Lang($type, $module, $lang = '');
public static function getName(string $type, string $module): string;
public static function getModules($type = '', $name = '');
// Surfaces the panel renders for a module.
public static function getPage(string $type, string $name, string $page, array $data = []): string;
public static function getController($type = '', $name = '', $cname = '');
public static function view($file, $variables = []): string;
public static function logo(string $name = '', $type = 'Servers'): string;
// Settings-field rendering, shared by every type that declares fields in config.php.
public static function fields_output($data = [], $input_name = ''): string;
public static function fields_output_wBuilder(AdminFormBuilder $form, $data = [], $input_name = ''): void;
// The module action log the panel shows under the client's action history.
public static function save_log($type = '', $module = '', $action = '', $request = '', $response = '', $processed = '');
Arguments That Change the Result
'All', not an empty string: for Mail and SMS an empty name means something else (see Pitfalls).
true, and only modules whose config['status'] equals it load. That is how the panel lists enabled addons only.
null up to the required count. A module with a mandatory argument still builds when you pass nothing.
Return Shapes
null. Cached per type, name and serialised parameters, so two calls in one request give the same object.
['config' => [], 'lang' => [], 'config_file' => ''], or false when the directory is missing. With 'All': a map of name to that same record, ordered by display name.
null when nothing has loaded it yet. It never reads the file itself.
[]. Unlike Config() it does read the file, falling back to the active interface language, then the system default, then English.
lang['name'], then config['name'], then the directory name. It loads for you, so it is safe to call cold.
Example
Building one module and calling it, then listing a whole type without building anything.
$module = Modules::getInstance("Registrars", "ExampleRegistrarModule");
if (!$module) throw new Exception(Language::gc("modules/error-not-found"));
// Guard on the contract, not on the class: a module is plain PHP and may predate a method.
if (!method_exists($module, "create")) throw new Exception(Language::gc("modules/error-unsupported"));
// Context is bound with setters, not passed as arguments.
$module->set_service($serviceId);
$result = $module->create();
// nominc = true: read config.php and lang/, do not include any class file.
$installed = Modules::Load("Currency", "All", true) ?: [];
$rows = [];
foreach ($installed as $name => $record) {
$rows[$name] = [
'label' => Modules::getName("Currency", $name),
'active' => (bool) ($record['config']['status'] ?? false),
'logo' => Modules::logo($name, "Currency"),
];
}
// Enabled modules only, in one call: the fourth argument is compared to config['status'].
$enabled = Modules::Load("Addons", "All", true, true) ?: [];
The reading side, when you know the module and need only its settings:
// Config() reads the cache only, so the load has to come first.
Modules::Load("Servers", "cPanel", true);
$config = Modules::Config("Servers", "cPanel") ?: [];
$fields = $config['fields'] ?? [];
// Same record, one call, when the load result is all you need.
$fields = Modules::Load("Servers", "cPanel", true)["config"]["fields"] ?? [];
Pitfalls
The factory includes the class file, fills the configuration and language properties, pads missing constructor arguments and caches the result. Constructing the class directly skips all of it. The module runs with an empty configuration, which looks exactly like a provider returning nothing.
The read is cache only. Called cold it returns null, which flows into your code as a missing setting rather than an error. Load first, or take the configuration from the load result.
For those two types only, a load with no name resolves the configured active driver and loads that one. Every other type reads the whole directory. For the full list, always pass 'All'.
The factory caches per type, name and parameters, so a loop over many services keeps handing back the same object, with whatever a previous iteration bound to it. Bind the new context with the setters at the top of every iteration.
Panel widgets come from privilege checks in the dashboard controller and are extended with hooks. A directory under the modules folder does nothing, and there is no widget module type.
Related Articles
Thanks for your feedback!
Our support team is here around the clock for anything you can't find above.