ProcessWire - Fieldtype erstellen (Module)

Aus Wikizone
Wechseln zu: Navigation, Suche
ProcessWire - Module schreiben
https://processwire.com/talk/topic/26407-fieldtype-modules/ Tutorial von 2021
https://processwire.com/talk/topic/20082-how-to-create-your-very-own-custom-fieldtypes-in-processwire/
https://processwire.com/api/ref/fieldtype/ Referenz für fieldtypes
https://processwire.com/talk/topic/2365-validation-for-own-inputfieldfieldtype-modules/

Fieldtypes und Inputfields[Bearbeiten]

Module für Eingabefelder im Admin Bereich sowie deren Frontendausgabe bestehen i.d.R. aus mehreren Modulen. Einem das die Klasse Fieldtype erweitert und eines das Inputfield erweitert.

Fieldtype definiert das Feld enthält das Datenhandling . Es ist zuständig für die Definition der Felder für die Datenbank. Inputfield, das ist für das Userformular und die Eingaben im Backend zuständig. Für das Rendering im Frontend benötigt man weitere Funktionen.

Fieldtype[Bearbeiten]

Die meisten Fieldtype Module implementieren nur wenige Methoden wie Fieldtype::sanitizeValue() (Pflichtfunktion) und Fieldtype::getDatabaseSchema(). Die Basisklasse enthält Methoden die oft von Page oder Field-Objekten aufgerufen werden.


Datenbankspeicherung[Bearbeiten]

Fieldtype::getDatabaseSchema()

Diese Funktion nutzt PW um die Datenbank anzulegen und Infos zu den Daten die im Feld gespeichert sind zu erhalten. Ist sie vorhanden werden die Datenbanktabllen automatisch angelegt, wenn in einem Template dieser Fieldtype genutzt wird. So sollte die Funktion aufgebaut sein:

  • Sollte ein Array wie im Beispiel zurückgeben mit einem Index auf dem Feldname und mit type Details als Wert (analog zu einem SQL Statement)
  • Indizes werden über das keys array durchgereicht. Beachte: Das Feld pages_id kann man in der Praxis mitsamt dem primary key holen, indem man vom parent Objekt über getDatabaseSchema($field) das Schema holt. (siehe Beispiel 2).
  • Als Minimum muss jedes Fieldtype ein data Feld und einen Index dafür besitzen.
  • Wenn ein PHP NULL Wert in der Datenbank als NULL gespeichert werden soll muss für das Feld DEFAULT NULL spezifiziert werden.


Beispiel

array(
 'data' => 'mediumtext NOT NULL',
 'keys' => array(
   'primary' => 'PRIMARY KEY (`pages_id`)',
   'FULLTEXT KEY data (data)',
 ),
 'xtra' => array(
   // optional extras, MySQL defaults will be used if omitted
   'append' =>
     'ENGINE={$this->config->dbEngine} ' .
     'DEFAULT CHARSET={$this->config->dbCharset}'

   // true (default) if this schema provides all storage for this fieldtype.
   // false if other storage is involved with this fieldtype, beyond this schema
   // (like repeaters, PageTable, etc.)
   'all' => true,
 )
);

Beispiel 2

public function getDatabaseSchema(Field $field) {
    $schema = parent::getDatabaseSchema($field);
    $schema['data'] = 'text NOT NULL';
    $schema['keys']['data'] = 'FULLTEXT KEY `data` (`data`)'; 
    return $schema;
  }

Usage

$array = $fieldtype->getDatabaseSchema(Field $field);

In der Datenbank wird ein Eintrag in der Tabelle fields gemacht sobald ein Fieldtype Modul installiert ist. Sobald ein Feld mit dem Feldtyp erstellt wird, landet eine zusätzliche Tabelle in der Datenbank

Tabelle: field_meinFeldName
pages_id|data
  • Wird das Feld aus dem Template gelöscht werden zugehörigen Einträge in der Tabelle gelöscht.
  • Löscht man das Feld wird die Tabelle gelöscht.
  • Deinstalliert man das Modul wird der Eintrag aus der Tabelle fields gelöscht.

Verbindung zu einem Inputfield[Bearbeiten]

Damit der Benutzer das Feld in einem Template nutzen und Daten eingeben kann benötigt man ein Inputfield. Man erstellt es wie unten gezeigt und holt sich das Inputfield mit der getInputfield() Methode.


**
   * Return the associated Inputfield
   * 
   * @param Page $page
   * @param Field $field
   * @return Inputfield
   *
   */
  public function getInputfield(Page $page, Field $field) {
    $inputField = $this->wire('modules')->get('InputfieldDemo'); 
    return $inputField; 
  }

Konfiguration im Details Tab[Bearbeiten]

Wenn ein Feld mit dem neuen Feldtyp angelegt wird kann man Konfigurationsmöglichkeiten im Details Tab unterbringen oder etwas ausgeben.

/**
   * Return the fields required to configure an instance of FieldtypeDemo
   * 
   * @param Field $field
   * @return InputfieldWrapper
   *
   */
  public function ___getConfigInputfields(Field $field) {
    $inputfields = parent::___getConfigInputfields($field);
    
    // custom config inputfields here
    $inputfields->add([
      'type' => 'markup',
      'value' => 'Goes to Details Tab: '.__FILE__ . ":" . __LINE__,
    ]);

    return $inputfields; 
  }

Inputfield[Bearbeiten]

https://processwire.com/api/ref/inputfield/
  • Basisklasse für Processwire Inputfield Module
  • Immer wenn man in ProcessWire etwas eingeben kann ist typischerweise ein Inputfield im Spiel.
  • In ProcessWire Feldern (Datenbankfelder) sind sie typischerweise mit einem Fieldtype assoziiert (s.o.).
  • Inputfields können oft auch ohne Fieldtype genutzt werden.
  • Inputfields unterstützen Hierarchien. Es können also Inputfelder in Inputfeldern existieren. In der Regel sind die Elternfelder dann vom Typ InputfieldWrapper (z.B. fieldsets)

Ein neues Inputfield erbt z.B. von der Klasse Inputfield schon die wichtigsten Eigenschaften.

class InputfieldDemo extends Inputfield {...}

Es kann damit schon ein Eingabefeld im Backend bereitstellen und man kann z.B. schon Basiskonfigurationen wie die Breite im Template einstellen.

TODO

Feld im Backend anzeigen[Bearbeiten]

Für die Ausgabe des Eingabefelds im Backend (Feld für Editor auf der Seite) gibt es bereits eine vordefinierte Klasse ___render() die man Überschreibt

/**
   * Render Inputfield
   * 
   * @return string
   * 
   */
  public function ___render() {
    $out = '<input name="demo" value="' . $this->value . '" type="text">'; 
    return $out; 
  }

Konfiguration im Input Tab[Bearbeiten]

Viele Eigenschaften (z.B. Breite im Backend) kommt schon über das Parent Objekt mit (Visibility, Column...). Mit der Funktion ___getConfigInputfields kann man neue Konfigurationsmöglichkeiten implementieren die dann im Input Tab im Backend auftauchen.

  /**
   * Get field configuration
   * 
   * @return InputfieldWrapper
   * @throws WireException
   * 
   */
  public function ___getConfigInputfields() {
    $inputfields = parent::___getConfigInputfields(); 

    // custom config inputfields here
    $inputfields->add([
      'type' => 'markup',
      'value' => __FILE__ . ":" . __LINE__,// gibt den Ort der Datei und die aktuelle Zeile aus.
    ]);

    return $inputfields; 
  }

Beispiel 2 (aus dem Quellcode von Inputfield.php)

// Example getConfigInputfields() implementation
public function ___getConfigInputfields() {
  // Get the defaults and $inputfields wrapper we can add to
  $inputfields = parent::___getConfigInputfields();
  // Add a new Inputfield to it
  $f = $this->wire('modules')->get('InputfieldText'); 
  $f->attr('name', 'first_name');
  $f->attr('value', $this->get('first_name')); 
  $f->label = 'Your First Name';
  $inputfields->add($f); 
  return $inputfields; 
}

Erzeugt ein Eingabefeld im Input Tab (damit es sinnvoll ist sollte es aber auch gespeichert werden ;-) )

Frontendausgabe[Bearbeiten]

Meistens möchte man noch eine angepasste Frontendausgabe. Dazu definiert man z.B. jeweils Objektklassen. Die das jeweilige Objekt (z.B. Events) repräsentieren. Hier kann man render Funktionen definieren. Man kann das Ganze natürlich auch größer anlegen und z.B. MVC Strukturen nehmen. Macht es aber in vielen Fällen auch zu umständlich.

  • FieldtypeEvents.module - Einbinden der Objekte (z.B. Events, EventArray) ,Tabellendefinition, getDatabaseSchema, sanitizer (Pflicht) ...
  • InputfieldEvents.module - Backend-Darstellung
  • Objekte die die Daten repräsentieren und Ausgeben

Demo Fieldtype mit Datenbankfeld[Bearbeiten]

FieldtypeDemo.module.php - see Fieldtype Baseclass for more details.

<?php namespace ProcessWire;

class FieldtypeDemo extends Fieldtype {
  public static function getModuleInfo() {
    return array(
      'title' => 'Demo',
      'version' => '0.0.1',
      'summary' => 'Demo of how to develop a PW Fieldtype',
      'installs' => 'InputfieldDemo',
    );
  }

  /**
   * Sanitize value for storage
   * 
   * @param Page $page
   * @param Field $field
   * @param string $value
   * @return string
   *
   */
  public function sanitizeValue(Page $page, Field $field, $value) {
    $this->message("Sanitizing value '$value'");
    return $value; 
  }

  /**
   * Format value for output (optional function)
   * this overrides the function from parent class ___formatValue 
   * is called automatically when page->of() is enabled
   * 
   * @param Page $page
   * @param Field $field
   * @param string $value
   * @return string
   *
   */
  public function ___formatValue(Page $page, Field $field, $value) {
    $value = (string) $value;
    // format value here
    return "Formatted value: $value";
  }

  /**
   * Return the associated Inputfield
   * 
   * @param Page $page
   * @param Field $field
   * @return Inputfield
   *
   */
  public function getInputfield(Page $page, Field $field) {
    $inputField = $this->wire('modules')->get('InputfieldDemo'); 
    return $inputField; 
  }

  // /**
  //  * Update a query to match the text with a fulltext index
  //  * 
  //  * @param DatabaseQuerySelect $query
  //  * @param string $table
  //  * @param string $subfield
  //  * @param string $operator
  //  * @param int|string $value
  //  * @return DatabaseQuerySelect
  //  *
  //  */
  // public function getMatchQuery($query, $table, $subfield, $operator, $value) {
  //   /** @var DatabaseQuerySelectFulltext $ft */
  //   $ft = $this->wire(new DatabaseQuerySelectFulltext($query)); 
  //   $ft->match($table, $subfield, $operator, $value); 
  //   return $query; 
  // }

  /**
   * Return the database schema in specified format
   * 
   * @param Field $field
   * @return array
   *
   */
  public function getDatabaseSchema(Field $field) {
    $schema = parent::getDatabaseSchema($field);
    $schema['data'] = 'text NOT NULL';
    $schema['keys']['data'] = 'FULLTEXT KEY `data` (`data`)'; 
    return $schema;
  }

  /**
   * Return the fields required to configure an instance of FieldtypeDemo
   * 
   * @param Field $field
   * @return InputfieldWrapper
   *
   */
  public function ___getConfigInputfields(Field $field) {
    $inputfields = parent::___getConfigInputfields($field);
    
    // custom config inputfields here
    $inputfields->add([
      'type' => 'markup',
      'value' => __FILE__ . ":" . __LINE__,
    ]);

    return $inputfields; 
  }
}

InputfieldDemo.module.php

<?php namespace ProcessWire;
class InputfieldDemo extends Inputfield {

  public static function getModuleInfo() {
    return array(
      'title' => 'Demo', // Module Title
      'summary' => 'Inputfield of FieldtypeDemo', // Module Summary
      'version' => '0.0.1',
      'requires' => 'FieldtypeDemo',
      );
  }

  // /**
  //  * Render ready
  //  * 
  //  * @param Inputfield $parent
  //  * @param bool $renderValueMode
  //  * @return bool
  //  * @throws WireException
  //  * 
  //  */
  // public function renderReady(Inputfield $parent = null, $renderValueMode = false) {
  //   // load assets here
  //   return parent::renderReady($parent, $renderValueMode);
  // }

  /**
   * Render Inputfield
   * 
   * @return string
   * 
   */
  public function ___render() {
    $out = '<input name="demo" value="' . $this->value . '" type="text">'; 
    return $out; 
  }

  // /**
  //  * Process input
  //  * 
  //  * @param WireInputData $input
  //  * @return $this
  //  * 
  //  */
  // public function ___processInput(WireInputData $input) {
  //   parent::___processInput($input);
  //   $this->message("Processing input of demo field: Value = " . $input->demo);
  //   return $this;
  // }

  /**
   * Get field configuration
   * 
   * @return InputfieldWrapper
   * @throws WireException
   * 
   */
  public function ___getConfigInputfields() {
    $inputfields = parent::___getConfigInputfields();

    // custom config inputfields here
    $inputfields->add([
      'type' => 'markup',
      'value' => __FILE__ . ":" . __LINE__,
    ]);

    return $inputfields; 
  }
}

Boilerplate von Bernhard Baumrock[Bearbeiten]

Ist gedacht als Klasse ausgehend von der man Fieldtypes startet. Im Moment kein Beispiel mit Datenbankzugriff.

https://processwire.com/talk/topic/20082-how-to-create-your-very-own-custom-fieldtypes-in-processwire/

Nutzt den Tracy Debugger zum besseren Testen (und Verstehen).

Basisklasse[Bearbeiten]

<?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");
  }

}

Beispiel Implementierung[Bearbeiten]

<?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;
  }

}

Valididate / Sanitize[Bearbeiten]

https://processwire.com/talk/topic/2365-validation-for-own-inputfieldfieldtype-modules/
https://processwire.com/talk/topic/969-input-validation-in-back-end-fields/
https://processwire.com/talk/topic/4476-validating-field-before-page-save/

Validierung kann bedeuten Validieren von Werten in der Konfiguration und Validieren von Werten wenn der User den Fieldtype in einem Template einsetzt. ProcessWire hat den sanitizer den man mit eigenen Methoden erweitern kann.

Die Funktion sanitizeValue() die Pflicht für jeden Fieldtype ist wird bei jedem Seitenaufruf ausgeführt. Daher ist sie auf Schnelligkeit getrimmt und einfach gehalten.

Mehr Möglichkeiten hat man über einen Hook in Inputfield::processInput.

Validierung über ProcessInput Hook[Bearbeiten]

public function init() {
       $this->addHookAfter("Inputfield::processInput",$this,"validateField");
   }
   public function validateField(HookEvent $event){
       $inputfield = $event->object;
       $name = $inputfield->name;
       if($name == "sidebar"){
           if(strlen($inputfield->attr("value")) > 10){
               $inputfield->error($this->_("Too long"));
           }
       }
   }

Beispiel 2

public function init(){
    $this->addHookAfter("InputfieldTextarea::processInput", $this, "validateText");
}

public function validateText($event){
    $field = $event->object;
    
    if($field->name == "body"){
        $page = $this->modules->ProcessPageEdit->getPage();
        $oldValue = $page->get("$field->name");
        $newValue = $field->value;
        echo "old value: " . $oldValue;
        echo "new value: " . $newValue;
        if($newValue !== $oldValue){
            $field->value = $oldValue;
            $field->error("Go away!");
        }
    }
}

getAttributes()[Bearbeiten]

Todo

https://github.com/ryancramerdesign/ProcessWire/blob/dev/wire/modules/Inputfield/InputfieldText.module#L71
https://github.com/ryancramerdesign/ProcessWire/blob/dev/wire/modules/Inputfield/InputfieldText.module#L50

Beispiele[Bearbeiten]

Fieldtype Events[Bearbeiten]

Beispielmodul von Ryan Cramer

https://modules.processwire.com/modules/fieldtype-events/

FieldtypePhone[Bearbeiten]

InputfieldPhone.module

<?php

/**
 * ProcessWire Phone Inputfieldtype
 * by Adrian Jones with code from "Soma" Philipp Urlich's Dimensions Fieldtype module and Ryan's core FieldtypeDatetime module
 *
 * ProcessWire 3.x
 * Copyright (C) 2010 by Ryan Cramer
 * Licensed under GNU/GPL v2, see LICENSE.TXT
 *
 * http://www.processwire.com
 * http://www.ryancramer.com
 *
 */

class InputfieldPhone extends Inputfield {

    public static function getModuleInfo() {
        return array(
            'title' => __('Phone Inputfield', __FILE__),
            'summary' => __('Multi part phone field, with custom output formatting options.', __FILE__),
            'version' => '3.1.0',
            'author' => 'Adrian Jones',
            'href' => 'http://modules.processwire.com/modules/fieldtype-phone/',
            'icon' => 'phone',
            'requires' => array("FieldtypePhone")
       );
    }

   /**
     * Default configuration for module
     *
     */
    static public function getDefaultData() {
        return array(
            "hide_input_labels" => 0,
            "placeholder_input_labels" => 0,
            "input_class" => '',
            "country_input" => 0,
            "country_input_label" => 'Ctry',
            "country_input_width" => 60,
            "area_code_input" => 0,
            "area_code_input_label" => 'Area',
            "area_code_input_width" => 80,
            "number_input_label" => 'Num',
            "number_input_width" => 140,
            "extension_input" => 0,
            "extension_input_label" => 'Ext',
            "extension_input_width" => 80,
            "output_format_override_input" => 0,
            "allow_letters_input" => 0
        );
    }

    /**
     * Construct the Inputfield, setting defaults for all properties
     *
     */
    public function __construct() {
        $this->fieldtypePhone = $this->wire('modules')->get('FieldtypePhone')->getArray();
        $this->set('output_format',$this->fieldtypePhone["output_format"]);
        $this->set('output_format_options',$this->fieldtypePhone["output_format_options"]);
        foreach(self::getDefaultData() as $key => $value) {
            $this->$key = $value;
        }
        parent::__construct();
    }

    /**
     * Per the Module interface, init() is called when the system is ready for API usage
     *
     */
    public function init() {
        return parent::init();
    }

    /**
     * Return the completed output of this Inputfield, ready for insertion in an XHTML form
     *
     * @return string
     *
     */
    public function ___render() {

        if($this->wire('languages')) {
            $language = $this->wire('user')->language;
            $lang = !$language->isDefault() ? $language : '';
        }
        else {
            $lang = '';
        }

        $out = '';

        $value = $this->attr('value') ? $this->attr('value') : new \Phone();

        $pattern = $this->allow_letters_input ? '[0-9A-Za-z]*' : '[0-9]*';

        if($this->country_input) {
            $out .= "<div class='phone_col'>";
            $out .= "<label>".($this->hide_input_labels ? '' : $this->{"country_input_label$lang"} . ' ')."<input type='text' pattern='".$pattern."' class='".$this->input_class."' ".($this->placeholder_input_labels ? ' placeholder="'.$this->{"country_input_label$lang"}.'"' : '') . ($this->country_input_width !== 0 ? ' style="width:'.$this->country_input_width.'px"' : '')." name='{$this->name}_country' id='Inputfield_{$this->name}_country' value='{$value->country}'/></label>";
            $out .= "</div>";
        }

        if($this->area_code_input) {
            $out .= "<div class='phone_col'>";
            $out .= "<label>".($this->hide_input_labels ? '' : $this->{"area_code_input_label$lang"} . ' ')."<input type='text' pattern='".$pattern."' class='".$this->input_class."' ".($this->placeholder_input_labels ? ' placeholder="'.$this->{"area_code_input_label$lang"}.'"' : '') . ($this->area_code_input_width !== 0 ? ' style="width:'.$this->area_code_input_width.'px"' : '')." name='{$this->name}_area_code' id='Inputfield_{$this->name}_area_code' value='{$value->area_code}'/></label>";
            $out .= "</div>";
        }

        $out .= "<div class='phone_col'>";
        $out .= "<label>".($this->hide_input_labels ? '' : $this->{"number_input_label$lang"} . ' ')."<input type='text' pattern='".$pattern."' class='".$this->input_class."' ".($this->placeholder_input_labels ? ' placeholder="'.$this->{"number_input_label$lang"}.'"' : '') . ($this->number_input_width !== 0 ? ' style="width:'.$this->number_input_width.'px"' : '')."  name='{$this->name}_number' id='Inputfield_{$this->name}_number' value='{$value->number}'/></label>";
        $out .= "</div>";

        if($this->extension_input) {
            $out .= "<div class='phone_col'>";
            $out .= "<label>".($this->hide_input_labels ? '' : $this->{"extension_input_label$lang"} . ' ')."<input type='text' pattern='".$pattern."' class='".$this->input_class."' ".($this->placeholder_input_labels ? ' placeholder="'.$this->{"extension_input_label$lang"}.'"' : '') . ($this->extension_input_width !== 0 ? ' style="width:'.$this->extension_input_width.'px"' : '')."  name='{$this->name}_extension' id='Inputfield_{$this->name}_extension' value='{$value->extension}'/></label>";
            $out .= "</div>";
        }

        if($this->output_format_override_input) {
            $out .= "<div class='phone_col'>";
            $out .= "<label>".($this->hide_input_labels ? '' : "{$this->_('Format')} ")."<select name='{$this->name}_output_format' id='Inputfield_{$this->name}_output_format'>";
            $out .= '<option value="" ' . ($this->output_format == '' ? 'selected' : '') . '>No Override</option>';
            $this->fieldtypePhone = $this->wire('modules')->get('FieldtypePhone');
            foreach($this->fieldtypePhone->buildOptions(explode("\n",$this->output_format_options), $this->data) as $option) {
                $out .= '<option value="'.$option[0].'" ' . (($option[0] == $value->output_format) ? 'selected' : '') . '>'.$option[1].'</option>';
            }
            $out .= "</select></label>";
            $out .= "</div>";
        }

        $out .= '<div style="clear:both; height:0">&nbsp;</div>';

        return $out;
    }

    /**
     * Process the input from the given WireInputData (usually $input->get or $input->post), load and clean the value for use in this Inputfield.
     *
     * @param WireInputData $input
     * @return $this
     *
     */
    public function ___processInput(WireInputData $input) {

        $this->fieldtypePhone = $this->wire('modules')->get('FieldtypePhone');

        $name = $this->attr('name');
        $value = $this->attr('value');

        if(is_null($value)) $value = new \Phone;

        $pn_names = array(
            'country' => $name . "_country",
            'area_code' => $name . "_area_code",
            'number' => $name . "_number",
            'extension' => $name . "_extension",
            'output_format' => $name . "_output_format"
       );

        // loop all inputs and set them if changed
        foreach($pn_names as $key => $name) {
            if(isset($input->$name)) {
                if($value->$key !== $input->$name) {
                    if(!$this->allow_letters_input && !is_numeric($input->$name) && !empty($input->$name) && $key != 'output_format') {
                        // in case the input isn't numeric show an error
                        $this->wire()->error($this->_("Field only accepts numeric values"));
                    }
                    elseif($key == 'output_format' || $this->allow_letters_input) {
                        $value->set($key, $this->wire('sanitizer')->text($input->$name));
                    }
                    else {
                        $value->set($key, $this->wire('sanitizer')->digits($input->$name));
                    }
                }
            }
        }

        if($value != $this->attr('value')) {
            $this->trackChange('value');
            // sets formatted value which is needed for Form Builder entries table
            $this->setAttribute('value', $this->fieldtypePhone->formatPhone($value->country, $value->area_code, $value->number, $value->extension, $this->fieldtypePhone->getFormatFromName($value->output_format ?: $this->output_format)));
        }
        return $this;
    }

    /**
     * Get any custom configuration fields for this Inputfield
     *
     * @return InputfieldWrapper
     *
     */
    public function ___getConfigInputfields() {

        $inputfields = parent::___getConfigInputfields();
        $this->fieldtypePhone = $this->wire('modules')->get('FieldtypePhone');
        $value = $this->hasField ?: $this;

        $f = $this->wire('modules')->get('InputfieldCheckbox');
        $f->attr('name', 'hide_input_labels');
        $f->label = __('Hide input labels', __FILE__);
        $f->description = __('Check to hide the component input labels', __FILE__);
        $f->columnWidth = 33;
        $f->attr('checked', $value->hide_input_labels ? 'checked' : '');
        $inputfields->append($f);

        $f = $this->wire('modules')->get('InputfieldCheckbox');
        $f->attr('name', 'placeholder_input_labels');
        $f->label = __('Placeholder input labels', __FILE__);
        $f->description = __('Check to show the component input labels as the placeholder', __FILE__);
        $f->columnWidth = 34;
        $f->attr('checked', $value->placeholder_input_labels ? 'checked' : '');
        $inputfields->append($f);

        $f = $this->wire('modules')->get('InputfieldText');
        $f->attr('name', 'input_class');
        $f->label = __('Input Class', __FILE__);
        $f->description = __('Class to add to component inputs.', __FILE__);
        $f->notes = __('eg. uk-input', __FILE__);
        $f->columnWidth = 33;
        $f->value = $value->input_class;
        $inputfields->append($f);

        // country
        $f = $this->wire('modules')->get('InputfieldCheckbox');
        $f->attr('name', 'country_input');
        $f->label = __('Country Code', __FILE__);
        $f->attr('checked', $value->country_input ? 'checked' : '');
        $f->description = __('Whether to ask for country code when entering phone numbers.', __FILE__);
        $f->columnWidth = 33;
        $inputfields->append($f);

        $f = $this->wire('modules')->get('InputfieldText');
        $f->attr('name', 'country_input_label');
        $f->label = __('Country input label', __FILE__);
        $f->attr('size', 100);
        $f->description = __('Name of Country input', __FILE__);
        $f->notes = __('Default: Ctry', __FILE__);
        $f->columnWidth = 34;
        $f->value = $value->country_input_label;
        if($this->wire('languages')) {
            $f->useLanguages = true;
            foreach($this->wire('languages') as $language) {
                if(!$language->isDefault() && isset($value->data["country_input_label$language"])) $f->set("value$language", $value->data["country_input_label$language"]);
            }
        }
        $inputfields->append($f);

        $f = $this->wire('modules')->get('InputfieldText');
        $f->attr('name', 'country_input_width');
        $f->label = __('Country input width', __FILE__);
        $f->attr('size', 10);
        $f->description = __('Width of the input in pixels.', __FILE__);
        $f->notes = __('Default: 60; 0 to not set width', __FILE__);
        $f->columnWidth = 33;
        $f->value = $value->country_input_width;
        $inputfields->append($f);


        // area code
        $f = $this->wire('modules')->get('InputfieldCheckbox');
        $f->attr('name', 'area_code_input');
        $f->label = __('Area Code', __FILE__);
        $f->description = __('Whether to ask for area code when entering phone numbers.', __FILE__);
        $f->notes = __('If this is unchecked, then area code and number will be store as one in the number field.', __FILE__);
        $f->columnWidth = 33;
        $f->attr('checked', $value->area_code_input ? 'checked' : '');
        $inputfields->append($f);

        $f = $this->wire('modules')->get('InputfieldText');
        $f->attr('name', 'area_code_input_label');
        $f->label = __('Area Code input name', __FILE__);
        $f->attr('size', 100);
        $f->description = __('Name of Area Code input', __FILE__);
        $f->notes = __('Default: Area', __FILE__);
        $f->columnWidth = 34;
        $f->value = $value->area_code_input_label;
        if($this->wire('languages')) {
            $f->useLanguages = true;
            foreach($this->wire('languages') as $language) {
                if(!$language->isDefault() && isset($value->data["area_code_input_label$language"])) $f->set("value$language", $value->data["area_code_input_label$language"]);
            }
        }
        $inputfields->append($f);

        $f = $this->wire('modules')->get('InputfieldText');
        $f->attr('name', 'area_code_input_width');
        $f->label = __('Area code input width', __FILE__);
        $f->attr('size', 10);
        $f->description = __('Width of the input in pixels.', __FILE__);
        $f->notes = __('Default: 80; 0 to not set width', __FILE__);
        $f->columnWidth = 33;
        $f->value = $value->area_code_input_width;
        $inputfields->append($f);

        // number
        $f = $this->wire('modules')->get('InputfieldText');
        $f->attr('name', 'number_input_label');
        $f->label = __('Number input name', __FILE__);
        $f->attr('size', 100);
        $f->description = __('Name of Number input', __FILE__);
        $f->notes = __('Default: Num', __FILE__);
        $f->columnWidth = 50;
        $f->value = $value->number_input_label;
        if($this->wire('languages')) {
            $f->useLanguages = true;
            foreach($this->wire('languages') as $language) {
                if(!$language->isDefault() && isset($value->data["number_input_label$language"])) $f->set("value$language", $value->data["number_input_label$language"]);
            }
        }
        $inputfields->append($f);

        $f = $this->wire('modules')->get('InputfieldText');
        $f->attr('name', 'number_input_width');
        $f->label = __('Number input width', __FILE__);
        $f->attr('size', 10);
        $f->description = __('Width of the input in pixels.', __FILE__);
        $f->notes = __('Default: 140; 0 to not set width', __FILE__);
        $f->columnWidth = 50;
        $f->value = $value->number_input_width;
        $inputfields->append($f);

        // extension
        $f = $this->wire('modules')->get('InputfieldCheckbox');
        $f->attr('name', 'extension_input');
        $f->label = __('Extension', __FILE__);
        $f->description = __('Whether to ask for extension when entering phone numbers.', __FILE__);
        $f->columnWidth = 33;
        $f->attr('checked', $value->extension_input ? 'checked' : '');
        $inputfields->append($f);

        $f = $this->wire('modules')->get('InputfieldText');
        $f->attr('name', 'extension_input_label');
        $f->label = __('Extension input name', __FILE__);
        $f->attr('size', 100);
        $f->description = __('Name of Extension input', __FILE__);
        $f->notes = __('Default: Ext', __FILE__);
        $f->columnWidth = 34;
        $f->value = $value->extension_input_label;
        if($this->wire('languages')) {
            $f->useLanguages = true;
            foreach($this->wire('languages') as $language) {
                if(!$language->isDefault() && isset($value->data["extension_input_label$language"])) $f->set("value$language", $value->data["extension_input_label$language"]);
            }
        }
        $inputfields->append($f);

        $f = $this->wire('modules')->get('InputfieldText');
        $f->attr('name', 'extension_input_width');
        $f->label = __('Extension input width', __FILE__);
        $f->attr('size', 10);
        $f->description = __('Width of the input in pixels.', __FILE__);
        $f->notes = __('Default: 80; 0 to not set width', __FILE__);
        $f->columnWidth = 33;
        $f->value = $value->extension_input_width;
        $inputfields->append($f);

        $f = $this->wire('modules')->get('InputfieldSelect');
        $f->attr('name', 'output_format');
        $f->label = __('Phone Output Format', __FILE__);
        $f->description = __("Select the format to be used when outputting phone numbers for this field.\n\nYou can define new formats for this dropdown select in the phone fieldtype module config settings.", __FILE__);
        $f->columnWidth = 66;
        $f->addOption('', __('None', __FILE__));
        foreach($this->fieldtypePhone->buildOptions(explode("\n", $this->fieldtypePhone->output_format_options), $this->data) as $option) {
            $f->addOption($option[0], $option[1]);
            if($value->output_format == $option[0]) $f->attr('value', $option[0]);
        }
        $inputfields->append($f);

        $f = $this->wire('modules')->get('InputfieldCheckbox');
        $f->attr('name', 'allow_letters_input');
        $f->label = __('Allow Letters in Input', __FILE__);
        $f->description = __('Whether to allow letters when entering phone numbers.', __FILE__);
        $f->notes = __('Some businesses use letters to make it easier to remember a number.', __FILE__);
        $f->columnWidth = 34;
        $f->attr('checked', $value->allow_letters_input ? 'checked' : '');
        $inputfields->append($f);

        $f = $this->wire('modules')->get('InputfieldCheckbox');
        $f->attr('name', 'output_format_override_input');
        $f->label = __('Output Format Override', __FILE__);
        $f->description = __('Whether to give option to override selected output format when entering phone numbers.', __FILE__);
        $f->attr('checked', $value->output_format_override_input ? 'checked' : '');
        $inputfields->append($f);

        return $inputfields;
    }

}

FieldtypePhone.module

<?php

/**
 * ProcessWire Phone Fieldtype
 * by Adrian Jones with code from "Soma" Philipp Urlich's Dimensions Fieldtype module and Ryan's core FieldtypeDatetime module
 *
 * Field that stores 4 numeric values for country/area code/number/extension and allows for multiple formatting options.
 *
 * ProcessWire 3.x
 * Copyright (C) 2010 by Ryan Cramer
 * Licensed under GNU/GPL v2, see LICENSE.TXT
 *
 * http://www.processwire.com
 * http://www.ryancramer.com
 *
 */

class FieldtypePhone extends Fieldtype implements Module, ConfigurableModule {


    public static function getModuleInfo() {
        return array(
            'title' => __('Phone', __FILE__),
            'summary' => __('Multi part phone field, with custom output formatting options.', __FILE__),
            'version' => '3.1.0',
            'author' => 'Adrian Jones',
            'href' => 'http://modules.processwire.com/modules/fieldtype-phone/',
            'installs' => 'InputfieldPhone',
            'requiredBy' => 'InputfieldPhone',
            'icon' => 'phone'
       );
    }

   /**
     * Default configuration for module
     *
     */
    static public function getDefaultData() {
        return array(
            "output_format" => "",
            "output_format_options" => '

/*North America without separate area code*/
northAmericaStandardNoSeparateAreaCode | {+[phoneCountry]} {([phoneNumber,0,3])} {[phoneNumber,3,3]}-{[phoneNumber,6,4]} {x[phoneExtension]} | 1,,2215673456,123
northAmericaStandardNoSeparateAreaCodeNoNumberDashes | {+[phoneCountry]} {([phoneNumber,0,3])} {[phoneNumber,3,7]} {x[phoneExtension]} | 1,,2215673456,123
northAmericaStandardNoSeparateAreaAllDashes | {+[phoneCountry]}-{[phoneNumber,0,3]}-{[phoneNumber,3,3]}-{[phoneNumber,6,4]} {x[phoneExtension]} | 1,,2215673456,123
northAmericaStandardNoSeparateAreaDashesNoNumberDashes | {+[phoneCountry]}-{[phoneNumber]} {x[phoneExtension]} | 1,,2215673456,123

/*North America with separate area code*/
northAmericaStandard | {+[phoneCountry]} {([phoneAreaCode])} {[phoneNumber,0,3]}-{[phoneNumber,3,4]} {x[phoneExtension]} | 1,221,5673456,123
northAmericaNoNumberDashes | {+[phoneCountry]} {([phoneAreaCode])} {[phoneNumber]} {x[phoneExtension]} | 1,221,5673456,123
northAmericaAllDashes| {+[phoneCountry]}-{[phoneAreaCode]}-{[phoneNumber,0,3]}-{[phoneNumber,3,4]} {x[phoneExtension]} | 1,221,5673456,123
northAmericaDashesNoNumberDashes | {+[phoneCountry]}-{[phoneAreaCode]}-{[phoneNumber]} {x[phoneExtension]} | 1,221,5673456,123

/*Australia*/
australiaNoCountryAreaCodeLeadingZero | {([phoneAreaCode,0,2])} {[phoneNumber,0,4]} {[phoneNumber,4,4]} {x[phoneExtension]} | 61,07,45673456,123
australiaWithCountryAreaCodeNoLeadingZero | {+[phoneCountry]} {([phoneAreaCode,1,1])} {[phoneNumber,0,4]} {[phoneNumber,4,4]} {x[phoneExtension]} | 61,07,45673456,123
'
        );
    }

    /**
     * Data as used by the get/set functions
     *
     */
    protected $data = array();

    /**
     * Populate the default config data
     *
     */
    public static $_data;

    public function __construct() {
        foreach(self::getDefaultData() as $key => $value) {
            $this->$key = $value;
        }
    }

    /**
     * Format the value for output, according to selected format and language
     *
     */
    public function ___formatValue(Page $page, Field $field, $value) {

        $outputCode = $this->getOutputFormat($value, $field);

        $value->formattedNumber = $this->formatPhone($value->country, $value->area_code, $value->number, $value->extension, $outputCode);
        $value->formattedNumberNoCtryNoExt = $this->formatPhone(null, $value->area_code, $value->number, null, $outputCode);
        $value->formattedNumberNoCtry = $this->formatPhone(null, $value->area_code, $value->number, $value->extension, $outputCode);
        $value->formattedNumberNoExt = $this->formatPhone($value->country, $value->area_code, $value->number, null, $outputCode);

        $value->unformattedNumberNoCtryNoExt = ($value->area_code ? $value->area_code : null) . ($value->number ? $value->number : null);
        $value->unformattedNumberNoCtry = ($value->area_code ? $value->area_code : null) . ($value->number ? $value->number : null) . ($value->extension ? $value->extension : null);
        $value->unformattedNumberNoExt = ($value->country ? $value->country : null) . ($value->area_code ? $value->area_code : null) . ($value->number ? $value->number : null);
        $value->unformattedNumber = $value->unformattedNumberNoExt . ($value->extension ? $value->extension : null);

        foreach(explode("\n",$this->data["output_format_options"]) as $format) {
            if(trim(preg_replace('!/\*.*?\*/!s', '', $format)) == '') continue;
            $formatParts = explode('|', $format);
            $formatName = trim($formatParts[0]);
            $formatCode = trim($formatParts[1]);
            $value->$formatName = $this->formatPhone($value->country, $value->area_code, $value->number, $value->extension, $formatCode);
        }

        return $value;
    }

    /**
     * Format the value for string output, eg in a Lister table
     *
     */
    public function ___markupValue(Page $page, Field $field, $value = null, $property = '') {
        if(is_null($value)) return;
        $outputCode = $this->getOutputFormat($value, $field);
        return $this->formatPhone($value->country, $value->area_code, $value->number, $value->extension, $outputCode);
    }

    /**
     *
     * Add mapping to different name for use in page selectors
     * This enables us to use it like "field.country=61, field.area_code=225, field.number=123456, field.extension=123"
     */
    public function getMatchQuery($query, $table, $subfield, $operator, $value) {
        if($subfield == 'raw') $subfield = 'data';
        if($subfield == 'country') $subfield = 'data_country';
        if($subfield == 'area_code') $subfield = 'data_area_code';
        if($subfield == 'number') $subfield = 'data_number';
        if($subfield == 'extension') $subfield = 'data_extension';

		if($this->wire('database')->isOperator($operator)) {
			// if dealing with something other than address, or operator is native to SQL,
			// then let Fieldtype::getMatchQuery handle it instead
			return parent::getMatchQuery($query, $table, $subfield, $operator, $value);
		}
		// if we get here, then we're performing either %= (LIKE and variations) or *= (FULLTEXT and variations)
		$ft = new DatabaseQuerySelectFulltext($query);
		$ft->match($table, $subfield, $operator, $value);
		return $query;

    }

    /**
     * get Inputfield for this fieldtype, set config attributes so they can be used in the inputfield
     *
     */
    public function getInputfield(Page $page, Field $field) {
        $pn = $this->wire('modules')->get('InputfieldPhone');
        return $pn;
    }

    /**
     * there's none compatible
     *
     */
    public function ___getCompatibleFieldtypes(Field $field) {
        return null;
    }

    /**
     * blank value is an WireData object Phone
     *
     */
    public function getBlankValue(Page $page, Field $field) {
        return new Phone($field);
    }

    /**
     * Any value will get sanitized before setting it to a page object
     * and before saving the data
     *
     * If value not of instance Phone return empty instance
     */
    public function sanitizeValue(Page $page, Field $field, $value) {

        if(!$value instanceof Phone) $value = $this->getBlankValue($page, $field);

        // report any changes to the field values
        if($value->isChanged('country')
            || $value->isChanged('area_code')
            || $value->isChanged('number')
            || $value->isChanged('extension')
            || $value->isChanged('output_format')) {
                $page->trackChange($field->name);
        }
        return $value;
    }

    /**
     * get values converted when fetched from db
     *
     */
    public function ___wakeupValue(Page $page, Field $field, $value) {

        // get blank phone number (pn)
        $pn = $this->getBlankValue($page, $field);

        $sanitizerType = $field->allow_letters_input ? 'text' : 'digits';

        // populate the pn
        if(isset($value['data'])) $pn->raw = $this->wire('sanitizer')->$sanitizerType($value['data']);
        if(isset($value['data_country'])) $pn->country = $this->wire('sanitizer')->$sanitizerType($value['data_country']);
        if(isset($value['data_area_code'])) $pn->area_code = $this->wire('sanitizer')->$sanitizerType($value['data_area_code']);
        if(isset($value['data_number'])) $pn->number = $this->wire('sanitizer')->$sanitizerType($value['data_number']);
        if(isset($value['data_extension'])) $pn->extension = $this->wire('sanitizer')->$sanitizerType($value['data_extension']);
        if(isset($value['data_output_format'])) $pn->output_format = $this->wire('sanitizer')->text($value['data_output_format']);

        return $pn;
    }

    /**
     * return converted from object to array for storing in database
     *
     */
    public function ___sleepValue(Page $page, Field $field, $value) {

        // throw error if value is not of the right type
        if(!$value instanceof Phone)
            throw new WireException("Expecting an instance of Phone");

        $sanitizerType = $field->allow_letters_input ? 'text' : 'digits';

        $sleepValue = array(
            'data' => $this->wire('sanitizer')->$sanitizerType($value->country . $value->area_code . $value->number),
            'data_country' => $this->wire('sanitizer')->$sanitizerType($value->country),
            'data_area_code' => $this->wire('sanitizer')->$sanitizerType($value->area_code),
            'data_number' => $this->wire('sanitizer')->$sanitizerType($value->number),
            'data_extension' => $this->wire('sanitizer')->$sanitizerType($value->extension),
            'data_output_format' => $this->wire('sanitizer')->text($value->output_format)
       );

        return $sleepValue;
    }

    /**
     * Get the database schema for this field
     *
     * @param Field $field In case it's needed for the schema, but usually should not.
     * @return array
     */
    public function getDatabaseSchema(Field $field) {

        $schema = parent::getDatabaseSchema($field);
        $schema['data'] = 'varchar(15) NOT NULL';
        $schema['data_country'] = 'varchar(15) NOT NULL';
        $schema['data_area_code'] = 'varchar(15) NOT NULL';
        $schema['data_number'] = 'varchar(15) NOT NULL';
        $schema['data_extension'] = 'varchar(15) NOT NULL';
        $schema['data_output_format'] = 'varchar(255) NOT NULL';
        // key for data will already be added from the parent
        $schema['keys']['data_country'] = 'KEY data_country(data_country)';
        $schema['keys']['data_area_code'] = 'KEY data_area_code(data_area_code)';
        $schema['keys']['data_number'] = 'KEY data_number(data_number)';
        $schema['keys']['data_extension'] = 'KEY data_extension(data_extension)';
        $schema['keys']['data_output_format'] = 'KEY data_output_format(data_output_format)';
        return $schema;
    }

    /**
     * Get any inputfields used for configuration of this Fieldtype.
     *
     * This is in addition to any configuration fields supplied by the parent Inputfield.
     *
     * @param Field $field
     * @return InputfieldWrapper
     *
     */
    public function getModuleConfigInputfields(array $data) {

        foreach(self::getDefaultData() as $key => $value) {
            if(!isset($data[$key]) || $data[$key]=='') $data[$key] = $value;
        }

        $inputfields = new InputfieldWrapper();

        $f = $this->wire('modules')->get('InputfieldSelect');
        $f->attr('name', 'output_format');
        $f->label = __('Phone Output Format', __FILE__);
        $f->description = __("Select the default format to be used when outputting phone numbers.\n\nYou can define new formats for this dropdown select in the 'Phone Output Format Options' field below.", __FILE__);
        $f->notes = __("This can be overridden on the Input tab of each 'phone' field.", __FILE__);
        $f->addOption('', __('None', __FILE__));
        foreach($this->buildOptions(explode("\n",$this->data["output_format_options"]), $this->data) as $option) {
            $f->addOption($option[0], $option[1]);
            if($this->data["output_format"] == $option[0]) $f->attr('value', $option[0]);
        }
        $inputfields->add($f);

        $f = $this->wire('modules')->get("InputfieldTextarea");
        $f->attr('name', 'output_format_options');
        $f->attr('value', $this->data["output_format_options"]);
        $f->attr('rows', 10);
        $f->label = __('Phone Output Format Options', __FILE__);
        $f->description = __("Any formats listed here will be available from the Phone Output Format selector above, as well as the Format Override selector when entering data for phone number fields.\n\nOne format per line: `name | format | example numbers`\n\nEach component of the phone number is surrounded by { }\nThe names of the component parts are surrounded by [ ]\nTwo optional comma separated numbers after the component name are used to get certain parts of the number using the [PHP substr() function](http://php.net/manual/function.substr.php), allowing for complete flexibility.\nAnything outside the [ ] or { } is used directly: +,-,(,),x, spaces, etc - whatever you want to use.\n\nPlease send me a PR on Github, or post to the support forum any new formats you create that you think others would find useful.", __FILE__);
        $inputfields->add($f);

        return $inputfields;
    }


    /**
     * Format a phone number with the given number format
     *
     * @param text $phoneCountry country code
     * @param text $phoneAreaCode area code
     * @param text $phoneNumber number
     * @param text $phoneExtension phone extension
     * @param string $format to use for formatting
     * @return string Formatted phone string
     *
     */
    public function formatPhone($phoneCountry, $phoneAreaCode, $phoneNumber, $phoneExtension, $format) {

        if(!$phoneNumber) return '';
        if(!strlen($format) || $format == '%s') return ($phoneCountry ? $phoneCountry : null) . ($phoneAreaCode ? $phoneAreaCode : null) . ($phoneNumber ? $phoneNumber : null) . ($phoneExtension ? $phoneExtension : null); // no formatting

        $pattern = preg_match_all("/{(.*?)}[^{]*/", $format, $components);

        $finalValue = '';
        $lastSuffix = '';
        foreach ($components[0] as $component) {

            $prefix = strstr($component, '[', true);
            $suffix = str_replace(']','',strstr($component, ']'));
            $component = str_replace(array($prefix, $suffix, '[', ']'), null, $component);

            if(strcspn($component, '0123456789') != strlen($component)) {
                $component_name = strstr($component, ',', true);
                $char_cutoffs = explode(',',ltrim(str_replace($component_name, '', $component),','));
                $value = trim(substr($$component_name, $char_cutoffs[0], $char_cutoffs[1]));
            }
            else {
                $component_name = $component;
                $value = $$component_name;
            }
            $finalValue .= ($value != '' ? $prefix . $value . $suffix : null);
            // if this component has no value, or is not numeric, remove the last suffix
            if($value == '' || !is_numeric($value)) $finalValue = rtrim($finalValue, $lastSuffix);
            $lastSuffix = str_replace('}', '', $suffix);
        }
        $finalValue = trim(str_replace(array('{', '}'), null, $finalValue));
        return $finalValue;
    }

    public function buildOptions($options, $data) {
        $optionsArr = array();
        foreach($options as $format) {
            if(trim(preg_replace('!/\*.*?\*/!s', '', $format)) == '') continue;
            $formatParts = explode('|', $format);
            $formatName = trim($formatParts[0]);
            $formatCode = trim($formatParts[1]);
            $defaultExampleNumbers = array(1,221,5673456,123);
            $exampleNumbers = isset($formatParts[2]) ? array_map('trim', explode(',', trim($formatParts[2]))) : $defaultExampleNumbers;
            $phoneNumberFormatted = $this->formatPhone(
                isset($exampleNumbers[0]) ? $exampleNumbers[0] : $defaultExampleNumbers[0],
                isset($exampleNumbers[1]) ? $exampleNumbers[1] : $defaultExampleNumbers[1],
                isset($exampleNumbers[2]) ? $exampleNumbers[2] : $defaultExampleNumbers[2],
                isset($exampleNumbers[3]) ? $exampleNumbers[3] : $defaultExampleNumbers[3],
                $formatCode
            );
            $optionsArr[] = array($formatName, $formatName . ' | ' . $phoneNumberFormatted);
        }
        return $optionsArr;
    }

    public function getFormatFromName($formatName) {
        foreach(explode("\n",$this->data['output_format_options']) as $format) {
            if(trim(preg_replace('!/\*.*?\*/!s', '', $format)) == '') continue;
            $formatParts = explode('|', $format);
            if(trim($formatParts[0]) == $formatName) {
                return trim($formatParts[1]);
            }
        }
    }

    public function getOutputFormat($value, $field) {
        if($value->output_format) {
            $output_format = $value->output_format;
        }
        elseif($field->output_format) {
            $output_format = $field->output_format;
        }
        else {
            $output_format = $this->data["output_format"];
        }

        return $this->getFormatFromName($output_format);
    }

}


/**
 * Helper WireData Class to hold a Phone object
 *
 */
class Phone extends WireData {

    public function __construct($field = null) {
        $this->field = $field;
        $this->set('country', null);
        $this->set('area_code', null);
        $this->set('number', null);
        $this->set('extension', null);
        $this->set('output_format', null);
    }

    public function set($key, $value) {

        if($key == 'country' || $key == 'area_code' || $key == 'number' || $key == 'extension') {
            // if value isn't numeric set it to blank and throw an exception so it can be seen on API usage
            if($this->field && !$this->field->allow_letters_input && !is_numeric($value) && !is_null($value) && $value != '') {
                $value = $this->$key ? $this->$key : '';
                throw new WireException("Phone Object only accepts numbers");
            }
        }
        return parent::set($key, $value);
    }

    public function get($key) {
        return parent::get($key);
    }

    public function __toString() {
        $number = (string)$this->formattedNumber ? (string)$this->formattedNumber : $this->data['number'];
        if(!$number) $number = '';
        return $number;
    }


}

SelectFile Modul von Martijn Geerts[Bearbeiten]

Besteht i.d.R. aus zwei Teilen:

  • Fieldtype (FieldtypeSelectFile.module) Ist für das Backend zuständig
  • Inputfield (InputfieldSelectFile.module) Ist für die Ausgabe im Frontend zuständig (render Methode)

FieldtypeSelectFile.module

<?php

/**
 * Fieldtype 'select file' stores a file/folder name selected in the associated
 * Inputfield.
 *
 * ©2019 Martijn Geerts
 *
 * ProcessWire 3.x
 * Copyright (C) 2010 by Ryan Cramer
 * Licensed under GNU/GPL v2, see LICENSE.TXT
 *
 * http://www.processwire.com
 * http://www.ryancramer.com
 *
 */

class FieldtypeSelectFile extends FieldtypeText {

	/**
	 * Return an array of module information
	 *
	 * @return array
	 */
	public static function getModuleInfo() {
		return array(
			'title' => __('Select File'),
			'version' => 105,
			'summary' => __('Fieldtype that stores a file or folder.'),
			'author' => 'Martijn Geerts',
			'href' => 'https://processwire.com/talk/topic/6377-fieldtypeselectfile-inputfieldselectfile/',
			'installs' => 'InputfieldSelectFile',
		);
	}

	/**
	 * This method is called when all system classes are loaded and ready for API usage
	 *
	 */
	public function init() {
		parent::init();
		$this->allowTextFormatters(false);
		$this->addHookBefore('Page::loaded', $this, 'changePageTemplate');
	}

	/**
	 * Change the page template to render
	 *
	 */
	public function changePageTemplate(HookEvent $event) {
		// Page before loaded
		$page = $event->object;
		// Inputfield object
		$inputfield = $page->fields->get('type=FieldtypeSelectFile');
		// Is not of type FieldtypeSelectFile
		if ($inputfield === NULL) return;
		// If change page template is not set in the Inputfield config
		if (!$inputfield->template) return;
		$value = trim($page->get($inputfield->name));
		// If no value return
		if (!$value) return;
		$path = $this->config->paths->templates;
		$folder = ltrim($inputfield->folderPath, '/') . '/';
		$filename = $path . $folder . $value;
		if (!is_file($filename)) return;
		$page->template->set('filename', $filename);
	}

	/**
	 * Sanitize value for storage
	 *
	 */
	public function sanitizeValue(Page $page, Field $field, $value) {
		$file = $this->config->paths->templates . trim(trim($field->folderPath, '/')) . '/' . $value;
		if(is_file($file) || is_dir($file)) return $value;
		return '';
	}

	/**
	 * Return new instance of the Inputfield associated with this Fieldtype
	 *
	 * @param Page $page
	 * @param Field $field
	 * @return Inputfield
	 *
	 */
	public function getInputfield(Page $page, Field $field) {
		$inputfield = $this->modules->get('InputfieldSelectFile');
		$inputfield->set('folderPath', $field->folderPath);
		$inputfield->set('fileExt', $field->fileExt);
		$inputfield->set('fileDesc', $field->fileDesc);
		$inputfield->set('hideFiles', $field->hideFiles);
		$inputfield->set('hideFolders', $field->hideFolders);
		$inputfield->set('sort', $field->sort);
		$inputfield->set('template', $field->template);
		return $inputfield;
	}

	/**
	 * Get the inputfield used for configuration of this Fieldtype.
	 *
	 * @param Field $field
	 * @return InputfieldWrapper
	 *
	 */
	public function ___getConfigInputfields(Field $field) {
		$error = false;
		if ($field->folderPath) {
			$folder = $this->config->paths->templates . ltrim($field->folderPath, '/');
			if (!is_dir($folder)) {
				$path = $this->config->urls->templates . ltrim($field->folderPath, '/');
				$error = sprintf($this->_("Folder %s doesn't exist."), $path);
			}
		}

		$inputfields = parent::___getConfigInputfields($field);

		$f = $this->modules->get('InputfieldText');
		$f->attr('name', 'folderPath');
		$f->label = $this->_("The folder containing the files and/or folders.");
		$f->attr('value', $field->folderPath);
		$f->description =
			sprintf($this->_('A relative path relative to the **%s** folder.'), $this->config->urls->templates) .
			' ' .
			sprintf($this->_('Leave blank for the **%s** folder.'), $this->config->urls->templates);
		$f->notes =
			$this->_("When the files are located in /site/templates/scripts/, type: scripts/");
		$f->getErrors(true);
		if ($error) $f->error($error);
		$inputfields->add($f);

		$f = $this->modules->get('InputfieldCheckbox');
		$f->attr('name', 'fileExt');
		$f->label = $this->_("Hide File Extension");
		$f->attr('autocheck', 1);
		$f->attr('uncheckedValue', 0);
		$f->attr('checkedValue', 1);
		$f->columnWidth = 20;
		$f->attr('value', $field->fileExt);
		$inputfields->add($f);

		$f = $this->modules->get('InputfieldCheckbox');
		$f->attr('name', 'fileDesc');
		$f->label = $this->_("Hide PHP File Description");
		$f->attr('autocheck', 1);
		$f->attr('uncheckedValue', 0);
		$f->attr('checkedValue', 1);
		$f->columnWidth = 20;
		$f->attr('value', $field->fileDesc);
		$inputfields->add($f);

		$f = $this->modules->get('InputfieldCheckbox');
		$f->attr('name', 'hideFiles');
		$f->label = $this->_("Hide Files");
		$f->attr('autocheck', 1);
		$f->attr('uncheckedValue', 0);
		$f->attr('checkedValue', 1);
		$f->columnWidth = 20;
		$f->attr('value', $field->hideFiles);
		$inputfields->add($f);

		$f = $this->modules->get('InputfieldCheckbox');
		$f->attr('name', 'hideFolders');
		$f->label = $this->_("Hide Folders");
		$f->attr('autocheck', 1);
		$f->attr('uncheckedValue', 0);
		$f->attr('checkedValue', 1);
		$f->columnWidth = 20;
		$f->attr('value', $field->hideFolders);
		$inputfields->add($f);

		$f = $this->modules->get('InputfieldCheckbox');
		$f->attr('name', 'sort');
		$f->label = $this->_("Natural Sort (Select options)");
		$f->attr('autocheck', 1);
		$f->attr('uncheckedValue', 0);
		$f->attr('checkedValue', 1);
		$f->columnWidth = 20;
		$f->attr('value', $field->sort);
		$inputfields->add($f);

		$f = $this->modules->get('InputfieldCheckbox');
		$f->attr('name', 'template');
		$f->label = $this->_('Change Page Template');
		$f->label2 = $this->_('Use selected file as template');
		$f->description =
			$this->_('Just before the **Page::loaded** event the selected file is set as template file for the page.') .
			' ' .
			$this->_('This setting can only be applied once per a page and folders are exluded from the select inputfield.');
		$f->attr('autocheck', 1);
		$f->attr('uncheckedValue', 0);
		$f->attr('checkedValue', 1);
		$f->attr('value', $field->template);
		$inputfields->add($f);

		return $inputfields;
	}
}

InputfieldSelectFile.module

<?php

/**
 * Inputfield 'select file' provides a HTML select to select a file or a folder
 * from disk. Per Inputfield you can set a folder to list from.
 *
 * ©2019 Martijn Geerts
 *
 * ProcessWire 3.x
 * Copyright (C) 2010 by Ryan Cramer
 * Licensed under GNU/GPL v2, see LICENSE.TXT
 *
 * http://www.processwire.com
 * http://www.ryancramer.com
 *
 */

class InputfieldSelectFile extends InputfieldText {

	/**
	 * Return an array of module information
	 *
	 * @return array
	 */
	public static function getModuleInfo() {
		return array(
			'title' => 'Select File',
			'version' => 105,
			'summary' => __('Inputfield to select a file or a folder.'),
			'author' => 'Martijn Geerts',
			'href' => 'https://processwire.com/talk/topic/6377-fieldtypeselectfile-inputfieldselectfile/',
			'requires' => array(
				'FieldtypeSelectFile',
			),
		);
	}

	/**
	 * Return the completed output of Inputfield select file
	 *
	 * @return string
	 *
	 */
	public function ___render() {

		$array = array();
		$folder = $this->config->paths->templates . trim(trim($this->folderPath), '/') . "/";
		$phpFileDescription = false;

		if(!is_dir($folder)) {
			$this->error($this->_("Path to files is invalid"));
		} else {
			$array[] = "<option value=''></option>";
			$handle = opendir($folder);
			while (false !== ($entry = readdir($handle))) {

				if (strpos($entry, '.') === 0) continue;
				if (is_file($folder . $entry) && $this->hideFiles) continue;
				if (is_dir($folder . $entry) && $this->hideFolders) continue;
				if (is_dir($folder . $entry) && $this->template) continue;
				if (is_file($folder . $entry) && $this->fileExt) {
					$exploded = explode('.', $entry);
					array_pop($exploded);
					$label = implode('.', $exploded);
				} else {
					$label = $entry;
				}
				// pull "Description" comment from php files if it exists (inspired by WordPress "Template Name" comment)
				if(!$this->fileDesc && pathinfo($entry)['extension'] === 'php') {
					$phpFileData = implode('', file($folder . $entry));
					if (preg_match('|Description:(.*)$|mi', $phpFileData, $desc)) {
						$phpFileDescription = trim(preg_replace('/\s*(?:\*\/|\?>).*/', '', $desc[1]));
						$label .= " (" . $phpFileDescription . ")";
					}
				}

				$selected = $entry == $this->value ? " selected" : '';
				$array[] = "<option value='" . $entry . "'" . $selected . ">" . $label . "</option>";
			}
			closedir($handle);
		}

		if ($this->sort) natcasesort($array);

		return "<select name='" . $this->name . "'>" . implode('', $array) . "</select>";
	}

	/**
	 * Get any custom configuration fields for Inputfield select file
	 *
	 * @return InputfieldWrapper
	 *
	 */
	public function ___getConfigInputfields() {
		$inputfields = parent::___getConfigInputfields();

		$f = $inputfields->get('stripTags');
		if($f) $inputfields->remove($f);

		$f = $inputfields->get('size');
		if($f) $inputfields->remove($f);

		$f = $inputfields->get('maxlength');
		if($f) $inputfields->remove($f);

		$f = $inputfields->get('placeholder');
		if($f) $inputfields->remove($f);

		$f = $inputfields->get('pattern');
		if($f) $inputfields->remove($f);

		return $inputfields;
	}
}

Fieldtype RuntimeMarkup[Bearbeiten]

https://modules.processwire.com/modules/fieldtype-runtime-markup/

Kann über ein externes PHP File rendern. Beispiel für ein Fieldtype ohne Datenbank-Zugriff.

Fieldtype Checkbox[Bearbeiten]

Einfaches Coremodule zum spicken.