ProcessWire
Recipes_

processwire-module-checklist

What I Do

I provide a comprehensive checklist for ProcessWire module development:

  • Pre-development planning and requirements
  • Hook setup and lifecycle methods
  • Field configuration patterns
  • Code quality standards
  • Testing strategies
  • Deployment and versioning
  • Troubleshooting common issues

When to Use Me

Use this skill when:

  • Starting a new module development project
  • Reviewing code before commit
  • Debugging module issues
  • Setting up development environment
  • Planning module architecture

Quick Reference: Module Structure

<?php namespace ProcessWire;

class MyModule extends WireData implements Module, ConfigurableModule {

    public static function getModuleInfo() {
        return [
            'title' => 'My Module',
            'summary' => 'Brief description',
            'version' => 100,  // 1.0.0
            'author' => 'Author Name',
            'href' => 'https://example.com',
            'singular' => true,
            'autoload' => true,
            'requires' => 'ProcessWire>=3.0.0',
            'permission' => 'my-module',
            'permissions' => [
                'my-module' => 'Use My Module',
            ],
            'page' => [  // Optional: creates admin page
                'name' => 'my-module',
                'parent' => 'setup',
                'title' => 'My Module',
            ],
            'icon' => 'cog',
        ];
    }

    public function __construct() {
        $this->set('mySetting', 'default');
        parent::__construct();
    }

    public function init() {
        // Early hooks, API may not be ready
        $this->addHookBefore('Pages::save', $this, 'hookPageSave');
    }

    public function ready() {
        // API is ready, most hooks go here
        if($this->page->template == 'admin') {
            // Admin-specific setup
        }
    }

    public function ___hookPageSave(HookEvent $event) {
        $page = $event->arguments(0);
        // Hook logic
    }

    public static function getModuleConfigInputfields(array $data) {
        $inputfields = new InputfieldWrapper();
        // Config fields...
        return $inputfields;
    }
}

Phase 1: Pre-Development

Requirements Checklist

  • Define what “done” looks like
  • List all features to implement
  • Identify affected templates/fields
  • List edge cases to handle
  • Research existing patterns in core modules
  • Check for similar modules in /site/modules/

Module Type Selection

NeedModule TypeBase Class
Admin tool with pagesProcessProcess implements Module
Modify behaviorAutoloadWireData implements Module
Custom field typeFieldtypeFieldtype implements Module
Custom inputInputfieldInputfield implements Module
Text formattingTextformatterTextformatter implements Module
Email sendingWireMailWireMail implements Module

Phase 2: Hook Setup

init() vs ready()

Need to access $page, $user, $pages?
├─ YES → Use ready()
└─ NO → Use init()
    (for very early hooks like URL routing)

Hook Type Selection

NeedHook TypeExample
Validate/modify argumentsbeforePages::saveReady
Modify return valueafterPages::saved
Replace behaviorreplacePage::render
Add method to classMethodPage::myMethod
Add property to classPropertyPage::myProperty

Hook Patterns

// Basic hook
$this->addHookBefore('Pages::save', $this, 'myHook');

// Conditional hook (template-specific)
$this->addHookBefore('Page(template=product)::render', $this, 'myHook');

// With priority (lower = earlier)
$this->addHookBefore('Pages::save', $this, 'myHook', ['priority' => 50]);

// Store hook ID for removal
$hookId = $this->addHookBefore('Pages::save', $this, 'myHook');
$this->removeHook($hookId);  // Remove later

Hookable Methods

// Use ___ prefix for hookable methods
public function ___savePage(HookEvent $event) {
    // Other modules can hook this
}

// Call without ___ to apply hooks
$this->savePage($event);

Phase 3: Field Configuration

Configuration Access Pattern

ALWAYS use $field->get('property') - works everywhere:

// CORRECT: Works in all contexts
$field = $this->fields->get('my_field');
$value = $field->get('myConfigOption');

// WRONG: Only works with addHookProperty
$inputfield->myConfigOption;  // Don't use this pattern

getConfigInputfields Hook

public function addConfigHook(HookEvent $event) {
    if (!$event->object instanceof InputfieldText) return;
    
    $inputfields = $event->return;
    $field = $this->fields->get($event->object->name);
    
    $f = $this->modules->get('InputfieldCheckbox');
    $f->attr('name', 'myOption');
    $f->label = $this->_('My Option');
    if($field && $field->get('myOption')) {
        $f->attr('checked', 'checked');
    }
    
    $inputfields->append($f);
}

Pattern Mixing Warning

// NEVER mix patterns - causes configuration not saving bugs

// In one place:
$field->get('generateGuid');  // ✓ Correct

// In another place:
$inputfield->generateGuid;    // ✗ WRONG - breaks persistence

Phase 4: Code Quality

Naming Conventions

ElementConventionExample
ClassesPascalCaseMyModule
MethodscamelCasesavePage()
VariablescamelCase$myVariable
ConstantsUPPER_CASEMAX_RETRIES
Hookable methods___ prefix___savePage()

PHPDoc Example

/**
 * Generate GUID on page save
 *
 * Hookable method called before page is saved.
 *
 * @param HookEvent $event Hook event object
 * @return void
 */
public function ___generateGuid(HookEvent $event) {
    $page = $event->arguments(0);
}

Input Sanitization

// Text input
$text = $sanitizer->text($input->post->text);

// For selectors (CRITICAL for security)
$search = $sanitizer->selectorValue($input->get->search);
$pages->find("title~=$search");

// Page names
$name = $sanitizer->pageName($input->post->name);

// Integers
$int = (int) $input->post->number;

Exception Types

TypeUse When
WireExceptionGeneral ProcessWire errors
WirePermissionExceptionAccess denied
Wire404ExceptionPage not found
WireValidationExceptionValidation failures

Phase 5: Testing

Manual Testing Checklist

  • Module installs without errors
  • Module uninstall works cleanly
  • Hooks fire at expected times
  • New pages work correctly
  • Existing pages not broken
  • Configuration persists after save
  • No PHP warnings in logs

Edge Cases Checklist

  • Empty strings handled
  • Null values handled
  • Maximum values enforced
  • Zero values handled
  • Special characters (quotes, unicode)
  • SQL injection attempts blocked

Performance Testing

// Profile slow operations
$timer = \Debug::timer();
$result = $this->expensiveOperation();
$elapsed = $timer->total();

if($elapsed > 0.2) {
    $this->log->save('performance', "Slow: {$elapsed}s");
}

Common Performance Issues

IssueFix
N+1 queriesUse caching or preload data
Heavy render hooksMove to save hooks
Large result setsAdd limit() to selectors
No cachingUse WireCache for expensive lookups

Phase 6: Deployment

Version Numbering

// Semantic versioning in getModuleInfo()
'version' => 102,  // 1.0.2 = 1*100 + 0*10 + 2

// MAJOR.MINOR.PATCH
// 100 = 1.0.0
// 101 = 1.0.1 (bug fix)
// 110 = 1.1.0 (new feature)
// 200 = 2.0.0 (breaking change)

Changelog Format

## [1.2.0] - 2024-01-15

### Added
- New feature description

### Changed
- Changed behavior description

### Fixed
- Bug fix description

### Breaking Changes
- Breaking change with migration notes

Pre-Deployment Checklist

  • Version incremented in getModuleInfo()
  • CHANGELOG.md updated
  • Full test suite passes
  • Tested on staging environment
  • Database backup ready
  • Rollback plan documented

Phase 7: Troubleshooting

Configuration Not Saving

SymptomLikely CauseFix
Values don’t persistPattern mixingUse $field->get() consistently
UI doesn’t appearWrong object typeCheck $event->object is Inputfield
Defaults not loadingMissing name attrSet $f->attr('name', 'key')
// Debug configuration issues
public function addConfigHook(HookEvent $event) {
    $this->log->save('debug', 'Object: ' . get_class($event->object));
}

Hooks Not Firing

SymptomLikely CauseFix
No executionModule not installedInstall via Modules
Still no executionAutoload offSet 'autoload' => true
Only sometimesWrong methodUse ready() for API access
// Debug hook execution
public function myHook(HookEvent $event) {
    $this->log->save('hook-test', 'Hook executed');
}

Wrong Object Context

// Always verify object type
public function myHook(HookEvent $event) {
    $obj = $event->object;
    
    if($obj instanceof Page) {
        // Safe Page operations
    } else if($obj instanceof Field) {
        // Safe Field operations
    } else {
        $this->log->save('error', 'Unexpected: ' . get_class($obj));
        return;
    }
}

Module Not Loading

# Check PHP syntax
php -l site/modules/MyModule/MyModule.module

# Check error logs
tail -f site/assets/logs/errors.txt
SymptomCheck
Not in Modules listSyntax error, wrong namespace
Autoload errorsMissing implements Module
Fatal errorsMissing required keys in getModuleInfo()

Investigation Protocol

Before removing or changing code:

  1. Read entire file - understand all hook placements
  2. Search all usages: grep -rn "pattern" MyModule.module
  3. Check git history: git log -p --all -S "pattern"
  4. Ask: “Why was this code added?”
  5. Test change in isolation first
  6. Update ALL references consistently
  7. Run full test suite
  8. Commit with clear message

Development Environment

Debug Configuration

// /site/config.php - Development
$config->debug = true;
$config->advanced = true;
$config->chmodDir = '0755';
$config->chmodFile = '0644';

Log Locations

/site/assets/logs/errors.txt      # PHP errors
/site/assets/logs/exceptions.txt  # Exceptions
/site/assets/logs/my-module.txt   # Module logs

VS Code Settings

{
  "files.associations": {
    "*.module": "php"
  },
  "intelephense.files.associations": {
    "*.module": "php"
  }
}

Cross-References

For ThisUse Skill
Hook types and eventsprocesswire-hooks
Field types and valuesprocesswire-fields
Module architectureprocesswire-modules
Admin interfacesprocesswire-advanced-modules
Field configuration patternsprocesswire-field-configuration
Selectors for finding pagesprocesswire-selectors

Key Takeaways

  1. Single pattern for config: Always use $field->get('property')
  2. Hookable methods: Use ___ prefix, call without ___
  3. init vs ready: Use ready() when you need $page, $user, etc.
  4. Sanitize everything: Especially selector values
  5. Test edge cases: Empty, null, max, special characters
  6. Version properly: Semantic versioning, update changelog
  7. Investigate before removing: Search usages, check history