Module Anatomy
The files a module is made of, the three names that must agree, and what the base class hands you.
Overview
A module is a directory with one mandatory class file, plus optional files the platform looks up by name. Nothing is registered: the loader builds each path from the type and the module name. A file in the right place is found, and a misspelled one is ignored without a warning.
Most types give you a base class that has already done the setup. Paths, configuration and language are ready before your first method runs, and provisioning types also know which service they act on. Rebuilding any of it by hand is the most common beginner mistake.
Structure
The File Tree
coremio/modules/{Type}/{Name}/
├── {Name}.php # REQUIRED: the class, same name as the directory
├── config.php # returns an array; settings, metadata, field definitions
├── logo.png # or .svg/.webp/.jpg; resolved by name when config does not name one
├── hooks.php # Hook::add() registrations, included on EVERY request
├── AdminArea.php # own admin page, registered from router.php
├── router.php # include + ModuleAdminArea::register()
├── lang/
│ ├── en.php # REQUIRED in practice: the fallback language
│ └── tr.php # one file per translated language
├── pages/ # settings and management markup, resolved by page name
├── views/ # same role, alternative directory name
├── controllers/ # named entry points reached through the controller dispatcher
├── assets/
│ ├── style/ # css
│ ├── js/ # javascript
│ └── images/ # interface images, not the logo
└── src/ # your own helper classes, NOT autoloaded
What Each File Is For
| Path | Required | Read when | What it holds |
|---|---|---|---|
{Name}.php | Yes | An instance is built | The class. Included unless the loader is told to skip it. |
config.php | In practice yes | Every load, with or without the class | An array: settings, metadata, field definitions, enabled flag. |
lang/en.php | Yes | Every load | An array. The fallback whenever the active language has no file. |
lang/{code}.php | Optional | That language is active | The same keys, translated. |
logo.png and friends | Optional | The panel shows the module | The icon. Found by extension when configuration names no file. |
hooks.php | Optional | Every request, for every module on disk | Hook registrations. They run whether or not the module is enabled. |
AdminArea.php plus router.php | Optional | Every request, before routing | An admin page of your own: route, menu entry, privileges. |
pages/ or views/ | Optional | A page is shown | Markup addressed by page name; both directory names are tried. |
controllers/ | Optional | A named controller is dispatched | One file per entry point, tried before the method of that name. |
assets/ | Optional | The browser requests it | Style, script and image files, addressed through the URL property. |
src/ | Optional | You include it yourself | Your API client and helpers. The autoloader does not reach here. |
Reference
The Three Names That Must Agree
cPanel is not CPanel.
.php. The loader builds this path and looks nowhere else.
_Module suffix, the bare name in the global namespace, then the name under the type namespace. Write new modules as the third form.
WISECP\Modules\{Type}, with the type spelled as the directory is. Inside it, a bare core class name resolves into the module namespace and fails at runtime. Import core classes, or prefix them with a backslash.
Base Class by Type
Eight of the sixteen types have one. The rest inherit nothing; their contract is whatever methods the core looks for.
| Base class | Type | Shares the trait | What its constructor already did |
|---|---|---|---|
ServerModule | Servers | Yes | Paths, configuration, language, the current admin, the default tool set, and the server when one was passed in. |
PaymentGatewayModule | Payment | No | Paths, configuration, language, the pay button label, an empty client info object. |
RegistrarModule | Registrars | Yes | Paths, configuration, language, and the crypt sub-key moved from the per-user key to the system key. |
ProductModule | Product | Yes | Paths, configuration and language, nothing else. |
SslProductModule | Product | Inherited | Abstract. The product base, plus the validation, reissue and SAN contract every certificate module answers. |
AddonModule | Addons | No | Paths, configuration, language, the admin area link, the signed-in admin and member records. |
FraudModule | Fraud | No | Paths, configuration, language, the current controller link, the signed-in identities. |
StorageModule | Storage | No | Abstract. Takes the storage configuration as a constructor array; no directory or URL property. |
SocialAuthProvider | SocialAuth | Yes | Abstract. Paths, configuration and language; every endpoint and the token check stay abstract. |
Inherited Properties
From the shared trait, so identical in server, registrar, product and social login modules.
| Property | Type | Filled by | Holds |
|---|---|---|---|
$_name | string | Constructor | Module name; the class short name when you do not set it. |
$_type | string | Constructor | Type directory, as the base class declared it. |
$dir | string | Constructor | Absolute path to the module directory, trailing separator included. |
$url | string | Constructor | Public URL of the module directory, trailing slash. Build asset links from it. |
$config | array | Constructor | The whole config.php array, already loaded. |
$lang | array | Constructor | Module strings for the active language. |
$service | array | Binding a service | The full service record; empty until you bind one. |
$product | array | Binding a service or a product | The product the service was ordered from. |
$order | array | Binding an order | The order record, while provisioning from an order. |
$user | array | Binding a service | Owner identity, contact details and billing address. |
$admin | array | Binding a service | The admin acting; empty outside the panel. |
$options | array | Binding a service | The service options, where a module keeps its per-service state. |
$addons | array | Binding a service | The service add-on records, keyed by add-on id. |
$addon_params | array | Binding a service | Configurable add-on values merged into totals; cancelled and waiting excluded. |
$addon_params_by_id | array | Binding a service | The same values per add-on, cancelled ones included. |
$requirement_params | array | Binding a service | Customer answers to product requirements, keyed by your parameter name. |
$callable_methods | array | You declare it | Allowlist of methods reachable by bare name from a URL; anything else is not. |
$error | string | Nothing, in new code | Left from the previous major version. Report failures by throwing instead. |
Inherited Helpers
// Context binding. Each takes an id OR the already-loaded record; an int is fetched for you.
public function set_service(array|int $service = []): void;
public function set_product(array|int $product = []): void;
public function set_order(array|int $order = []): void;
// Persist $this->options back onto the bound service. False when nothing is bound.
public function save_options(): bool;
// Rewrite config.php. $auto_status = true flips status on when a settings array is present.
protected function save_config($data = [], $auto_status = true);
// One log row per provider call, shown in the panel's action history.
protected function save_log($action = '', $request = '', $response = '', $processed = ''): int|bool;
// Encrypt with the module's crypt sub-key; $key overrides it for one call.
protected function encode_str(string $str = '', string $key = ''): string;
protected function decode_str(string $str = '', string $key = ''): string;
// Resolved logo URL, or an empty string.
public function logo(): string;
// Metered metric limits enabled on the bound service, keyed by metric type.
protected function enabled_metrics(): array;
protected function enabled_metric_values(): array;
protected function reapply_enabled_metrics(): void;
// Data attributes for a dropdown that loads its options from one of your methods.
protected function method_url_data(string $method): array;
// Dispatch "reset-password" to handle_reset_password(). Returns null when it does not exist.
protected function use_method($param = '');
// Add-on values for one option, multiplied by quantity unless the param opts out.
public function resolveAddonConfigurable(array $moduleParams, int $quantity = 0): array;
false as the second argument when a settings write should not also enable the module.
user by default, switched to system by the registrar base. A value encrypted under one key does not decrypt under the other.
handle_ method, dashes becoming underscores. Any other name is unreachable from the panel action buttons.
api_url in the request array; it is lifted into its own column.
The Addon Exception
An addon module does not share the trait. None of the properties above exist on it, apart from the four its own base declares.
// Properties: $config, $lang, $dir, $url, $area_link, $_name, $user, $admin, $error.
public function __construct();
// Render views/{file}.php with $variables extracted into it. Returns the markup.
protected function view($file = '', $variables = []): string;
// Privilege keys this addon adds to the admin role screen.
public function privileges();
// Called by the addon settings screen: $pFields is the posted settings,
// $accessPs the posted privilege selection.
public function save_settings($pFields, $accessPs): bool;
// Enable or disable from the addon list.
public function change_addon_status($arg = '');
// Rewrite config.php. Note the single argument: no auto-status flag here.
public function save_config($data = []): bool;
// Same crypt helpers, but the addon base defaults the sub-key to 'system'.
protected function encode_str(string $str = '', string $key = ''): string;
protected function decode_str(string $str = '', string $key = ''): string;
public function use_default_settings($formElements = null);
public function isEnabled();
Example
A skeleton that declares nothing it inherits, and the caller that drives it.
namespace WISECP\Modules\Registrars;
use Exception;
use Language;
use RegistrarModule;
use WISECP\Modules\Registrars\AcmeDomains\ApiClient;
class AcmeDomains extends RegistrarModule
{
private ?ApiClient $api = null;
// No constructor. The base one already set $_name, $_type, $dir, $url, $config and $lang.
// Build the client lazily instead: the service is bound AFTER construction, so anything
// that needs $this->service cannot run here.
private function api(): ApiClient
{
if ($this->api) return $this->api;
// src/ is not autoloaded, so the file is included explicitly.
include_once $this->dir . 'src' . DS . 'ApiClient.php';
$settings = $this->config['settings'] ?? [];
$this->api = new ApiClient(
(string) ($settings['username'] ?? ''),
$this->decode_str((string) ($settings['apiKey'] ?? '')),
);
return $this->api;
}
// NOT create(). The base class owns create() and dispatches to register() or
// transfer() depending on whether an EPP code is present. Overriding create()
// would drop the transfer branch and both of its hooks.
public function register(): array|bool
{
// The bound record carries the ordered domain in options; service['name']
// is the fallback. There is no 'domain' column on the service itself.
$domain = (string) ($this->options['domain'] ?? $this->service['name'] ?? '');
if ($domain === '') throw new Exception(Language::gc("acme/error-no-domain"));
// 'year' is the registration length. service['period'] is the billing cycle
// string ('y', 'm', 'none'), never a number of years.
$year = (int) ($this->options['year'] ?? $this->service['period_time'] ?? 1) ?: 1;
$response = $this->api()->register($domain, $year);
// Every provider call is recorded; api_url is lifted into its own column.
$this->save_log('register', ['api_url' => $this->api()->last_url, 'domain' => $domain], $response);
if (!($response['ok'] ?? false))
throw new Exception($response['message'] ?? Language::gc("acme/error-refused"));
return true;
}
}
$module = Modules::getInstance("Registrars", "AcmeDomains");
if (!$module) throw new Exception(Language::gc("modules/error-not-found"));
// Binding is a separate step, and it is what fills $service, $product, $user and $options.
$module->set_service($serviceId);
try {
// The caller always asks for create(). On a registrar that is the base method,
// which routes to register() or transfer() and runs the domain hooks around it.
$result = $module->create();
}
catch (\Throwable $e) {
// A module reports failure by throwing; the caller turns the message into a response.
return $operation->output(['status' => "error", 'message' => $e->getMessage()]);
}
// Module state the module changed goes back to the service through its own helper.
$module->save_options();
Pitfalls
A disabled or half-finished module still registers its hooks. Gate the body on the module's own enabled flag, and keep expensive work out of the file.
The autoloader maps the type namespace to the type directory and stops there. An import under the module's source folder loads no file. Include it before first use. A fatal inside a hook silently kills the rest of that hook body.
It is looked up under the module namespace and not found, at runtime rather than at lint time. The file appears to work, because the classes you did import are fine.
Configuration and language are ready there, but the service, product, owner and options are bound afterwards. A client built from service data at that moment reads empty values. It then fails against the provider with a message that points nowhere near the cause. Build it lazily on first use.
A name the core calls is not automatically the name you implement. On a registrar the base already defines create(). It chooses between registration and transfer, runs the gate and action hooks, and turns a submitted transfer into a pending state. You write register() and transfer(). Check the base before overriding.
The names configuration and settings are also tried for each other. Pick one of each and stay with it.
Related Articles
Thanks for your feedback!
Our support team is here around the clock for anything you can't find above.