Jump to content

How to create your very own custom Fieldtypes in ProcessWire


bernhard

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:

  Reveal hidden contents

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

See this module for an easy example of how to extend FieldtypeText:

 

  • Like 19
Link to comment
Share on other sites

  On 10/3/2018 at 3:55 PM, bernhard said:

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

Expand  

@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 4
  • Thanks 1
Link to comment
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:

  Reveal hidden contents

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...

  Reveal hidden contents

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 8
Link to comment
Share on other sites

  On 10/4/2018 at 4:12 PM, 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?

Expand  

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
Link to comment
Share on other sites

  • 4 months later...

Hm... Just got reminded about this post. Actually I think it's not the best idea to have a render() and renderReady() method in a Fieldtype module. That are methods that belong to an Inputfield module and I think it's not good to mix them up...

I'd be more than happy though to have a good and extensive documentation about developing fieldtypes for ProcessWire...

  • Like 2
Link to comment
Share on other sites

  • 4 months later...
  On 7/14/2019 at 9:03 AM, gunter said:

Which kind of magic is used so that the BaseFieldtypeRuntime does not appear in the fieldtype list?
I want extend my own base fieldtype, thats why I want to know this.

Expand  

To be honest: I don't know. But I'd be happt to know it if some knows it or finds out ? 

Link to comment
Share on other sites

  On 7/14/2019 at 9:34 AM, bernhard said:

To be honest: I don't know. But I'd be happt to know it if some knows it or finds out ? 

Expand  

Both the list on the Modules page and the fieldtype select on field edit screen expect fieldtype module names to start with "Fieldtype". BaseFieldtypeRuntime starts with "Base", so it's grouped under "Base" on the Modules page, and also not included in the fieldtypes (wire)array managed by the core (see /wire/core/Fieldtypes.php).

  • Like 2
  • Thanks 1
Link to comment
Share on other sites

  • 3 months later...

Memo to myself and mybe to safe someone else from losing hours trying to understand why a multi-language Inputfield does not render() as expected...

Learnings:

1) Inputfield::render() is only hookable for single-language Inputfields! When PW renders a multi-language Inputfield the LanguageSupport module adds a hook that fires after the original Inputfield::render(). This hook calls the render() method for each language and to avoid circular references it does that directly on $inputfield->___render() and not $inputfield->render(): https://github.com/processwire/processwire/blob/51629cdd5f381d3881133baf83e1bd2d9306f867/wire/modules/LanguageSupport/LanguageSupport.module#L445-L453

2) When building a custom Inputfield that supports a multi-language setup it is critical that its render() method is defined with 3 underscores! Otherwise the LanguageSupport hook that adds the markup for the other languages' fields would not fire.

  • Like 8
Link to comment
Share on other sites

  • 1 year later...

maybe a stupid question, I'm far from being an expert, but what's so exciting about avoiding the db in the first place? Aren't inputfields and inputfieldtypes not supposed to do nothing else than just that: insert data to the db? In other words, if you don't insert/input data in the db, why even call it inputfield?

I'm trying to create an actual inputfield (that inserts data in the db) that I would use within a module I'm building. No sure if I would handle this separately or all in one module. I'm trying to start with FieldTypeEvents but it's indeed quite complex. I never know if what ever bit of code I'm looking at concerns the admin interface or the frontend. Also don't know why there are two files: FieldTypeEvents.module and InputfieldEvents.module. That goes for other Fieldtypes as well, I guess all of them.

Link to comment
Share on other sites

You can think of it like this:

  • Fieldtype = responsible for storing/sanitizing data
  • Inputfield = responsible for UI/markup

That's why it is very simple to create an inputfield. A very basic inputfield could just output "hello world" without any connection to the DB or other business logic. It's not exciting to avoid the DB, but if its not necessary because you use the Inputfield just for presentation then you can simply save time/effort that is not needed ? On the other hand using Inputfields instead of plain HTML has the benefit that you get a UI component that every PW user is familiar with and that you can control like any other Inputfield (setting columnWidth, toggling, label, icon, ...). See https://processwire.com/blog/posts/building-custom-admin-pages-with-process-modules/#adding-your-first-field-inputfieldmarkup

The name INPUTfield can be a little confusing at the beginning, but after time it made a lot of sense for me and I could not think of a better wording now.

  • Like 4
Link to comment
Share on other sites

thanks @bernhard, it helps

I'm still wondering though, as I want to create a custom fieldtype inside a custom module, should one module do both? or should one module require/install the other, i.e. two separate .module.php files or one .module.php file? Also, if I use two .module.php files, could both files be in the same module folder or separate? In my understanding, two folders would mean two modules, but I might be wrong.

thanks for advice!

 

Link to comment
Share on other sites

/site/modules/MyModule/MyModule.module.php
/site/modules/MyModule/FieldtypeMyModule.module.php
/site/modules/MyModule/InputfieldMyModule.module.php

  Then add FieldtypeMyModule and InputfieldMyModule to the "installs" array of MyModule.module.php and MyModule to "required" array of the fieldtype and inputfield.

  • Like 2
Link to comment
Share on other sites

  On 10/3/2018 at 3:55 PM, bernhard said:

Building your very own custom Fieldtypes in ProcessWire seems to be hard, but it actually is not

Expand  

I disagree, it is actually that hard : D

All I'm trying to do is re-use the FieldTypeEvents/InputfieldEvents to build a sort of "week" inputfield, each table cell reprenting a individual checkboxes. So instead of 3 columns, it would have 7 (1 for each weekday) + 1 on the left. So I amended where those subfields are defined accordingly. However, now I get 

Call to a member function hasPermission() on null in wire/core/Modules.php:1389

Not sure if that tells anyone anything. Then I need to circle back to my db backup.

Also, maybe a bit confusing in this particular case: Is $event always arbitrary, I mean coicidentally the name of the variable? Because you also have $event when using hooks which you can't really change, so need to be clear on this if this is prone to being confused.

Link to comment
Share on other sites

Hi @fruid. I’m not going to get into this fully right now, just some suggestions.

Most importantly: What you really want to do is just buy FieldtypeTable. It’s dope, trust me.

  On 4/3/2021 at 1:07 PM, fruid said:

Call to a member function hasPermission() on null in wire/core/Modules.php:1389

Expand  

Secondly, do yourself a favor and install Tracy. I have no idea how that error came about, but the stack trace will help you figure it out. If you can’t get it to work, I suggest starting a separate thread with specific questions and code snippets and Tracy traces. Your project is kind of out of scope for Bernhard’s tutorial here ?

  On 4/3/2021 at 1:07 PM, fruid said:

I disagree, it is actually that hard : D

Expand  

Well, you’re trying to make a Multi-Field (extend FieldtypeMulti), which is a little more of an endeavor than a single value field.

Lastly, you can name a hook’s $event argument anything you want, if that is what you mean. $event is kind of a convention, but you might as well use $e or $hook or $args or whatever. You can even just leave it out if you don’t need it. Hooks work with callback functions. That is to say, it’s your own function. You name the function itself and its arguments.

  • Like 3
Link to comment
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.
×
×
  • Create New...