bernhard

How to create your very own custom Fieldtypes in ProcessWire

Recommended Posts

Building your very own custom Fieldtypes in ProcessWire seems to be hard, but it actually is not, once you get the concept. In this post I'll share a simple and as minimalistic module as it can get that everybody can use as a boilerplate for his own Fieldtypes.

  1. A Fieldtype that does not store any information in the database
  2. A Fieldtype that stores information in the database (to be done)
  3. Make your Fieldtype configurable

 

1. A Fieldtype that does not store any information in the database

Some of you might know the excellent RuntimeMarkup module by @kongondo. I've used it a lot in the past, but I get more and more into developing custom fieldtypes rather than hacking the runtime module to my needs. That has several advantages, but it has also one major drawback: It is always a pain to setup a new Fieldtype, because there are lots of methods (see the base class /wire/core/Fieldtype.php) and you have to know which methods you need, which ones you have to remove and adopt etc.; Not any more!

Ryan has developed a showcase module (the Events Fieldtype) to learn from, but when I first peaked into it, it felt quite complex and I thought it would be a lot of effort to learn Fieldtype development). There are also some easy and small Fieldtypes to learn from, like the FieldtypeCheckbox. But all of them require a good understanding of what is going on, you need to modify existing config inputfields that might have been added to that fieldtype and afterall it's not as easy as it could be.

With my new approach I plan to use a boilerplate fieldtype to start from (in OOP terms to "extend" from) and only change the parts i need... More on that later.

Here is the boilerplate module with some debugging info to illustrate the internal flow on the tracy console:

Spoiler

<?php namespace ProcessWire;
/**
 * Simple Fieldtype Boilerplate that does not store any data in the database
 *
 * @author Bernhard Baumrock, 03.10.2018
 * @license Licensed under MIT
 * @link https://www.baumrock.com
 */
class FieldtypeMarkup extends Fieldtype {

  public static function getModuleInfo() {
    return [
      'title' => 'Markup',
      'version' => '0.0.1',
      'summary' => 'Most simple Fieldtype to only display an InputfieldMarkup in the page editor',
      'icon' => 'code',
    ];
  }

  /**
   * link the core inputfieldmarkup to this fieldtype
   */
  public function getInputfield(Page $page, Field $field) {
    return $this->wire->modules('InputfieldMarkup');
  }

  /**
   * very first retrieval of the value
   */
  public function ___loadPageField(Page $page, Field $field) {
    d('value set manually to 1', '___loadPageField');
    return 1;
  }
  
  /**
   * value was loaded, now we can convert it
   * eg we can convert an integer page id to a page object
   */
  public function ___wakeupValue(Page $page, Field $field, $value) {
    $value = $this->wire->pages->get($value);
    d($value, '___wakeupValue');
    return $value;
  }

  /**
   * convert the wakeupValue to the given format
   * eg: convert a page object to a string
   */
  public function ___formatValue(Page $page, Field $field, $value) {
    $value = "this is page {$value->id}";
    d($value, '___formatValue');
    return $value;
  }

  /**
   * sanitize the value whenever it is stored or pulled
   */
  public function sanitizeValue(Page $page, Field $field, $value) {
    d($value, 'sanitizeValue');
    return $value;
  }

  /**
   * The following functions are defined as replacements to keep this fieldtype out of the DB
   */
  public function ___sleepValue(Page $page, Field $field, $value) { return $value; }
  public function getLoadQuery(Field $field, DatabaseQuerySelect $query) { return $query; }
  public function ___savePageField(Page $page, Field $field) { return true; }
  public function ___deletePageField(Page $page, Field $field) { return true; }
  public function ___deleteField(Field $field) { return true; }
  public function getDatabaseSchema(Field $field) { return array(); }
  public function ___createField(Field $field) { return true; }
  public function ___getCompatibleFieldtypes(Field $field) { return new Fieldtypes(); }
  public function getLoadQueryAutojoin(Field $field, DatabaseQuerySelect $query) { return null; }
  public function getMatchQuery($query, $table, $subfield, $operator, $value) {
    throw new WireException("Field '{$query->field->name}' is runtime and not queryable");
  }

}

 

See the module in action in the tracy console ( @adrian has done such an amazing job with that module!!!):

GVIBut4.png

The code should be self-explaining. What is interesting, though, is that after the ### getUnformatted ### dump there is only one call to "___formatValue". All the other calls (loadPageField, wakeupValue and sanitizeValue) are not executed because the value is already stored in memory. Only the formatting part was not done at that point.

With that concept it is very easy to create your very own custom Fieldtypes:

<?php namespace ProcessWire;
/**
 * Demo Fieldtype Extending the Boilerplate Runtime Fieldtype
 * 
 * @author Bernhard Baumrock, 03.10.2018
 * @license Licensed under MIT
 * @link https://www.baumrock.com
 */
class FieldtypeDemo extends FieldtypeMarkup {

  public static function getModuleInfo() {
    return [
      'title' => 'Demo',
      'version' => '0.0.1',
      'summary' => 'Demo Fieldtype',
      'icon' => 'code',
    ];
  }

  /**
   * convert the wakeupValue to the given format
   * eg: convert a page object to a string
   */
  public function ___formatValue(Page $page, Field $field, $value) {
    $value = parent::___formatValue($page, $field, $value) . " - but even better!";
    d($value, '___formatValue --> convert to whatever format you need');
    return $value;
  }

}

BIMp0nn.png

Notice the change "but even better" in the last two dumps. I think it can't get any easier, can it?! 🙂 I'll continue testing and improving this module, so any comments are welcome!

 

2. A Fieldtype that stores information in the database

To be done - I'll update this post once I had the time to dig into it!

  • Like 16

Share this post


Link to post
Share on other sites
5 hours ago, bernhard said:

1. A Fieldtype that does not store any information in the database

@bernhard, you can actually remove a few more methods from such a fieldtype - FieldtypeFieldsetOpen is a good one to refer to for a fieldtype that doesn't store anything in the database.

A while ago I made a simple runtime-only fieldtype that generates inputfield markup from PHP files in a subfolder within /site/templates/ - sort of like a stripped-back version of RuntimeMarkup without any config fields in admin. It was just intended for use in my own projects and I didn't think it was worth announcing publicly seeing as we already have kongondo's module, but I've moved it to a public repo in case it is interesting for you to take a look at: https://github.com/Toutouwai/FieldtypeRuntimeOnly

  • Like 3
  • Thanks 1

Share this post


Link to post
Share on other sites

Thx @Robin S, I had a look to those fieldtypes and you where partially right. I removed sleepValue and deletePageField since they should never get called on a non-db fieldtype. But your runtimeonly field does actually have too few methods if you want to keep it completely out of the db. Your fieldtype creates an empty db table. Not sure if that is intended?

 

I've invested some more time and really like this approach of building new Fieldtypes! Is really simple, see this example of a new Fieldtype called "FieldtypeNow":

I renamed the base fieldtype to "BaseFieldtypeRuntime" and it really does not do anything other than providing the boilerplate. It does not even show up in the list when you create a new field in your pw installation (screenshot later).

This is the current code:

Spoiler

<?php namespace ProcessWire;
/**
 * Simple Fieldtype boilerplate that does not store any data in the database
 *
 * @author Bernhard Baumrock, 03.10.2018
 * @license Licensed under MIT
 * @link https://www.baumrock.com
 */


class BaseFieldtypeRuntime extends Fieldtype {

  protected $page;
  protected $field;

  public static function getModuleInfo() {
    return [
      'title' => 'BaseFieldtypeRuntime',
      'version' => '1.0.0',
      'summary' => 'Simple Fieldtype boilerplate that does not store any data in the database',
      'icon' => 'code',
    ];
  }

  /**
   * link the core InputfieldMarkup to this fieldtype
   */
  public function getInputfield(Page $page, Field $field) {
    $this->page = $page;
    $this->field = $field;

    /** @var InputfieldMarkup $inputfield */
    $inputfield = $this->wire->modules('InputfieldMarkup');
    $field->label = $field->label ?: $field->name;

    // replace the inputfield's render method with our own
    $inputfield->addHookBefore('render', function(HookEvent $event) {
      $event->replace = true;
      $event->return = $this->render();
    });

    // call the renderReady method when the inputfield is available
    $inputfield->addHookAfter('renderReadyHook', function(HookEvent $event) {
      $this->renderReady();
    });

    // modify config screen to disable AJAX options
    $inputfield->addHookAfter('getConfigInputfields', function($event) {
      $inputfields = $event->return;
      $options = [
        Inputfield::collapsedNo,
        Inputfield::collapsedYes,
        Inputfield::collapsedNever,
      ];
      $collapsed = $inputfields->get('collapsed');
      foreach(array_keys($collapsed->getOptions()) as $option) {
        if(in_array($option, $options)) continue;
        $collapsed->removeOption($option);
      }
    });

    return $inputfield;
  }

  /**
   * methods for providing the field markup and the renderReady actions
   * in this case nothing happens at all, but you can extend this fieldtype
   * and do whatever you need by overriding these two methods
   */
  public function render() { return ''; }
  public function renderReady() { return; }

  /**
   * The following functions are defined as replacements to keep this fieldtype out of the DB
   */
  public function sanitizeValue(Page $page, Field $field, $value) { return; }
  public function loadPageField(Page $page, Field $field) { return; }
  public function getLoadQuery(Field $field, DatabaseQuerySelect $query) { return $query; }
  public function savePageField(Page $page, Field $field) { return true; }
  public function deleteField(Field $field) { return true; }
  public function getDatabaseSchema(Field $field) { return array(); }
  public function createField(Field $field) { return true; }
  public function getCompatibleFieldtypes(Field $field) { return $this->wire(new Fieldtypes()); }
  public function getLoadQueryAutojoin(Field $field, DatabaseQuerySelect $query) { return null; }
  public function getMatchQuery($query, $table, $subfield, $operator, $value) {
    throw new WireException("Field '{$query->field->name}' is runtime and not queryable");
  }

}

 

Actually it does only define the inputfield and add some hooks to replace the render and renderReady methods by our own implementations and define all the functions necessary to keep the db out of the game:

1cMOt10.png

Simple, right? 🙂 

This is how the installation screen looks like:

ZoW7fJp.png

The BaseFieldtype is set as dependency, so FieldtypeNow can only be installed when the Base Fieldtype is available. Once installed, you can easily create a new field of that type:

sZHvdvK.png

Notice that there is no Fieldtype "BaseFieldtypeRuntime" in this list as I mentioned above. You can then add your field to a template and edit any page of that template:

<?php namespace ProcessWire;
/**
 * Demo Fieldtype Extending the Boilerplate Runtime Fieldtype
 * 
 * @author Bernhard Baumrock, 03.10.2018
 * @license Licensed under MIT
 * @link https://www.baumrock.com
 */
class FieldtypeNow extends BaseFieldtypeRuntime {

  public static function getModuleInfo() {
    return [
      'title' => 'FieldtypeNow',
      'version' => '0.0.1',
      'summary' => 'Fieldtype showing the current time',
      'icon' => 'code',
      'requires' => ['BaseFieldtypeRuntime'],
    ];
  }

  public function render() {
    return time();
  }

}

Hk3fo71.png

Another Fieldtype rendering the content of a php file named like the field (very similar to the existing modules by @kongondo RuntimeMarkup, @Robin S RuntimeOnly and @kixe FieldtypeMarkup). You actually only have to implement the render() method, and if you need you can load scripts in the renderReady() method...

Spoiler

<?php namespace ProcessWire;
/**
 * Demo Fieldtype Extending the Boilerplate Runtime Fieldtype
 * 
 * @author Bernhard Baumrock, 03.10.2018
 * @license Licensed under MIT
 * @link https://www.baumrock.com
 */
class FieldtypeRenderFile extends BaseFieldtypeRuntime {

  public static function getModuleInfo() {
    return [
      'title' => 'Render File',
      'version' => '0.0.1',
      'summary' => 'Fieldtype rendering a php file placed in /site/templates/'. substr(strrchr(__CLASS__, "\\"), 1),
      'icon' => 'code',
      'requires' => ['BaseFieldtypeRuntime'],
    ];
  }

  /**
   * render content of included file
   */
  public function render() {
    $filename = "{$this->field}.{$this->page->template}.php";
    if(file_exists($this->path($filename))) return $this->renderFile($filename);

    $filename = "{$this->field}.php";
    if(file_exists($this->path($filename))) return $this->renderFile($filename);
    
    return;
  }

  /**
   * add scripts and styles named like this field
   */
  public function renderReady() {
    $this->loadAssets();
  }

  // ########## helper functions ##########

  /**
   * render file and pass variables
   */
  private function renderFile($filename) {
    return $this->wire->files->render($this->path($filename), [
      'page' => $this->page,
      'field' => $this->field,
      'inputfield' => $this,
    ]);
  }

  /**
   * add asset to scripts / styles
   */
  private function loadAssets() {
    
    $filename = "{$this->field}.{$this->page->template}.js";
    if($this->exists($filename)) $this->loadScript($filename);
    else {
      $filename = "{$this->field}.js";
      if($this->exists($filename)) $this->loadScript($filename);
    }
    
    $filename = "{$this->field}.{$this->page->template}.css";
    if($this->exists($filename)) $this->loadStyle($filename);
    else {
      $filename = "{$this->field}.css";
      if($this->exists($filename)) $this->loadStyle($filename);
    }
  }

  /**
   * load script
   */
  private function loadScript($filename) {
    $this->wire->config->scripts->add($this->url($filename));
  }

  /**
   * load style
   */
  private function loadStyle($filename) {
    $this->wire->config->styles->add($this->url($filename));
  }

  /**
   * does this file exist?
   */
  private function exists($filename) {
    return is_file($this->path($filename));
  }

  /**
   * return the assets path for php files
   */
  private function path($filename) {
    return $this->wire->config->paths->templates . $this->className . '/' . $filename;
  }
  
  /**
   * return the assets url for css and js files
   */
  private function url($filename) {
    return $this->wire->config->urls->templates . $this->className . '/' . $filename;
  }

}

 

0xa4IFR.png

This fieldtype loads files that are named like this:

  • site/templates/FieldtypeRenderFile/{fieldname}.{templatename}.[php/css/js]
  • site/templates/FieldtypeRenderFile/{fieldname}.[php/css/js]
  • Like 5

Share this post


Link to post
Share on other sites
5 hours ago, bernhard said:

But your runtimeonly field does actually have too few methods if you want to keep it completely out of the db. Your fieldtype creates an empty db table. Not sure if that is intended?

Interesting. It's based on FieldtypeFieldsetOpen, which likewise creates a database table (as does FieldtypeFieldsetClose). Not sure why these fieldtypes do that considering they don't store data, but it doesn't do any harm that I can see.

  • Like 1

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

  • Recently Browsing   0 members

    No registered users viewing this page.