silverstripe-framework/src/Forms/DatetimeField.php

643 lines
16 KiB
PHP
Raw Normal View History

FEATURE New DatetimeField class (form field wrapper composed of DateField andTimeField) FEATURE New DateField and TimeField form classes with more consistent API and easier localization API CHANGE Date/time parsing in DateField, TimeField and DatetimeField defaults to i18n::get_locale() ('en_US') instead of using en_NZ/en_GB specific parsing. Use i18n::set_locale('en_NZ') in mysite/_config.php to revert to old behaviour. API CHANGE constructor parameter in TimeField needs to be in ISO date notation (not PHP's date()) API CHANGE TimeField, DateField and related subclasses use Zend_Date for date parsing, meaning they're stricer than the previously used strtotime() API CHANGE Removed DMYCalendarDateField and CalendarDateField, use DateField with setConfig('showcalendar') API CHANGE Removed CompositeDateField, DMYDateField, use DateField with setConfig('dmyfields') API CHANGE Removed DropdownTimeField, use TimeField with setConfig('showdropdown') API CHANGE Removed PopupDateTimeField, use DatetimeField API CHANGE Changed 'date', 'month' and 'year' HTML field names to lowercase in DMYDateField API CHANGE Removed support for ambiguous date formats in DateField, e.g. '06/03/03'. Use DateField->setConfig('dateformat', '<format>') to revert to this behaviour. API CHANGE Removed flag from DateField, CalendarDateField etc., use DateField->setConfig('min') and DateField->setConfig('max') ENHANCEMENT Using Zend_Date for DateField and TimeField, with more robust date handling, starting localization support. Set globally via i18n::set_locale(), or for a field instance through setLocale(). Note: Javascript validation is not localized yet. (from r99360) git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@102859 467b73ca-7a2a-4603-9d3b-597d59a354a9
2010-04-14 06:38:40 +02:00
<?php
namespace SilverStripe\Forms;
use IntlDateFormatter;
use InvalidArgumentException;
use SilverStripe\i18n\i18n;
use SilverStripe\ORM\FieldType\DBDatetime;
FEATURE New DatetimeField class (form field wrapper composed of DateField andTimeField) FEATURE New DateField and TimeField form classes with more consistent API and easier localization API CHANGE Date/time parsing in DateField, TimeField and DatetimeField defaults to i18n::get_locale() ('en_US') instead of using en_NZ/en_GB specific parsing. Use i18n::set_locale('en_NZ') in mysite/_config.php to revert to old behaviour. API CHANGE constructor parameter in TimeField needs to be in ISO date notation (not PHP's date()) API CHANGE TimeField, DateField and related subclasses use Zend_Date for date parsing, meaning they're stricer than the previously used strtotime() API CHANGE Removed DMYCalendarDateField and CalendarDateField, use DateField with setConfig('showcalendar') API CHANGE Removed CompositeDateField, DMYDateField, use DateField with setConfig('dmyfields') API CHANGE Removed DropdownTimeField, use TimeField with setConfig('showdropdown') API CHANGE Removed PopupDateTimeField, use DatetimeField API CHANGE Changed 'date', 'month' and 'year' HTML field names to lowercase in DMYDateField API CHANGE Removed support for ambiguous date formats in DateField, e.g. '06/03/03'. Use DateField->setConfig('dateformat', '<format>') to revert to this behaviour. API CHANGE Removed flag from DateField, CalendarDateField etc., use DateField->setConfig('min') and DateField->setConfig('max') ENHANCEMENT Using Zend_Date for DateField and TimeField, with more robust date handling, starting localization support. Set globally via i18n::set_locale(), or for a field instance through setLocale(). Note: Javascript validation is not localized yet. (from r99360) git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@102859 467b73ca-7a2a-4603-9d3b-597d59a354a9
2010-04-14 06:38:40 +02:00
/**
* Form field used for editing date time string
*
2017-04-20 05:08:44 +02:00
* # Localization
*
* See {@link DateField}
*
FEATURE New DatetimeField class (form field wrapper composed of DateField andTimeField) FEATURE New DateField and TimeField form classes with more consistent API and easier localization API CHANGE Date/time parsing in DateField, TimeField and DatetimeField defaults to i18n::get_locale() ('en_US') instead of using en_NZ/en_GB specific parsing. Use i18n::set_locale('en_NZ') in mysite/_config.php to revert to old behaviour. API CHANGE constructor parameter in TimeField needs to be in ISO date notation (not PHP's date()) API CHANGE TimeField, DateField and related subclasses use Zend_Date for date parsing, meaning they're stricer than the previously used strtotime() API CHANGE Removed DMYCalendarDateField and CalendarDateField, use DateField with setConfig('showcalendar') API CHANGE Removed CompositeDateField, DMYDateField, use DateField with setConfig('dmyfields') API CHANGE Removed DropdownTimeField, use TimeField with setConfig('showdropdown') API CHANGE Removed PopupDateTimeField, use DatetimeField API CHANGE Changed 'date', 'month' and 'year' HTML field names to lowercase in DMYDateField API CHANGE Removed support for ambiguous date formats in DateField, e.g. '06/03/03'. Use DateField->setConfig('dateformat', '<format>') to revert to this behaviour. API CHANGE Removed flag from DateField, CalendarDateField etc., use DateField->setConfig('min') and DateField->setConfig('max') ENHANCEMENT Using Zend_Date for DateField and TimeField, with more robust date handling, starting localization support. Set globally via i18n::set_locale(), or for a field instance through setLocale(). Note: Javascript validation is not localized yet. (from r99360) git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@102859 467b73ca-7a2a-4603-9d3b-597d59a354a9
2010-04-14 06:38:40 +02:00
* # Configuration
2014-08-15 08:53:05 +02:00
*
* - "timezone": Set a different timezone for viewing. {@link dataValue()} will still save
* the time in PHP's default timezone (date_default_timezone_get()), its only a view setting.
FEATURE New DatetimeField class (form field wrapper composed of DateField andTimeField) FEATURE New DateField and TimeField form classes with more consistent API and easier localization API CHANGE Date/time parsing in DateField, TimeField and DatetimeField defaults to i18n::get_locale() ('en_US') instead of using en_NZ/en_GB specific parsing. Use i18n::set_locale('en_NZ') in mysite/_config.php to revert to old behaviour. API CHANGE constructor parameter in TimeField needs to be in ISO date notation (not PHP's date()) API CHANGE TimeField, DateField and related subclasses use Zend_Date for date parsing, meaning they're stricer than the previously used strtotime() API CHANGE Removed DMYCalendarDateField and CalendarDateField, use DateField with setConfig('showcalendar') API CHANGE Removed CompositeDateField, DMYDateField, use DateField with setConfig('dmyfields') API CHANGE Removed DropdownTimeField, use TimeField with setConfig('showdropdown') API CHANGE Removed PopupDateTimeField, use DatetimeField API CHANGE Changed 'date', 'month' and 'year' HTML field names to lowercase in DMYDateField API CHANGE Removed support for ambiguous date formats in DateField, e.g. '06/03/03'. Use DateField->setConfig('dateformat', '<format>') to revert to this behaviour. API CHANGE Removed flag from DateField, CalendarDateField etc., use DateField->setConfig('min') and DateField->setConfig('max') ENHANCEMENT Using Zend_Date for DateField and TimeField, with more robust date handling, starting localization support. Set globally via i18n::set_locale(), or for a field instance through setLocale(). Note: Javascript validation is not localized yet. (from r99360) git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@102859 467b73ca-7a2a-4603-9d3b-597d59a354a9
2010-04-14 06:38:40 +02:00
*/
class DatetimeField extends TextField
2016-11-29 00:31:16 +01:00
{
/**
* @var bool
*/
protected $html5 = true;
/**
* Override locale. If empty will default to current locale
*
* @var string
*/
protected $locale = null;
/**
* Min date time
*
* @var string ISO 8601 date time for min date time
*/
protected $minDatetime = null;
/**
* Max date time
*
* @var string ISO 860 date time for max date time
*/
protected $maxDatetime = null;
/**
* Override date format. If empty will default to that used by the current locale.
*
* @var null
*/
protected $datetimeFormat = null;
/**
* Length of this date (full, short, etc).
*
* @see http://php.net/manual/en/class.intldateformatter.php#intl.intldateformatter-constants
* @var int
*/
protected $dateLength = null;
/**
* Length of this time (full, short, etc).
*
* @see http://php.net/manual/en/class.intldateformatter.php#intl.intldateformatter-constants
* @var int
2016-11-29 00:31:16 +01:00
*/
protected $timeLength = null;
2016-11-29 00:31:16 +01:00
/**
* Unparsed value, used exclusively for comparing with internal value
* to detect invalid values.
*
* @var mixed
2016-11-29 00:31:16 +01:00
*/
protected $rawValue = null;
2016-11-29 00:31:16 +01:00
protected $schemaDataType = FormField::SCHEMA_DATA_TYPE_DATETIME;
/**
* Date time order
*
* @var string
2016-11-29 00:31:16 +01:00
*/
protected $dateTimeOrder = '{date} {time}';
2016-11-29 00:31:16 +01:00
public function __construct($name, $title = null, $value = "")
{
parent::__construct($name, $title, $value);
}
public function setForm($form)
{
parent::setForm($form);
return $this;
}
public function setName($name)
{
parent::setName($name);
return $this;
}
public function getAttributes()
{
$attributes = parent::getAttributes();
$attributes['lang'] = i18n::convert_rfc1766($this->getLocale());
if ($this->getHTML5()) {
$attributes['type'] = 'datetime-local';
$attributes['min'] = $this->getMinDatetime();
$attributes['max'] = $this->getMaxDatetime();
}
return $attributes;
}
2017-04-12 12:39:23 +02:00
public function getSchemaDataDefaults()
{
$defaults = parent::getSchemaDataDefaults();
return array_merge($defaults, [
'html5' => $this->getHTML5()
]);
}
public function Type()
{
return 'text datetime';
}
public function getHTML5()
{
return $this->html5;
}
public function setHTML5($bool)
{
$this->html5 = $bool;
2016-11-29 00:31:16 +01:00
return $this;
}
/**
* Assign value posted from form submission
*
* @param mixed $value
* @param mixed $data
* @return $this
2016-11-29 00:31:16 +01:00
*/
public function setSubmittedValue($value, $data = null)
2016-11-29 00:31:16 +01:00
{
// Save raw value for later validation
$this->rawValue = $value;
// Null case
if (!$value) {
$this->value = null;
return $this;
}
// Parse from submitted value
$this->value = $this->localisedToISO8601($value);
2017-04-20 05:08:44 +02:00
return $this;
}
/**
* Convert date localised in the current locale to ISO 8601 date
*
* @param string $date
* @return string The formatted date, or null if not a valid date
*/
public function localisedToISO8601($datetime)
{
if (!$datetime) {
return null;
}
$fromFormatter = $this->getFormatter();
$toFormatter = $this->getISO8601Formatter();
$timestamp = $fromFormatter->parse($datetime);
if ($timestamp === false) {
return null;
}
return $toFormatter->format($timestamp) ?: null;
}
/**
* Get date formatter with the standard locale / date format
*
* @throws \LogicException
* @return IntlDateFormatter
*/
protected function getFormatter()
{
if ($this->getHTML5() && $this->datetimeFormat && $this->datetimeFormat !== DBDatetime::ISO_DATETIME) {
throw new \LogicException(
'Please opt-out of HTML5 processing of ISO 8601 dates via setHTML5(false) if using setDatetimeFormat()'
);
}
if ($this->getHTML5() && $this->dateLength) {
throw new \LogicException(
'Please opt-out of HTML5 processing of ISO 8601 dates via setHTML5(false) if using setDateLength()'
);
}
if ($this->getHTML5() && $this->locale) {
throw new \LogicException(
'Please opt-out of HTML5 processing of ISO 8601 dates via setHTML5(false) if using setLocale()'
);
}
2016-11-29 00:31:16 +01:00
$formatter = IntlDateFormatter::create(
$this->getLocale(),
$this->getDateLength(),
$this->getTimeLength(),
$this->getTimezone()
);
if ($this->getHTML5()) {
// Browsers expect ISO 8601 dates, localisation is handled on the client
$formatter->setPattern(DBDatetime::ISO_DATETIME);
} elseif ($this->datetimeFormat) {
2017-04-20 05:08:44 +02:00
// Don't invoke getDatetimeFormat() directly to avoid infinite loop
$ok = $formatter->setPattern($this->datetimeFormat);
if (!$ok) {
throw new InvalidArgumentException("Invalid date format {$this->datetimeFormat}");
}
}
return $formatter;
2016-11-29 00:31:16 +01:00
}
/**
* Get date format in CLDR standard format
*
* This can be set explicitly. If not, this will be generated from the current locale
* with the current date length.
*
* @see http://userguide.icu-project.org/formatparse/datetime#TOC-Date-Field-Symbol-Table
*/
2017-04-20 05:08:44 +02:00
public function getDatetimeFormat()
{
if ($this->getHTML5()) {
// Browsers expect ISO 8601 dates, localisation is handled on the client
$this->setDatetimeFormat(DBDatetime::ISO_DATETIME);
}
if ($this->datetimeFormat) {
return $this->datetimeFormat;
}
// Get from locale
return $this->getFormatter()->getPattern();
}
/**
* Set date format in CLDR standard format.
* Only applicable with {@link setHTML5(false)}.
*
* @see http://userguide.icu-project.org/formatparse/datetime#TOC-Date-Field-Symbol-Table
* @param string $format
* @return $this
*/
public function setDatetimeFormat($format)
{
$this->datetimeFormat = $format;
return $this;
}
2016-11-29 00:31:16 +01:00
/**
* Get formatter for converting to the target timezone, if timezone is set
* Can return null if no timezone set
*
* @return IntlDateFormatter|null
2016-11-29 00:31:16 +01:00
*/
protected function getTimezoneFormatter()
2016-11-29 00:31:16 +01:00
{
$timezone = $this->getTimezone();
if (!$timezone) {
return null;
}
// Build new formatter with the altered timezone
$formatter = clone $this->getISO8601Formatter();
$formatter->setTimeZone($timezone);
return $formatter;
2016-11-29 00:31:16 +01:00
}
/**
* Get a date formatter for the ISO 8601 format
2016-11-29 00:31:16 +01:00
*
* @return IntlDateFormatter
*/
protected function getISO8601Formatter()
{
$formatter = IntlDateFormatter::create(
2017-02-22 04:14:53 +01:00
i18n::config()->uninherited('default_locale'),
IntlDateFormatter::MEDIUM,
IntlDateFormatter::MEDIUM,
date_default_timezone_get() // Default to server timezone
);
$formatter->setLenient(false);
// CLDR iso8601 date.
// Note we omit timezone from this format, and we assume server TZ always.
$formatter->setPattern('y-MM-dd HH:mm:ss');
return $formatter;
}
/**
* Assign value from iso8601 string
2016-11-29 00:31:16 +01:00
*
* @param mixed $value
* @param mixed $data
2016-11-29 00:31:16 +01:00
* @return $this
*/
public function setValue($value, $data = null)
2016-11-29 00:31:16 +01:00
{
2017-04-12 12:39:23 +02:00
// Save raw value for later validation
$this->rawValue = $value;
// Empty value
if (empty($value)) {
2016-11-29 00:31:16 +01:00
$this->value = null;
return $this;
}
if (is_array($value)) {
throw new InvalidArgumentException("Use setSubmittedValue to assign by array");
};
// Validate iso 8601 date
// If invalid, assign for later validation failure
$isoFormatter = $this->getISO8601Formatter();
$timestamp = $isoFormatter->parse($value);
if ($timestamp === false) {
return $this;
2016-11-29 00:31:16 +01:00
}
// Cleanup date
$value = $isoFormatter->format($timestamp);
// Save value
$this->value = $value;
// Shift iso date into timezone before assignment to subfields
$timezoneFormatter = $this->getTimezoneFormatter();
if ($timezoneFormatter) {
$value = $timezoneFormatter->format($timestamp);
}
2016-11-29 00:31:16 +01:00
return $this;
}
public function Value()
{
return $this->iso8601ToLocalised($this->value);
}
/**
* Convert an ISO 8601 localised date into the format specified by the
* current date format.
*
* @param string $date
* @return string The formatted date, or null if not a valid date
*/
public function iso8601ToLocalised($datetime)
{
$datetime = $this->tidyISO8601($datetime);
if (!$datetime) {
return null;
}
$fromFormatter = $this->getISO8601Formatter();
$toFormatter = $this->getFormatter();
$timestamp = $fromFormatter->parse($datetime);
if ($timestamp === false) {
return null;
}
return $toFormatter->format($timestamp) ?: null;
}
/**
* Tidy up iso8601-ish date, or approximation
*
* @param string $date Date in iso8601 or approximate form
* @return string iso8601 date, or null if not valid
*/
public function tidyISO8601($datetime)
{
if (!$datetime) {
return null;
}
// Re-run through formatter to tidy up (e.g. remove time component)
$formatter = $this->getISO8601Formatter();
$timestamp = $formatter->parse($datetime);
if ($timestamp === false) {
// Fallback to strtotime
$timestamp = strtotime($datetime, DBDatetime::now()->getTimestamp());
if ($timestamp === false) {
return null;
}
}
return $formatter->format($timestamp);
}
/**
* Get length of the date format to use. One of:
*
* - IntlDateFormatter::SHORT
* - IntlDateFormatter::MEDIUM
* - IntlDateFormatter::LONG
* - IntlDateFormatter::FULL
*
* @see http://php.net/manual/en/class.intldateformatter.php#intl.intldateformatter-constants
* @return int
*/
public function getDateLength()
{
if ($this->dateLength) {
return $this->dateLength;
}
return IntlDateFormatter::MEDIUM;
}
/**
* Get length of the date format to use.
* Only applicable with {@link setHTML5(false)}.
*
* @see http://php.net/manual/en/class.intldateformatter.php#intl.intldateformatter-constants
*
* @param int $length
* @return $this
*/
public function setDateLength($length)
{
$this->dateLength = $length;
return $this;
}
/**
* Get length of the date format to use. One of:
*
* - IntlDateFormatter::SHORT
* - IntlDateFormatter::MEDIUM
* - IntlDateFormatter::LONG
* - IntlDateFormatter::FULL
*
* @see http://php.net/manual/en/class.intldateformatter.php#intl.intldateformatter-constants
* @return int
*/
public function getTimeLength()
{
if ($this->timeLength) {
return $this->timeLength;
2016-11-29 00:31:16 +01:00
}
return IntlDateFormatter::MEDIUM;
}
2016-11-29 00:31:16 +01:00
/**
* Get length of the date format to use.
* Only applicable with {@link setHTML5(false)}.
*
* @see http://php.net/manual/en/class.intldateformatter.php#intl.intldateformatter-constants
*
* @param int $length
* @return $this
*/
public function setTimeLength($length)
{
$this->timeLength = $length;
return $this;
2016-11-29 00:31:16 +01:00
}
public function setDisabled($bool)
{
parent::setDisabled($bool);
return $this;
}
public function setReadonly($bool)
{
parent::setReadonly($bool);
return $this;
}
/**
* Set default locale for this field. If omitted will default to the current locale.
*
* @param string $locale
* @return $this
2016-11-29 00:31:16 +01:00
*/
public function setLocale($locale)
2016-11-29 00:31:16 +01:00
{
$this->locale = $locale;
return $this;
2016-11-29 00:31:16 +01:00
}
/**
* Get locale for this field
*
* @return string
2016-11-29 00:31:16 +01:00
*/
public function getLocale()
2016-11-29 00:31:16 +01:00
{
return $this->locale ?: i18n::get_locale();
2016-11-29 00:31:16 +01:00
}
/**
* @return string
2016-11-29 00:31:16 +01:00
*/
public function getMinDatetime()
2016-11-29 00:31:16 +01:00
{
return $this->minDatetime;
2016-11-29 00:31:16 +01:00
}
/**
* @param string $minDatetime
* @return $this
2016-11-29 00:31:16 +01:00
*/
public function setMinDatetime($minDatetime)
2016-11-29 00:31:16 +01:00
{
$this->minDatetime = $this->tidyISO8601($minDatetime);
return $this;
2016-11-29 00:31:16 +01:00
}
/**
* @return string
2016-11-29 00:31:16 +01:00
*/
public function getMaxDatetime()
2016-11-29 00:31:16 +01:00
{
return $this->maxDatetime;
2016-11-29 00:31:16 +01:00
}
/**
* @param string $maxDatetime
* @return $this
2016-11-29 00:31:16 +01:00
*/
public function setMaxDatetime($maxDatetime)
2016-11-29 00:31:16 +01:00
{
$this->maxDatetime = $this->tidyISO8601($maxDatetime);
return $this;
2016-11-29 00:31:16 +01:00
}
/**
* @param Validator $validator
* @return bool
*/
2016-11-29 00:31:16 +01:00
public function validate($validator)
{
// Don't validate empty fields
if (empty($this->rawValue)) {
return true;
}
2016-11-29 00:31:16 +01:00
// We submitted a value, but it couldn't be parsed
if (empty($this->value)) {
$validator->validationError(
$this->name,
_t(
'DateField.VALIDDATEFORMAT2',
"Please enter a valid date format ({format})",
2017-04-20 05:08:44 +02:00
['format' => $this->getDatetimeFormat()]
)
);
return false;
}
// Check min date
$min = $this->getMinDatetime();
if ($min) {
$oops = strtotime($this->value) < strtotime($min);
if ($oops) {
$validator->validationError(
$this->name,
_t(
'DateField.VALIDDATEMINDATE',
"Your date has to be newer or matching the minimum allowed date ({date})",
['date' => $this->iso8601ToLocalised($min)]
)
);
return false;
}
}
// Check max date
$max = $this->getMaxDatetime();
if ($max) {
$oops = strtotime($this->value) > strtotime($max);
if ($oops) {
$validator->validationError(
$this->name,
_t(
'DateField.VALIDDATEMAXDATE',
"Your date has to be older or matching the maximum allowed date ({date})",
['date' => $this->iso8601ToLocalised($max)]
)
);
return false;
}
}
return true;
2016-11-29 00:31:16 +01:00
}
public function performReadonlyTransformation()
{
$field = clone $this;
$field->setReadonly(true);
return $field;
}
/**
* @return string
*/
public function getTimezone()
{
return $this->timezone;
}
/**
* Custom timezone
*
* @var string
*/
protected $timezone = null;
/**
* @param string $timezone
* @return $this
*/
public function setTimezone($timezone)
{
if ($this->value && $timezone !== $this->timezone) {
throw new \BadMethodCallException("Can't change timezone after setting a value");
}
// Note: DateField has no timezone option, and TimeField::setTimezone
// should be ignored
$this->timezone = $timezone;
return $this;
}
FEATURE New DatetimeField class (form field wrapper composed of DateField andTimeField) FEATURE New DateField and TimeField form classes with more consistent API and easier localization API CHANGE Date/time parsing in DateField, TimeField and DatetimeField defaults to i18n::get_locale() ('en_US') instead of using en_NZ/en_GB specific parsing. Use i18n::set_locale('en_NZ') in mysite/_config.php to revert to old behaviour. API CHANGE constructor parameter in TimeField needs to be in ISO date notation (not PHP's date()) API CHANGE TimeField, DateField and related subclasses use Zend_Date for date parsing, meaning they're stricer than the previously used strtotime() API CHANGE Removed DMYCalendarDateField and CalendarDateField, use DateField with setConfig('showcalendar') API CHANGE Removed CompositeDateField, DMYDateField, use DateField with setConfig('dmyfields') API CHANGE Removed DropdownTimeField, use TimeField with setConfig('showdropdown') API CHANGE Removed PopupDateTimeField, use DatetimeField API CHANGE Changed 'date', 'month' and 'year' HTML field names to lowercase in DMYDateField API CHANGE Removed support for ambiguous date formats in DateField, e.g. '06/03/03'. Use DateField->setConfig('dateformat', '<format>') to revert to this behaviour. API CHANGE Removed flag from DateField, CalendarDateField etc., use DateField->setConfig('min') and DateField->setConfig('max') ENHANCEMENT Using Zend_Date for DateField and TimeField, with more robust date handling, starting localization support. Set globally via i18n::set_locale(), or for a field instance through setLocale(). Note: Javascript validation is not localized yet. (from r99360) git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@102859 467b73ca-7a2a-4603-9d3b-597d59a354a9
2010-04-14 06:38:40 +02:00
}