The Module System

6 Aufrufe Markdown

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.

coremio/modules The whole extension surface: one subdirectory per type, one per module inside it. 16 types, 300 modules, including the Sample sandbox modules that ship as templates.
Modules The registry and factory, all static. It scans directories, includes class files, caches configuration and language packs, and builds instances.
Module type The parent directory name, with its exact case (Servers, Payment, Registrars). It is a string in every registry call, so a typo gives a silent empty result rather than an error.
Module name The module directory name. The class file and the class carry the same name, character for character, including case.

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.

TypeBase classWhat the core calls it forPresent
ServersServerModuleProvisioning and managing a hosting account, a virtual machine or a game server on a control panel50
PaymentPaymentGatewayModuleTaking money: the payment screen, the capture, the callback and the settlement164
RegistrarsRegistrarModuleRegistering, renewing and transferring a domain name at a registrar21
ProductProductModule, SslProductModuleA product that is provisioned without a server. All four non-sample modules here are SSL certificate products7
AddonsAddonModuleA feature bolted onto the panel itself: its own settings page, privileges and hooks9
SMSnone, plain classSending a text message through a provider11
Mailnone, plain classDelivering outgoing mail, by SMTP or through a provider API4
Authenticationnone, plain classA second factor at login: mail code, SMS code or an authenticator app3
Pipenone, plain classPulling mail from a mailbox and turning it into support tickets3
Importsnone, plain classMigrating clients, services and invoices in from another platform3
FraudFraudModuleScoring an order or a signup and recording what was found2
SocialAuthSocialAuthProviderSocial login: the authorisation redirect, the code exchange and the identity token check3
Captchanone, plain classChallenging a public form before it is accepted4
Currencynone, plain classFetching exchange rates for the configured currencies7
IPnone, plain classResolving an address to a country and a network, used for locale and risk3
StorageStorageModule, CloudStorageModuleA remote destination backups are uploaded to and restored from6

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.

what a load touches
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.

class name candidates, in order
$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:

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

$name = 'All' Scan the whole type directory instead of one module. The literal 'All', not an empty string: for Mail and SMS an empty name means something else (see Pitfalls).
$nominc = true No include. Configuration and language are read, the class file is not touched. The cheap call, used by every listing screen.
$status Left as the default empty string it filters nothing. Pass a non-string, in practice true, and only modules whose config['status'] equals it load. That is how the panel lists enabled addons only.
$params Positional constructor arguments. The factory reflects the constructor and pads with null up to the required count. A module with a mandatory argument still builds when you pass nothing.

Return Shapes

getInstance() The object, or null. Cached per type, name and serialised parameters, so two calls in one request give the same object.
Load() With a name: ['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.
Config() The cached configuration array, or null when nothing has loaded it yet. It never reads the file itself.
Lang() The language array, or []. Unlike Config() it does read the file, falling back to the active interface language, then the system default, then English.
getName() The display label: 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.

calling one module
$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();
listing a type, no instances built
// 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:

configuration without an instance
// 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

Never build a module with new

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.

Reading configuration without loading returns null, not an empty array

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.

An empty name means the active module for Mail and SMS

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 instance is shared for the whole request

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.

Dashboard widgets are not a module type

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.

War das hilfreich?

Vielen Dank für Ihre Rückmeldung!

Brauchen Sie weitere Hilfe?

Unser Support-Team ist rund um die Uhr für Sie da, wenn Sie oben nicht fündig werden.