ProcessWire Custom Module Development
ProcessWire's module system is one of the main arguments for choosing this CMS. Any module is a PHP class inheriting Wire or one of its descendants. A module can be a hook on system events, custom field type, process (admin page), or just a set of functions available via $modules->get("MyModule").
Module Types
| Type | Base Class | Purpose |
|---|---|---|
| Normal | WireData / Wire |
Helpers, utilities, API integrations |
| Fieldtype | Fieldtype |
New field type |
| Inputfield | Inputfield |
Field editing widget |
| Process | Process |
Admin page |
| Textformatter | Textformatter |
Text processing on output |
| Module (hookable) | WireData + hooks |
Behavior extension |
Minimal Module Structure
/site/modules/
MyModule/
MyModule.module.php # main file (or MyModule.php)
MyModule.info.php # metadata (optional, can be inline)
<?php
// MyModule.module.php
class MyModule extends WireData implements Module {
public static function getModuleInfo(): array {
return [
'title' => 'My Module',
'version' => '1.0.0',
'summary' => 'Module description',
'author' => 'Dev Name',
'requires' => ['ProcessWire>=3.0.0'],
'autoload' => true, // load on every request
'singular' => true, // one instance per request
];
}
public function init(): void {
// Hooks and initialization
$this->addHookAfter('Pages::saved', $this, 'onPageSaved');
}
protected function onPageSaved(HookEvent $event): void {
$page = $event->arguments(0);
if ($page->template->name !== 'product') return;
// logic after product page save
}
}
Hooks: Before and After
ProcessWire allows intercepting nearly any API method:
// Hook "before" — can modify arguments or cancel execution
$this->addHookBefore('Pages::delete', function(HookEvent $e) {
$page = $e->arguments(0);
if ($page->template->name === 'protected') {
$e->replace = true; // cancel original method
throw new WireException("Deletion forbidden for protected template");
}
});
// Hook "after" — can modify return value
$this->addHookAfter('Page::render', function(HookEvent $e) {
$html = $e->return;
// add metric at end of page
$e->return = str_replace('</body>', '<script>analytics();</script></body>', $html);
});
Module Configuration
Modules with settings implement ConfigurableModule interface:
class MyModule extends WireData implements Module, ConfigurableModule {
protected static array $defaults = [
'api_key' => '',
'cache_ttl' => 3600,
'debug_mode' => 0,
];
public static function getDefaultConfig(): array {
return self::$defaults;
}
public static function getModuleConfigInputfields(array $data): InputfieldWrapper {
$modules = wire('modules');
$fields = new InputfieldWrapper();
$data = array_merge(self::$defaults, $data);
$f = $modules->get('InputfieldText');
$f->attr('name', 'api_key');
$f->label = 'API Key';
$f->value = $data['api_key'];
$fields->add($f);
$f = $modules->get('InputfieldInteger');
$f->attr('name', 'cache_ttl');
$f->label = 'Cache Time (seconds)';
$f->value = $data['cache_ttl'];
$fields->add($f);
return $fields;
}
}
Values are read inside module via $this->api_key, $this->cache_ttl.
Example: External API Integration Module
class ExternalApiSync extends WireData implements Module {
public static function getModuleInfo(): array {
return [
'title' => 'External API Sync',
'version' => '1.2.0',
'autoload' => false,
'singular' => true,
];
}
public function syncProducts(): int {
$apiUrl = "https://api.supplier.com/v2/products";
$headers = ["Authorization: Bearer {$this->api_key}"];
$http = $this->wire('modules')->get('WireHttp');
$response = $http->getJSON($apiUrl, true, [], $headers);
if (!$response) {
$this->wire('log')->error("ExternalApiSync: failed to fetch data");
return 0;
}
$count = 0;
foreach ($response['items'] as $item) {
$p = $this->wire('pages')->get("template=product, external_id={$item['id']}");
if (!$p->id) {
$p = new Page();
$p->template = 'product';
$p->parent = $this->wire('pages')->get('/catalog/');
}
$p->title = $item['name'];
$p->external_id = $item['id'];
$p->price = $item['price'];
$p->save();
$count++;
}
return $count;
}
}
Call from template or CLI:
$sync = $modules->get('ExternalApiSync');
$count = $sync->syncProducts();
echo "Synced: $count products";
Process Module: Admin Page
class ProcessMyAdmin extends Process {
public static function getModuleInfo(): array {
return [
'title' => 'My Admin Page',
'version' => '1.0.0',
'requires' => ['ProcessWire>=3.0.0'],
'page' => [
'name' => 'my-admin',
'title' => 'My Admin',
'parent' => 'admin',
],
];
}
public function execute(): string {
$out = "<h2>Dashboard</h2>";
$out .= "<p>Products count: " . $this->pages->count("template=product") . "</p>";
return $out;
}
}
Upon installation, module automatically creates page in admin tree at specified path.
Module Development Timeline
| Type | Complexity | Timeline |
|---|---|---|
| Hook + utility | low | 2–6 h |
| API integration | medium | 1–3 days |
| Custom Fieldtype + Inputfield | high | 3–7 days |
| Process (full admin page) | medium–high | 2–5 days |







