ProcessWire Custom Module Development

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.
Development and maintenance of all types of websites:
Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:
Development stages
Latest works
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    822
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    847
  • image_website-sbh_0.png
    Website development for SBH Partners
    999
  • image_website-_0.png
    Website development for Red Pear
    451

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