Doing more with fewer fields in ProcessWire

Coming from WordPress or another CMS, one of the most attractive features of ProcessWire is its fields. It's simple to set up templates and fields to suit any setup, but as complexity and use cases start to build up, so does the number of fields that are very similar, but vary in only an option or two. While having many fields doesn't cause any performance issues, it is always easier to operate and maintain fewer fields in the templates and on the backend.

Field-Template contexts

One way to eliminate the need for a new field is to utilize Field-Template Contexts. This feature allows different templates to change how a field behaves or how it's displayed in the page editor. They're quite useful if you only need to change the field label, description, visibility, and dependencies, but nothing substantial. This is often limiting and you may still find yourself forced to create a second or third image field just to limit maximum files or change its output formatting.

Fear not, we can change that.

Exploring the core

While I was "studying" the core modules, I've come upon this great little method for Fieldtypes and Inputfields called getConfigAllowContext() which lets you define the field settings that can be overridden per template. Searching about it more, I've found this blog post from @ryan that introduces this feature in v2.5.7.

So, sitting down to modify FieldtypeImage, first problem emerges. There isn't any getConfigAllowContext() method that we can hook into. Its parent class FieldtypeFile does not have it either. It's only defined in the base class Fieldtype. Sure, we can hook into that and check fieldtype and modify context aware settings, yadda yadda yadda, but it's too tedious. What else?

Use addHookMethod(), of course.
Even if a class does not implement a method, we can implement it ourselves using hooks.

Cool. But how will we know what to add?
Reading FieldtypeFile.module, available options are laid out in the getConfigInputfields() method.

public function ___getConfigInputfields(Field $field) {
    // ...

    // max files
    $f = $this->modules->get('InputfieldInteger');
    $f->attr('name', 'maxFiles'); 
    // ...

    // output format
    $f = $this->modules->get('InputfieldRadios'); 
    $f->attr('name', 'outputFormat'); 
    // ...

    // default value page
    $f = $this->modules->get('InputfieldPageListSelect'); 
    $f->attr('name', 'defaultValuePage'); 
    // ...

    // use tags
    $f = $this->modules->get("InputfieldRadios"); 
    $f->attr('name', 'useTags');
    // ...

    // (more options)

    // ...
    return $inputfields;

Now the last thing is to write the hook itself.

Adding the method hook

Since FieldtypeFile class doesn't implement getConfigAllowContext() , we have to use addHookMethod(), addHookAfter() has no effect. In site/ready.php, adding:

$this->addHookMethod('FieldtypeFile::getConfigAllowContext', function (HookEvent $e) {
    $allowables = ['maxFiles', 'defaultValuePage', 'outputFormat'];
    $e->return = array_merge($e->return, $allowables);

gives us what we need.

Logging in to the backend, we can see the results for ourselves. After allowing additional settings, they are unlocked in the template settings.

While editing a page, when we drop multiple images into an image field, only the first one is uploaded. Trying to adding more, the image is replaced with the new one.

Bottom line

Fields are the foundations of ProcessWire. They're capable and require no effort to use. However, for better sustainability, more compact and D.R.Y. code, try not to indulge in them, and repurpose as much as you can. Master Field-Template contexts, they can come in pretty handy and let you achieve so much with very little.