Subversion Repositories web.active

Rev

Rev 1 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | Download

<?php namespace ProcessWire;

/**
 * ProcessWire Module Fieldtype
 *
 * Field that stores reference to another Module. 
 *
 * For documentation about the fields used in this class, please see:  
 * /wire/core/Fieldtype.php
 * 
 * ProcessWire 3.x, Copyright 2022 by Ryan Cramer
 * https://processwire.com
 * 
 */

class FieldtypeModule extends Fieldtype {

  /**
   * Get module info
   * 
   * @return array
   * 
   */
  public static function getModuleInfo() {
    return array(
      'title' => 'Module Reference',
      'version' => 102,
      'summary' => 'Field that stores a reference to another module',
      'permanent' => true, 
    );
  }

  /**
   * Get blank value
   * 
   * @param Page $page
   * @param Field $field
   * @return bool|int|null|ModulePlaceholder
   * @throws WireException
   * 
   */
  public function getBlankValue(Page $page, Field $field) {
    $blankType = $field->get('blankType');
    if($blankType === 'zero') {
      $value = 0;
    } else if($blankType === 'false') {
      $value = false;
    } else if($blankType === 'placeholder') {
      $value = $this->wire(new ModulePlaceholder());
    } else {
      $value = null;
    }
    return $value;
  }

  /**
   * Should this Fieldtype only be allowed for new fields in advanced mode?
   * 
   * @return bool
   *
   */
  public function isAdvanced() {
    return true; 
  }

  /**
   * Sanitize value
   * 
   * @param Page $page
   * @param Field $field
   * @param int|object|WireArray|string $value
   * @return int|bool|null|Module
   * 
   */
  public function sanitizeValue(Page $page, Field $field, $value) {
    if(!$value) return $this->getBlankValue($page, $field);
    $modules = $this->wire()->modules;
    if($field->get('instantiateModule')) return $value instanceof Module ? $value : $modules->get($value); 
    if(ctype_digit("$value")) return $modules->getModuleClass((int) $value); 
    return $modules->getModuleID($value) ? $value : $this->getBlankValue($page, $field);
  }

  /**
   * Wakeup value
   * 
   * @param Page $page
   * @param Field $field
   * @param array|int|string $value
   * @return string
   * 
   */
  public function ___wakeupValue(Page $page, Field $field, $value) {
    if(empty($value)) return $this->getBlankValue($page, $field);
    $modules = $this->wire()->modules;
    if($field->get('instantiateModule')) return $modules->get($value); 
    return $modules->getModuleClass((int) $value); 
  }

  /**
   * Sleep value
   * 
   * @param Page $page
   * @param Field $field
   * @param array|float|int|object|string $value
   * @return int
   * 
   */
  public function ___sleepValue(Page $page, Field $field, $value) {
    $blankValue = $this->getBlankValue($page, $field);
    if(!$value || "$blankValue" == "$value") {
      return 0;
    } else {
      return $this->wire()->modules->getModuleID($value);
    }
  }

  /**
   * Get all selectable modules for given field
   * 
   * @param Field $field
   * @return array Returns array of associative arrays with class, title, label, summary.
   * 
   */
  public function getSelectableModules(Field $field) {
    
    $modules = $this->wire()->modules;
    $options = array();
    $moduleTypes = $field->get('moduleTypes');
    $inputfieldClass = $field->get('inputfieldClass');
    $labelField = $field->get('labelField');
    $matchType = $field->get('matchType'); // verbose or prefix
    
    if($matchType !== 'prefix' || empty($moduleTypes)) {
      $checkModules = $modules;
    } else {
      $checkModules = array();
      foreach($moduleTypes as $prefix) {
        foreach($modules->findByPrefix($prefix, false) as $moduleName) {
          $checkModules[] = $moduleName;
        }
      }
    }

    foreach($checkModules as $module) {
      
      if($matchType != 'prefix' && !empty($moduleTypes)) {
        $ns = $modules->getModuleNamespace($module);
        $parents = wireClassParents($ns ? "$ns$module" : "$module");
        $found = false;
        foreach($moduleTypes as $moduleType) {
          if(!in_array($moduleType, $parents)) continue;
          $found = true;
          break;
        }
        if(!$found) continue;
      }
      
      $class = (string) $module;
      $summary = '';
      $title = '';
      
      if($labelField === 'title') {
        $info = $modules->getModuleInfo($module);
        $id = $info['id'];
        $label = !empty($info['title']) ? $info['title'] : (string) $module;
        $title = $label;
        $keyType = 'title';
        
      } else if($labelField === 'title-summary') {
        $info = $modules->getModuleInfoVerbose($module);
        $id = $info['id'];
        $title = $info['title'];
        $label = !empty($title) ? $title : (string) $module;
        $keyType = 'title';
        if(!empty($info['summary'])) {
          $summary = $info['summary'];
          if($inputfieldClass === 'InputfieldRadios') {
            $label .= " [span.detail] • $summary [/span]";
          } else {
            $label .= " • $summary";
          }
        }
        
      } else {
        $info = $modules->getModuleInfo($module);
        $id = $info['id'];
        $keyType = 'class';
        $label = $class;
      }

      $option = array(
        'id' => $id, 
        'class' => $class,
        'label' => $label,
        'title' => $title,
        'summary' => $summary,
      );
      
      $key = $option[$keyType];
      while(isset($options[$key])) $key .= ' '; // just in case
      $options[$key] = $option;
    }

    ksort($options);
    
    return array_values($options);
  }

  /**
   * Get Inputfield
   * 
   * @param Page $page
   * @param Field $field
   * @return Inputfield
   * 
   */
  public function getInputfield(Page $page, Field $field) {
  
    $modules = $this->wire()->modules;

    $inputfieldClass = $field->get('inputfieldClass');
    $inputfieldClass = $inputfieldClass ? $inputfieldClass : 'InputfieldSelect';
    
    /** @var InputfieldSelect $inputfield */
    $inputfield = $modules->get($inputfieldClass); 
    if(!$inputfield) $inputfield = $modules->get('InputfieldSelect'); 

    $inputfield->attr('name', $field->name); 
    $inputfield->class = $this->className();
    
    if($inputfieldClass == 'InputfieldRadios' && $field->get('showNoneOption')) {
      $inputfield->addOption(0, $this->_('None'));
    }

    foreach($this->getSelectableModules($field) as $info) {
      $inputfield->addOption($info['class'], $info['label']); 
    }

    return $inputfield; 
  }

  /**
   * Get database schema
   * 
   * @param Field $field
   * @return array
   * 
   */
  public function getDatabaseSchema(Field $field) {
    $schema = parent::getDatabaseSchema($field); 
    $schema['data'] = 'int NOT NULL';
    return $schema;
  }

  /**
   * Configure field
   * 
   * @param Field $field
   * @return InputfieldWrapper
   * 
   */
  public function ___getConfigInputfields(Field $field) {

    $modules = $this->wire()->modules;
    $inputfields = parent::___getConfigInputfields($field);
    $moduleTypes = array();
    
    foreach($modules as $module) {
      if(strpos($module->className(), 'AdminTheme') === 0) {
        $matches = array('', 'AdminTheme');
      } else {
        if(!preg_match('/^([A-Za-z][a-z0-9_]+)/', $module->className(), $matches)) continue;
      }
      $moduleType = $matches[1];
      $moduleTypes[$moduleType] = $moduleType;
    }
    ksort($moduleTypes);

    /** @var InputfieldCheckboxes $f */
    $f = $modules->get("InputfieldCheckboxes"); 
    $f->attr('name', 'moduleTypes'); 
    $f->addOptions($moduleTypes);
    $value = $field->get('moduleTypes');
    if(!is_array($value)) $value = array();
    $f->attr('value', $value);
    $f->label = $this->_('Module Types');
    $f->description = $this->_('Check all of the module types that may be selectable in this field.');
    $f->optionWidth = 250;
    $inputfields->append($f); 
  
    /** @var InputfieldRadios $f */
    $f = $modules->get('InputfieldRadios');
    $f->attr('name', 'matchType'); 
    $f->label = $this->_('Module matching type');
    $f->addOption('prefix', $this->_('Prefix')); 
    $f->addOption('verbose', $this->_('Inheritance'));
    $f->notes = $this->_('The prefix option is recommended for better performance unless it fails to match a module you intend.');
    $value = $field->get('matchType');
    $f->val($value ? $value : 'verbose');
    $inputfields->add($f);

    /** @var InputfieldCheckbox $f */
    $f = $modules->get("InputfieldCheckbox"); 
    $f->attr('name', 'instantiateModule'); 
    $f->label = $this->_('Make this field an instance of the selected module?'); 
    $f->description = $this->_('If checked, the field value will be an actual instance of the selected module. If not checked, the field value will be a string containing the class name of the module.'); // instantiate module description
    if($field->get('instantiateModule')) $f->attr('checked', 'checked'); 
    $inputfields->add($f); 

    /** @var InputfieldRadios $f */
    $f = $modules->get('InputfieldRadios'); 
    $f->label = $this->_('Options Label'); 
    $f->attr('name', 'labelField'); 
    $f->addOption('', $this->_('Name')); 
    $f->addOption('title', $this->_('Title'));
    $f->addOption('title-summary', $this->_('Title and summary'));
    $f->attr('value', $field->get('labelField')); 
    $f->columnWidth = 50; 
    $inputfields->add($f);

    /** @var InputfieldRadios $f */
    $f = $modules->get('InputfieldRadios'); 
    $f->label = $this->_('Input Type'); 
    $f->attr('name', 'inputfieldClass'); 
    $f->addOption('', $this->_('Select')); 
    $f->addOption('InputfieldRadios', $this->_('Radios')); 
    $f->columnWidth = 50;
    $f->attr('value', $field->get('inputfieldClass'));
    $inputfields->add($f); 
  
    /** @var InputfieldCheckbox $f */
    $f = $modules->get('InputfieldCheckbox');
    $f->label = $this->_('Show a “None” option?');
    $f->attr('name', 'showNoneOption');
    if($field->get('showNoneOption')) $f->attr('checked', 'checked'); 
    $f->showIf = 'inputfieldClass=InputfieldRadios';
    $inputfields->add($f);
  
    /** @var InputfieldRadios $f */
    $f = $modules->get('InputfieldRadios');
    $f->attr('name', 'blankType');
    $f->label = $this->_('Blank value type');
    $f->addOption('null', 'Null');
    $f->addOption('zero', 'Integer 0');
    $f->addOption('false', 'Boolean false');
    $f->addOption('placeholder', 'ModulePlaceholder instance');
    $value = $field->get('blankType');
    if($value === null) $value = 'null';
    $f->val($value);
    $inputfields->add($f);

    return $inputfields;      
  }

  /**
   * Get compatible Fieldtypes
   * 
   * @param Field $field
   * @return Fieldtypes 
   * 
   */
  public function ___getCompatibleFieldtypes(Field $field) {
    $fieldtypes = $this->wire(new Fieldtypes());
    foreach($this->wire()->fieldtypes as $fieldtype) {
      if($fieldtype instanceof FieldtypeModule) $fieldtypes->add($fieldtype);
    }
    return $fieldtypes;
  }
  
  /**
   * Render a markup string of the value.
   *
   * @param Page $page Page that $value comes from
   * @param Field $field Field that $value comes from
   * @param mixed $value Optionally specify the value returned by `$page->getFormatted('field')`.
   *  When specified, value must be a formatted value.
   *  If null or not specified (recommended), it will be retrieved automatically.
   * @param string $property Optionally specify the property or index to render. If omitted, entire value is rendered.
   * @return string|MarkupFieldtype Returns a string or object that can be output as a string, ready for output.
   *  Return a MarkupFieldtype value when suitable so that the caller has potential specify additional
   *  config options before typecasting it to a string.
   *
   */
  public function ___markupValue(Page $page, Field $field, $value = null, $property = '') {
    if($value === null) $value = $page->getFormatted($field->name);
    $value = (string) $value;
    if(empty($value)) $value = '';
    return $value;
  }
  
  /**
   * Return array with information about what properties and operators can be used with this field.
   *
   * @param Field $field
   * @param array $data Array of extra data, when/if needed
   * @return array See `FieldSelectorInfo` class for details.
   *
   */
  public function ___getSelectorInfo(Field $field, array $data = array()) {
    if($data) {}
    $selectorInfo = $this->wire(new FieldSelectorInfo());
    $labelField = $field->get('labelField');
    $info = $selectorInfo->getSelectorInfo($field);
    $info['input'] = 'select';
    $info['operators'] = array('=', '!=');
    $selectableModules = $this->getSelectableModules($field);
    foreach($selectableModules as $m) {
      $id = (int) $m['id'];
      if($labelField === 'title' || $labelField === 'title-summary') {
        $label = $m['title'];
      } else {
        $label = $m['class'];
      }
      $info['options'][$id] = $label;
    }
    return $info;
  }
  
  /**
   * Get the database query that matches a Fieldtype table’s data with a given value.
   *
   * @param PageFinderDatabaseQuerySelect $query
   * @param string $table The table name to use
   * @param string $subfield Name of the subfield (typically 'data', unless selector explicitly specified another)
   * @param string $operator The comparison operator.
   * @param mixed $value Value to find.
   * @return PageFinderDatabaseQuerySelect|DatabaseQuerySelect $query
   * @throws WireException
   *
   */
  public function getMatchQuery($query, $table, $subfield, $operator, $value) {
    if(empty($subfield)) $subfield = 'data';
    if(!empty($value) && !ctype_digit("$value") && $subfield = 'data') {
      $value = $this->wire()->modules->getModuleID($value); // convert class name to ID
    }
    return parent::getMatchQuery($query, $table, $subfield, $operator, $value);
  }
}