Разработка кастомного модуля ProcessWire
Модульная система ProcessWire — один из главных аргументов в пользу этой CMS. Любой модуль — это PHP-класс, наследующий Wire или один из его потомков. Модуль может быть хуком на системные события, кастомным типом поля, процессом (страницей в админке) или просто набором функций, доступных через $modules->get("MyModule").
Типы модулей
| Тип | Базовый класс | Назначение |
|---|---|---|
| Обычный | WireData / Wire |
Хелперы, утилиты, интеграции API |
| Fieldtype | Fieldtype |
Новый тип поля |
| Inputfield | Inputfield |
Виджет редактирования поля |
| Process | Process |
Страница в админке |
| Textformatter | Textformatter |
Обработка текста при выводе |
| Module (hookable) | WireData + хуки |
Расширение поведения |
Минимальная структура модуля
/site/modules/
MyModule/
MyModule.module.php # основной файл (или MyModule.php)
MyModule.info.php # метаданные (опционально, можно инлайн)
<?php
// MyModule.module.php
class MyModule extends WireData implements Module {
public static function getModuleInfo(): array {
return [
'title' => 'My Module',
'version' => '1.0.0',
'summary' => 'Описание модуля',
'author' => 'Dev Name',
'requires' => ['ProcessWire>=3.0.0'],
'autoload' => true, // загружать при каждом запросе
'singular' => true, // один экземпляр на запрос
];
}
public function init(): void {
// Хуки и инициализация
$this->addHookAfter('Pages::saved', $this, 'onPageSaved');
}
protected function onPageSaved(HookEvent $event): void {
$page = $event->arguments(0);
if ($page->template->name !== 'product') return;
// логика после сохранения страницы типа product
}
}
Хуки: до и после
ProcessWire позволяет перехватывать практически любой метод API:
// Хук "до" — можно изменить аргументы или отменить выполнение
$this->addHookBefore('Pages::delete', function(HookEvent $e) {
$page = $e->arguments(0);
if ($page->template->name === 'protected') {
$e->replace = true; // отменить выполнение оригинального метода
throw new WireException("Удаление запрещено для шаблона protected");
}
});
// Хук "after" — можно изменить возвращаемое значение
$this->addHookAfter('Page::render', function(HookEvent $e) {
$html = $e->return;
// добавить метрику в конец страницы
$e->return = str_replace('</body>', '<script>analytics();</script></body>', $html);
});
Конфигурация модуля
Модули с настройками реализуют интерфейс ConfigurableModule:
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 = 'Время кэша (секунды)';
$f->value = $data['cache_ttl'];
$fields->add($f);
return $fields;
}
}
Значения читаются внутри модуля через $this->api_key, $this->cache_ttl.
Пример: модуль интеграции с внешним API
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: не удалось получить данные");
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;
}
}
Вызов из шаблона или CLI:
$sync = $modules->get('ExternalApiSync');
$count = $sync->syncProducts();
echo "Синхронизировано: $count товаров";
Process-модуль: страница в админке
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>Панель управления</h2>";
$out .= "<p>Количество товаров: " . $this->pages->count("template=product") . "</p>";
return $out;
}
}
При установке модуль автоматически создаёт страницу в административном дереве по указанному пути.
Тестирование и отладка
// В _init.php при debug-режиме
if ($config->debug) {
$log = $modules->get('MyModule');
// ProcessWire логирует в /site/assets/logs/
wire('log')->save('my-module', "Модуль загружен: " . date('H:i:s'));
}
bd() — встроенная функция дебага (аналог var_dump с форматированием в Tracy Debugger):
bd($page->getArray(), 'page fields'); // выводит в Tracy bar
Сроки разработки модулей
| Тип модуля | Сложность | Срок |
|---|---|---|
| Хук + утилита | низкая | 2–6 ч |
| Интеграция с API | средняя | 1–3 дня |
| Кастомный Fieldtype + Inputfield | высокая | 3–7 дней |
| Process (полноценная admin-страница) | средняя–высокая | 2–5 дней |







