pw-module-development
Use when building, structuring, or refactoring native backend modules for ProcessWire using PHP 8.4 and strict typing.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Use when building, structuring, or refactoring native backend modules for ProcessWire using PHP 8.4 and strict typing.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
Use when designing, structuring, or rendering HTML for ProcessWire Admin interfaces, custom Process modules, or Inputfields.
Use when brainstorming or designing ProcessWire modules, templates, field schemas, or hooks to resolve ambiguity and validate architecture before implementation.
Use when creating, executing, or managing Pest tests within ProcessWire or ProcessWire modules, including Test-Driven Development (TDD) tasks.
Use when encountering any bug, test failure, blank screen of death, or unexpected behavior in ProcessWire before proposing fixes.
Use when creating or updating module documentation, package READMEs, architecture guides, or CLI command references for ProcessWire projects.
Use when creating implementation plans from approved specifications to ensure ProcessWire-native architecture, safe migration structures, and strict test-driven task execution.
| name | pw-module-development |
| description | Use when building, structuring, or refactoring native backend modules for ProcessWire using PHP 8.4 and strict typing. |
| risk | safe |
| source | processwire-boost |
| date_added | 2026-04-08 |
This skill dictates the principles of building Modules (not simple plugins) for the ProcessWire CMS ecosystem. It requires "deep thinking", "zero tolerance for structural flaws", adherence to "strict typing", and total integration with ProcessWire's core philosophy. It is tailored specifically for ProcessWire 3.x architectures and PHP 8+ standards.
Before writing any module code, run through this architectural checklist:
autoload (runs on every request), singular (single instance), or only initialized in the admin interface? Do not arbitrarily make modules autoload if they do not require global event hooks.Pages::saveReady)?wire() vs Dependency Injection: Within the module's scope, use $this->pages or $this->wire('pages'). Avoid utilizing the global wire() function inside module classes.readonly classes, and property type declarations? Is declare(strict_types=1); at the top of the file?composer.json? Has a dedicated src/ folder been established for PSR-4 autoloading?site/modules/MyCustomModule/) and the primary file MyCustomModule.module.php.ProcessWire\Module interface (or extend ProcessWire\WireData). Depending on the use case, implement Module (standard) or ConfigurableModule (for graphical settings).public static function getModuleInfo() method using modern array notation (including version => 100, title, summary, and constraints like requires => ['ProcessWire>=3.0.210', 'PHP>=8.4.0']).vendor/autoload.php inside the module's init() method.declare(strict_types=1);.___): public function ___myCustomMethod().<?php
declare(strict_types=1);
namespace MyCustomModule;
use ProcessWire\Module;
use ProcessWire\WireData;
use ProcessWire\HookEvent;
class MyCustomModule extends WireData implements Module
{
// PHP 8.4 Property Promotion & Strict Typing
public function __construct(
protected readonly string $logName = 'my-custom-module',
public int $defaultLimit = 10
) {}
public static function getModuleInfo(): array
{
return [
'title' => 'My Custom Module',
'version' => '0.1.1',
'summary' => 'Executes custom business logic effectively.',
'autoload' => true,
'singular' => true,
'requires' => [
'ProcessWire>=3.0.210',
'PHP>=8.4.0',
],
'icon' => 'cogs'
];
}
public function init(): void
{
// Module-scoped composer autoloading
$autoloader = __DIR__ . '/vendor/autoload.php';
if (file_exists($autoloader)) {
require_once $autoloader;
}
// Attach system Hooks
$this->addHookAfter('Pages::saveReady', $this, 'hookSaveReady');
}
protected function hookSaveReady(HookEvent $event): void
{
$page = $event->arguments(0);
if ($page->template->name !== 'my_target_template') {
return;
}
// Execute business logic...
$this->wire()->log->save($this->logName, "Page {$page->id} triggered.");
}
}
$this->wire()->input->post('email', 'email') or $this->wire()->sanitizer->text($string).$this->wire()->database object. In ProcessWire this is typically a WireDatabasePDO wrapper (PDO-like API), not a native PDO instance. Always use prepared statements. Do not use string interpolation for SQL queries.___uninstall()), ensure complete cleanup of custom database tables, cache files, and residual data.// Database Best Practice:
$database = $this->wire()->database;
$query = $database->prepare("SELECT id, data FROM custom_table WHERE status = :status");
$query->bindValue(':status', 1, \PDO::PARAM_INT);
$query->execute();
$results = $query->fetchAll(\PDO::FETCH_ASSOC);
WireData or Wire, do not use wire() to fetch other objects. Use direct property access like $this->pages, $this->modules, $this->sanitizer, $this->input.$this->modules->get('ModuleName') and verify the instance against null, ensuring configuration and state correctly boot up prior to interaction.composer (Only permitted when module-specific, isolated third-party logic is actually necessary).wire tinker (Command-line REPL for instantly observing ProcessWire code and directly interacting with API objects).(Pass these direct prompts to the agent to initiate workflows instantly)
[Scaffolding - Core Module Skeleton]
"Generate a new ProcessWire module named
CustomLogger. It should be configured asautoloadandsingular. Define aPages::saveReadyhook insideinit(). The hook needs to log an entry (undercustom-loggerin ProcessWire system logs) exclusively for pages matching thearticletemplate. Strictly follow the wire-module-development skill protocols: use PHP 8.4 features, valid strict types, and robust class structures."
[Database Integration - Automated Installer]
"Implement
___install()and___uninstall()methodology for the module. Upon installation, structurally generate an InnoDB database table namedcustom_log_tablefeaturing 3 columns: id, page_id, and message (utilize utf8mb4 charset). During removal, appropriately drop this table. Write highly secure PDO execution code without any interpolation."
[Security Protocol - Input Sanitization]
"Develop a
processInputmethod leveraging the ProcessWire Input API. Methodically pull 3 fields (title,description) received via POST. Filter and secure them utilizing the designated sanitizer components ($sanitizer->email(),$sanitizer->text(), etc.) and store the clean results in an output array."
$pages->find("name={$_GET['name']}") -> Severe Security Vuln! Selectors must not accept unrestrained inputs without applying Sanitizer.\ProcessWire\wire('pages') -> Calling the global function from within a module is a poor and computationally unnecessary pattern. Instead: $this->wire()->pages or $this->pages.try { ... } catch(\Exception $e) {} -> Swallowing exceptions obscures debugging. Continually implement error logging: $this->wire()->log->error($e->getMessage()).$this->_('English String'). Translations are handled via the ProcessWire translation UI.CRITICAL RULE FOR ALL AI AGENTS: When you need to understand, use, or hook into a ProcessWire core class or module, you MUST NEVER guess or hallucinate the API methods.
.agents/docs/index.md..agents/docs/core/Page.md), and use your file reading tools to read its methods, parameters, and hookable (🪝) events before writing any code.See also:
pw-expertfor detailed API variable access patterns across different contexts.
When processing many pages, use generators to avoid memory exhaustion:
function iteratePages(Pages $pages, string $selector): Generator
{
$start = 0;
$limit = 100;
while (true) {
$batch = $pages->find("{$selector}, start={$start}, limit={$limit}");
if ($batch->count() === 0) {
break;
}
foreach ($batch as $page) {
yield $page;
}
$start += $limit;
$pages->uncacheAll();
}
}
enum PageStatus: int
{
case Draft = 0;
case Published = 1;
case Hidden = 1024;
case Unpublished = 2048;
}
$icon = match ($page->template->name) {
'article' => 'file-text',
'gallery' => 'images',
'contact' => 'envelope',
default => 'file',
};
$pages->uncacheAll() after processing large batches$page->of(false) before modifying output-formatted pages$pages->count($selector) over $pages->find($selector)->count()use ProcessWire\WireException;
if (!$field || !$field->id) {
throw new WireException("Field '{$fieldName}' not found.");
}
WireException for ProcessWire-specific errors\RuntimeException for general PHP errors$this->wire()->log->error() in modulesnamespace ProcessWire; for templates, modules, and migrationsfinal class by default — only remove final if inheritance is explicitly needed$camelCase for variables, PascalCase for classes, snake_case for database field names