Subversion Repositories web.active

Rev

Rev 22 | Blame | Compare with Previous | Last modification | View Log | Download

<?php namespace ProcessWire;

/**
 * ProcessWire Datetime Fieldtype
 *
 * Holds date and optionally time values. 
 *
 * 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 FieldtypeDatetime extends Fieldtype {
  
  public static function getModuleInfo() {
    return array(
      'title' => 'Datetime',
      'version' => 105,
      'summary' => 'Field that stores a date and optionally time',
    );
  }

  /**
   * Default date output format
   * 
   */
  const defaultDateOutputFormat = 'Y-m-d'; 

  /**
   * Return all predefined PHP date() formats for use as dates
   * 
   * Note: this method moved to the WireDateTime class and is kept here for backwards compatibility.
   * 
   * @deprecated Use WireDateTime class instead
   * @return array
   *
   */
  static public function getDateFormats() {
    return WireDateTime::_getDateFormats();
  }

  /**
   * Return all predefined PHP date() formats for use as times
   *
   * Note: this method moved to the WireDateTime class and is kept here for backwards compatibility.
   *
   * @deprecated Use WireDateTime class instead
   * @return array
   * 
   */
  static public function getTimeFormats() {
    return WireDateTime::_getTimeFormats();
  }

  /**
   * Given a date/time string and expected format, convert it to a unix timestamp
   *
   * Note: this method moved to the WireDateTime class and is kept here for backwards compatibility.
   * 
   * @param string $str Date/time string
   * @param string $format Format of the date/time string in PHP date syntax
   * @return int
   * @deprecated Use WireDateTime class instead
   *
   */
  static public function stringToTimestamp($str, $format) {
    $wdt = new WireDateTime();
    return $wdt->stringToTimestamp($str, $format);
  }

  /**
   * Format a date with the given PHP date() or PHP strftime() format
   * 
   * Note: this method moved to the WireDateTime class and is kept here for backwards compatibility.
   *
   * @param int $value Unix timestamp of date
   * @param string $format date() or strftime() format string to use for formatting
   * @return string Formatted date string
   * @deprecated Use WireDateTime class instead
   *
   */
  static public function formatDate($value, $format) {
    $wdt = new WireDateTime();
    return $wdt->formatDate($value, $format);
  }

  /**
   * Given a date() format, convert it to either 'js', 'strftime' or 'regex' format
   * 
   * Note: this method moved to the WireDateTime class and is kept here for backwards compatibility.
   *
   * @param string $format PHP date() format
   * @param string $type New format to convert to: either 'js', 'strftime', or 'regex' 
   * @return string
   * @deprecated Use WireDateTime class instead
   *
   */
  static public function convertDateFormat($format, $type) {
    $wdt = new WireDateTime();
    return $wdt->convertDateFormat($format, $type);
  }

  /*********************************************************************************************************************************
   *********************************************************************************************************************************/

  /**
   * Return the Inputfield used for date/time (InputfieldDatetime)
   * 
   * @param Page $page
   * @param Field $field
   * @return InputfieldDatetime
   *
   */
  public function getInputfield(Page $page, Field $field) {
    /** @var InputfieldDatetime $inputfield */
    $inputfield = $this->wire('modules')->get('InputfieldDatetime'); 
    $inputfield->class = $this->className();
    return $inputfield; 
  }

  /**
   * Get predefined setups for newly created fields of this type
   *
   * #pw-internal
   * #pw-hooker
   *
   * @return array
   * @since 3.0.213
   *
   */
  public function ___getFieldSetups() {
    return array(
      'datetime' => array(
        'title' => $this->_x('Date and Time', 'datetime-setup'), // label for datetime field setup
        'dateOutputFormat' => $this->_x('j F Y g:i a', 'datetime-setup'), // default date/time output format for datetime (PHP dateformat)
        'dateInputFormat' => $this->_x('Y-m-d', 'datetime-setup'), // default date input format for datetime (PHP dateformat)
        'timeInputFormat' => $this->_x('g:i a', 'datetime-setup'), // default time input format for datetime (PHP dateformat)
        'inputType' => $this->_x('html', 'datetime-setup'), // default input type to use for datetime - one of: html, text, select
        'htmlType' => 'datetime',
      ),
      'date' => array(
        'title' => $this->_x('Date', 'date-setup'), // label for date-only fields setup
        'dateOutputFormat' => $this->_x('j F Y', 'date-setup'), // default output format for date (PHP dateformat)
        'dateInputFormat' => $this->_x('Y-m-d', 'date-setup'), // default input format for date (PHP dateformat)
        'timeInputFormat' => '', 
        'inputType' => $this->_x('html', 'date-setup'), // default input type to use for date - one of: html, text, select
        'htmlType' => 'date',
      ),
      'time' => array(
        'title' => $this->_x('Time', 'time-setup'), // label for time-only field setup
        'dateOutputFormat' => $this->_x('g:i a', 'time-setup'), // default output format for time (PHP dateformat)
        'dateInputFormat' => '',
        'timeInputFormat' => $this->_x('g:i a', 'time-setup'), // default input format for time (PHP dateformat)
        'inputType' => $this->_x('html', 'time-setup'), // default input type to use for time - one of: html, text, select
        'htmlType' => 'time',
      ),
    );
  }

  /**
   * Sanitize value, per Fieldtype interface
   * 
   * @param Page $page
   * @param Field $field
   * @param string|int|\DateTime $value
   * @return int
   *  
   */
  public function sanitizeValue(Page $page, Field $field, $value) {
    return $this->_sanitizeValue($value); 
  }

  /**
   * Sanitize a value assumed to be either a timestamp or in strtotime() compatible format
   * 
   * @param string|int|\DateTime
   * @return int|string Returns unix timestamp integer or blank string if empty or invalid value
   *
   */
  protected function _sanitizeValue($value) {
    if(empty($value)) {
      // empty value
      $value = '';
    } else if(is_int($value)) {
      // value okay as-is
    } else if($value instanceof \DateTime) {
      // instance of DateTime 
      $value = $value->getTimestamp();
    } else if(ctype_digit(ltrim("$value", '-'))) {
      // already a timestamp
      $value = (int) $value;
    } else {
      // convert date string to time
      $value = strtotime($value);
      if($value === false) $value = '';
    }
    return $value; 
  }

  /**
   * Format the value for output, according to selected format and language
   * 
   * @param Page $page
   * @param Field $field
   * @param int $value
   * @return string
   *
   */
  public function ___formatValue(Page $page, Field $field, $value) {
    $format = '';
    if($this->languages && !$this->user->language->isDefault()) $format = $field->get("dateOutputFormat{$this->user->language}"); 
    if(!$format) $format = $field->get('dateOutputFormat');
    return $this->wire('datetime')->formatDate($value, $format);
  }

  /**
   * Export value
   * 
   * @param Page $page
   * @param Field $field
   * @param int $value
   * @param array $options
   * @return string
   * 
   */
  public function ___exportValue(Page $page, Field $field, $value, array $options = array()) {
    if(!$value) return '';
    return date('Y-m-d H:i:s', $value); 
  }
  
  /**
   * Return whether the given value is considered empty or not
   *
   * @param Field $field
   * @param mixed $value
   * @return bool
   *
   */
  public function isEmptyValue(Field $field, $value) {
    
    if($value instanceof Selector) {
      // PageFinder is asking if it should let this Fieldtype handle the operator/value
      // combination with potential empty value present in a Selector
      $selector = $value;
      $op = substr($selector->operator, 0, 1);
      // tell PageFinder we will handle greater-than/less-than conditions in our getMatchQuery()
      if($op === '>' || $op === '<') return true;
    }
    
    // note: 0000-00-00 intentionally returns true, which is what $value is when PageFinder is testing
    // whether the Fieldtype recognizes an empty ISO-8601 date that it will convert to matching null
    
    $value = trim($value, '-0 ');
    
    return !strlen($value); 
  }

  /**
   * Match a date/time value in the database, as used by PageFinder
   * 
   * @param PageFinderDatabaseQuerySelect $query
   * @param string $table
   * @param string $subfield
   * @param string $operator
   * @param int|string $value
   * @return DatabaseQuerySelect
   * @throws WireException if given invalid operator
   *
   */
  public function getMatchQuery($query, $table, $subfield, $operator, $value) {
    
    $database = $this->wire()->database;
    $intValue = $this->_sanitizeValue($value);
    $table = $database->escapeTable($table);
    $subfield = $subfield ? $database->escapeCol($subfield) : 'data';
    $minDT = '1000-01-01 00:00:00'; // $maxDT = '9999-12-31 23:59:59';
    
    if(is_string($value) && in_array($operator, array('%=', '^='))) {
      // partial date string match
      if(!ctype_digit($value)) {
        $value = str_replace(array('/', '.'), '-', trim($value));
      }
      if(!ctype_digit(str_replace(array('-', ' '), '', $value))) {
        throw new WireException("Invalid partial date string '$value' (numbers, hyphens and space only)");
      }
      $value = str_replace(array('%', '_'), '', $value);
      $value = $operator === '^=' ? "$value%" : "%$value%";
      $query->where("$table.$subfield LIKE ?", $value);
      
    } else if(!$database->isOperator($operator)) {
      // invalid operator
      throw new WireException("$this invalid date operator: $operator");

    } else if(is_int($intValue)) {
      // matching a populated value that successfully converted to unix timestamp
      $dateString = date('Y-m-d H:i:s', $intValue);
      if($dateString !== false) {
        $query->where("$table.$subfield$operator?", $dateString);
      }
      
    } else {
      // matching an empty value
      if(in_array($operator, array('!=', '>', '>='))) {
        // match NOT empty (!=0, >0)
        $query->where("$table.$subfield>=?", $minDT);
        
      } else if(in_array($operator, array('=', '<', '<='))) {
        // match empty (=0, <0, <=0): match null or value below unix timestamp range
        // this includes 0000-00-00 when present and used by MySQL version
        $query->where("$table.$subfield IS NULL OR $table.$subfield<?", $minDT);
        
      } else {
        // unsupported operator
        throw new WireException("$this operator cannot be used here: $operator"); 
      }
    }
    
    return $query; 
  }

  /**
   * Get selector info
   * 
   * @param Field $field
   * @param array $data
   * @return array
   * 
   */
  public function ___getSelectorInfo(Field $field, array $data = array()) {
    $a = parent::___getSelectorInfo($field, $data); 
    $a['operators'] = array('=', '!=', '>', '>=', '<', '<=', '%=', '^=', '=""', '!=""'); 
    return $a;
  }

  /**
   * Return database schema used by this field
   * 
   * @param Field $field
   * @return array
   *
   */
  public function getDatabaseSchema(Field $field) {
    $schema = parent::getDatabaseSchema($field); 
    $schema['data'] = 'datetime NOT NULL';
    $schema['keys']['data'] = 'KEY data (data)'; 
    return $schema;
  }

  /**
   * Convert value from timestamp to Y-m-d H:i:s date string
   * 
   * @param Page $page
   * @param Field $field
   * @param string|int $value
   * @return string
   *
   */
  public function ___sleepValue(Page $page, Field $field, $value) {
    $value = $this->_sanitizeValue($value); 
    return is_int($value) ? date('Y-m-d H:i:s', $value) : '';
  }

  /**
   * Convert value from Y-m-d H:i:s string to timestamp
   * 
   * @param Page $page
   * @param Field $field
   * @param string $value
   * @return int
   *
   */
  public function ___wakeupValue(Page $page, Field $field, $value) {
    if(empty($value)) return '';
    $value = strtotime($value); 
    if($value === false) $value = '';
    return $value; 
  }

  /**
   * Get compatible Fieldtypes
   * 
   * @param Field $field
   * @return Fieldtypes
   * @throws WireException
   *
   */
  public function ___getCompatibleFieldtypes(Field $field) {
    $fieldtypes = $this->wire(new Fieldtypes());
    foreach($this->wire('fieldtypes') as $fieldtype) {
      if($fieldtype instanceof FieldtypeDatetime) $fieldtypes->add($fieldtype);
    }
    return $fieldtypes;
  }

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

    $modules = $this->wire()->modules;
    $inputfields = parent::___getConfigInputfields($field);
    $wdt = $this->wire('datetime'); /** @var WireDateTime $wdt */
    $dateOutputFormat = (string) $field->get('dateOutputFormat');

    /** @var InputfieldSelect $f */
    $f = $modules->get('InputfieldSelect'); 
    $f->attr('name', '_dateOutputFormat'); 
    $f->label = $this->_('Date Output Format'); 
    $f->description = $this->_('Select the format to be used when outputting dates with this field.') . ' ';
    $f->description .= $this->_('For relative date/time formats, see the Time Output Format.'); 
    $f->icon = 'calendar';
    $f->addOption('', $this->_('None'));
    $f->columnWidth = 50;
    $date = strlen(date('jn')) < 4 ? time() : strtotime('2016-04-08 5:10:02 PM');
    $found = false;
    foreach($wdt->getDateFormats() as $format) {
      $dateFormatted = (string) $wdt->formatDate($date, $format);   
      if($format == 'U') $dateFormatted .= " " . $this->_('(unix timestamp)');
      $f->addOption($format, "$dateFormatted [$format]"); 
      if(!$found && strpos($dateOutputFormat, $format) !== false) {
        $f->attr('value', $format); 
        $found = true; 
      }
    }
    $f->attr('onchange', "$('#Inputfield_dateOutputFormat').val($(this).val() + ' ' + $('#Inputfield__timeOutputFormat').val());"); 
    $inputfields->add($f); 

    /** @var InputfieldSelect $f */
    $f = $modules->get('InputfieldSelect'); 
    $f->attr('name', '_timeOutputFormat'); 
    $f->label = $this->_('Time Output Format'); 
    $f->description = $this->_('If you want to output time in addition to the date, select the format to be used when outputting time with this field. This will be combined with the date format.'); 
    $f->icon = 'clock-o';
    $f->addOption('', $this->_('None'));
    $f->columnWidth = 50;
    $date = strtotime('5:10:02 PM');
    foreach($wdt->getTimeFormats() as $format) {
      $timeFormatted = $wdt->formatDate($date, $format);  
      $f->addOption($format, "$timeFormatted [$format]"); 
      if(strpos($dateOutputFormat, $format) !== false) {
        list(,$test) = explode($format, $dateOutputFormat);
        if(!strlen($test)) $f->attr('value', $format);
      }
    }
    $f->attr('onchange', "$('#Inputfield_dateOutputFormat').val($('#Inputfield__dateOutputFormat').val() + ' ' + $(this).val());"); 
    //$f->collapsed = Inputfield::collapsedBlank;
    $inputfields->add($f);

    /** @var InputfieldText $f */
    $f = $modules->get("InputfieldText"); 
    $f->attr('name', 'dateOutputFormat'); 
    $f->attr('value', $field->get('dateOutputFormat')); 
    $f->attr('size', 20); 
    $f->label = $this->_('Date/Time Output Format Code');
    $f->description = $this->_('The date/time will be output according to the format below. This is automatically built from the date/time selections above, but you may change it as needed to suit your needs.') . ' ';
    $f->description .= $this->_('See the [PHP date](https://www.php.net/manual/en/function.date.php) function reference for more information on how to customize this format. Alternatively, you may use a [PHP strftime](https://www.php.net/manual/en/function.strftime.php) format if desired for localization.'); 
    $f->icon = 'code';
    $f->collapsed = Inputfield::collapsedYes; 
    if($this->languages) {
      $f->useLanguages = true; 
      foreach($this->languages as $language) {
        /** @var Language $language */
        if($language->isDefault()) continue; 
        $f->set("value$language", (string) $field->get('dateOutputFormat' . $language)); 
      }
    }
    $inputfields->add($f); 

    return $inputfields; 
  }
}