diff --git a/src/Forms/DateField.php b/src/Forms/DateField.php index 33e4a9c22..9d698e012 100644 --- a/src/Forms/DateField.php +++ b/src/Forms/DateField.php @@ -194,7 +194,7 @@ class DateField extends TextField } // Get from locale - return $this->getFormatter()->getPattern(); + return $this->getFrontendFormatter()->getPattern(); } /** @@ -217,7 +217,7 @@ class DateField extends TextField * @throws \LogicException * @return IntlDateFormatter */ - protected function getFormatter() + protected function getFrontendFormatter() { if ($this->getHTML5() && $this->dateFormat && $this->dateFormat !== DBDate::ISO_DATE) { throw new \LogicException( @@ -261,7 +261,7 @@ class DateField extends TextField * * @return IntlDateFormatter */ - protected function getISO8601Formatter() + protected function getInternalFormatter() { $locale = i18n::config()->uninherited('default_locale'); $formatter = IntlDateFormatter::create( @@ -327,10 +327,19 @@ class DateField extends TextField } // Parse from submitted value - $this->value = $this->localisedToISO8601($value); + $this->value = $this->frontendToInternal($value); return $this; } + /** + * Assign value based on {@link $datetimeFormat}, which might be localised. + * + * When $html5=true, assign value from ISO 8601 string. + * + * @param mixed $value + * @param mixed $data + * @return $this + */ public function setValue($value, $data = null) { // Save raw value for later validation @@ -343,13 +352,13 @@ class DateField extends TextField } // Re-run through formatter to tidy up (e.g. remove time component) - $this->value = $this->tidyISO8601($value); + $this->value = $this->tidyInternal($value); return $this; } public function Value() { - return $this->iso8601ToLocalised($this->value); + return $this->internalToFrontend($this->value); } public function performReadonlyTransformation() @@ -394,7 +403,7 @@ class DateField extends TextField _t( 'DateField.VALIDDATEMINDATE', "Your date has to be newer or matching the minimum allowed date ({date})", - ['date' => $this->iso8601ToLocalised($min)] + ['date' => $this->internalToFrontend($min)] ) ); return false; @@ -411,7 +420,7 @@ class DateField extends TextField _t( 'DateField.VALIDDATEMAXDATE', "Your date has to be older or matching the maximum allowed date ({date})", - ['date' => $this->iso8601ToLocalised($max)] + ['date' => $this->internalToFrontend($max)] ) ); return false; @@ -466,7 +475,7 @@ class DateField extends TextField */ public function setMinDate($minDate) { - $this->minDate = $this->tidyISO8601($minDate); + $this->minDate = $this->tidyInternal($minDate); return $this; } @@ -484,23 +493,24 @@ class DateField extends TextField */ public function setMaxDate($maxDate) { - $this->maxDate = $this->tidyISO8601($maxDate); + $this->maxDate = $this->tidyInternal($maxDate); return $this; } /** - * Convert date localised in the current locale to ISO 8601 date + * Convert frontend date to the internal representation (ISO 8601). + * The frontend date is also in ISO 8601 when $html5=true. * * @param string $date * @return string The formatted date, or null if not a valid date */ - public function localisedToISO8601($date) + protected function frontendToInternal($date) { if (!$date) { return null; } - $fromFormatter = $this->getFormatter(); - $toFormatter = $this->getISO8601Formatter(); + $fromFormatter = $this->getFrontendFormatter(); + $toFormatter = $this->getInternalFormatter(); $timestamp = $fromFormatter->parse($date); if ($timestamp === false) { return null; @@ -509,20 +519,21 @@ class DateField extends TextField } /** - * Convert an ISO 8601 localised date into the format specified by the - * current date format. + * Convert the internal date representation (ISO 8601) to a format used by the frontend, + * as defined by {@link $dateFormat}. With $html5=true, the frontend date will also be + * in ISO 8601. * * @param string $date * @return string The formatted date, or null if not a valid date */ - public function iso8601ToLocalised($date) + protected function internalToFrontend($date) { - $date = $this->tidyISO8601($date); + $date = $this->tidyInternal($date); if (!$date) { return null; } - $fromFormatter = $this->getISO8601Formatter(); - $toFormatter = $this->getFormatter(); + $fromFormatter = $this->getInternalFormatter(); + $toFormatter = $this->getFrontendFormatter(); $timestamp = $fromFormatter->parse($date); if ($timestamp === false) { return null; @@ -531,18 +542,19 @@ class DateField extends TextField } /** - * Tidy up iso8601-ish date, or approximation + * Tidy up the internal date representation (ISO 8601), + * and fall back to strtotime() if there's parsing errors. * - * @param string $date Date in iso8601 or approximate form - * @return string iso8601 date, or null if not valid + * @param string $date Date in ISO 8601 or approximate form + * @return string ISO 8601 date, or null if not valid */ - public function tidyISO8601($date) + protected function tidyInternal($date) { if (!$date) { return null; } // Re-run through formatter to tidy up (e.g. remove time component) - $formatter = $this->getISO8601Formatter(); + $formatter = $this->getInternalFormatter(); $timestamp = $formatter->parse($date); if ($timestamp === false) { // Fallback to strtotime diff --git a/src/Forms/DatetimeField.php b/src/Forms/DatetimeField.php index 9003b4f58..c551f1a31 100644 --- a/src/Forms/DatetimeField.php +++ b/src/Forms/DatetimeField.php @@ -156,25 +156,25 @@ class DatetimeField extends TextField } // Parse from submitted value - $this->value = $this->localisedToISO8601($value); + $this->value = $this->frontendToInternal($value); return $this; } /** - * Convert date localised in the current locale to ISO 8601 date. - * Note that "localised" could also mean ISO format when $html5=true. + * Convert frontend date to the internal representation (ISO 8601). + * The frontend date is also in ISO 8601 when $html5=true. * * @param string $datetime * @return string The formatted date, or null if not a valid date */ - public function localisedToISO8601($datetime) + public function frontendToInternal($datetime) { if (!$datetime) { return null; } - $fromFormatter = $this->getFormatter(); - $toFormatter = $this->getISO8601Formatter(); + $fromFormatter = $this->getFrontendFormatter(); + $toFormatter = $this->getInternalFormatter(); // Try to parse time with seconds $timestamp = $fromFormatter->parse($datetime); @@ -198,7 +198,7 @@ class DatetimeField extends TextField * @throws \LogicException * @return IntlDateFormatter */ - protected function getFormatter() + protected function getFrontendFormatter() { if ($this->getHTML5() && $this->datetimeFormat && $this->datetimeFormat !== DBDatetime::ISO_DATETIME_NORMALISED) { throw new \LogicException( @@ -254,7 +254,7 @@ class DatetimeField extends TextField } // Get from locale - return $this->getFormatter()->getPattern(); + return $this->getFrontendFormatter()->getPattern(); } /** @@ -285,7 +285,7 @@ class DatetimeField extends TextField } // Build new formatter with the altered timezone - $formatter = clone $this->getISO8601Formatter(); + $formatter = clone $this->getInternalFormatter(); $formatter->setTimeZone($timezone); // ISO8601 date with a standard "T" separator (W3C standard). @@ -300,7 +300,7 @@ class DatetimeField extends TextField * * @return IntlDateFormatter */ - protected function getISO8601Formatter() + protected function getInternalFormatter() { $formatter = IntlDateFormatter::create( i18n::config()->uninherited('default_locale'), @@ -311,14 +311,13 @@ class DatetimeField extends TextField $formatter->setLenient(false); // Note we omit timezone from this format, and we always assume server TZ - // ISO8601 date with a standard "T" separator (W3C standard). $formatter->setPattern(DBDatetime::ISO_DATETIME_NORMALISED); return $formatter; } /** - * Assign value based on {@link $datetimeFormat}. + * Assign value based on {@link $datetimeFormat}, which might be localised. * * When $html5=true, assign value from ISO 8601 normalised string (with a "T" separator). * Falls back to an ISO 8601 string (with a whitespace separator). @@ -340,14 +339,14 @@ class DatetimeField extends TextField // Validate iso 8601 date // If invalid, assign for later validation failure - $isoFormatter = $this->getISO8601Formatter(); - $timestamp = $isoFormatter->parse($value); + $internalFormatter = $this->getInternalFormatter(); + $timestamp = $internalFormatter->parse($value); // Retry without "T" separator if (!$timestamp) { - $isoFallbackFormatter = $this->getISO8601Formatter(); - $isoFallbackFormatter->setPattern(DBDatetime::ISO_DATETIME); - $timestamp = $isoFallbackFormatter->parse($value); + $fallbackFormatter = $this->getInternalFormatter(); + $fallbackFormatter->setPattern(DBDatetime::ISO_DATETIME); + $timestamp = $fallbackFormatter->parse($value); } if ($timestamp === false) { @@ -355,7 +354,7 @@ class DatetimeField extends TextField } // Cleanup date - $value = $isoFormatter->format($timestamp); + $value = $internalFormatter->format($timestamp); // Save value $this->value = $value; @@ -363,14 +362,27 @@ class DatetimeField extends TextField return $this; } + /** + * Returns the frontend representation of the field value, + * according to the defined {@link dateFormat}. + * With $html5=true, this will be in ISO 8601 format. + * + * @return string + */ public function Value() { - return $this->iso8601ToLocalised($this->value); + return $this->internalToFrontend($this->value); } + /** + * Returns the field value in the internal representation (ISO 8601), + * suitable for insertion into the data object. + * + * @return string + */ public function dataValue() { - return $this->iso8601ToDataValue($this->value); + return $this->internalToDataValue($this->value); } /** @@ -379,10 +391,10 @@ class DatetimeField extends TextField * @param string $datetime * @return string The formatted date and time, or null if not a valid date and time */ - public function iso8601ToDataValue($datetime) + public function internalToDataValue($datetime) { - $fromFormatter = $this->getISO8601Formatter(); - $toFormatter = $this->getFormatter(); + $fromFormatter = $this->getInternalFormatter(); + $toFormatter = $this->getFrontendFormatter(); // Set default timezone (avoid shifting data values into user timezone) $toFormatter->setTimezone(date_default_timezone_get()); @@ -398,19 +410,21 @@ class DatetimeField extends TextField } /** - * Convert an ISO 8601 localised datetime into the format specified by the current format. + * Convert the internal date representation (ISO 8601) to a format used by the frontend, + * as defined by {@link $dateFormat}. With $html5=true, the frontend date will also be + * in ISO 8601. * * @param string $datetime * @return string The formatted date and time, or null if not a valid date and time */ - public function iso8601ToLocalised($datetime) + public function internalToFrontend($datetime) { - $datetime = $this->tidyISO8601($datetime); + $datetime = $this->tidyInternal($datetime); if (!$datetime) { return null; } - $fromFormatter = $this->getISO8601Formatter(); - $toFormatter = $this->getFormatter(); + $fromFormatter = $this->getInternalFormatter(); + $toFormatter = $this->getFrontendFormatter(); $timestamp = $fromFormatter->parse($datetime); if ($timestamp === false) { return null; @@ -420,18 +434,19 @@ class DatetimeField extends TextField } /** - * Tidy up iso8601-ish date, or approximation + * Tidy up the internal date representation (ISO 8601), + * and fall back to strtotime() if there's parsing errors. * * @param string $date Date in ISO 8601 or approximate form - * @return string iso8601 date, or null if not valid + * @return string ISO 8601 date, or null if not valid */ - public function tidyISO8601($datetime) + public function tidyInternal($datetime) { if (!$datetime) { return null; } // Re-run through formatter to tidy up (e.g. remove time component) - $formatter = $this->getISO8601Formatter(); + $formatter = $this->getInternalFormatter(); $timestamp = $formatter->parse($datetime); if ($timestamp === false) { // Fallback to strtotime @@ -559,7 +574,7 @@ class DatetimeField extends TextField */ public function setMinDatetime($minDatetime) { - $this->minDatetime = $this->tidyISO8601($minDatetime); + $this->minDatetime = $this->tidyInternal($minDatetime); return $this; } @@ -577,7 +592,7 @@ class DatetimeField extends TextField */ public function setMaxDatetime($maxDatetime) { - $this->maxDatetime = $this->tidyISO8601($maxDatetime); + $this->maxDatetime = $this->tidyInternal($maxDatetime); return $this; } @@ -615,7 +630,7 @@ class DatetimeField extends TextField _t( 'DatetimeField.VALIDDATETIMEMINDATE', "Your date has to be newer or matching the minimum allowed date and time ({datetime})", - ['datetime' => $this->iso8601ToLocalised($min)] + ['datetime' => $this->internalToFrontend($min)] ) ); return false; @@ -632,7 +647,7 @@ class DatetimeField extends TextField _t( 'DatetimeField.VALIDDATEMAXDATETIME', "Your date has to be older or matching the maximum allowed date and time ({datetime})", - ['datetime' => $this->iso8601ToLocalised($max)] + ['datetime' => $this->internalToFrontend($max)] ) ); return false; diff --git a/src/Forms/TimeField.php b/src/Forms/TimeField.php index ff430031b..4cfa187ac 100644 --- a/src/Forms/TimeField.php +++ b/src/Forms/TimeField.php @@ -103,7 +103,7 @@ class TimeField extends TextField } // Get from locale - return $this->getFormatter()->getPattern(); + return $this->getFrontendFormatter()->getPattern(); } /** @@ -159,7 +159,7 @@ class TimeField extends TextField * * @return IntlDateFormatter */ - protected function getFormatter() + protected function getFrontendFormatter() { if ($this->getHTML5() && $this->timeFormat && $this->timeFormat !== DBTime::ISO_TIME) { throw new \LogicException( @@ -204,7 +204,7 @@ class TimeField extends TextField * * @return IntlDateFormatter */ - protected function getISO8601Formatter() + protected function getInternalFormatter() { $formatter = IntlDateFormatter::create( i18n::config()->uninherited('default_locale'), @@ -260,7 +260,7 @@ class TimeField extends TextField $this->rawValue = $value; // Parse from submitted value - $this->value = $this->localisedToISO8601($value); + $this->value = $this->frontendToInternal($value); return $this; } @@ -283,13 +283,13 @@ class TimeField extends TextField } // Re-run through formatter to tidy up (e.g. remove date component) - $this->value = $this->tidyISO8601($value); + $this->value = $this->tidyInternal($value); return $this; } public function Value() { - $localised = $this->iso8601ToLocalised($this->value); + $localised = $this->internalToFrontend($this->value); if ($localised) { return $localised; } @@ -305,7 +305,7 @@ class TimeField extends TextField */ public function getMidnight() { - $formatter = $this->getFormatter(); + $formatter = $this->getFrontendFormatter(); $timestamp = $this->withTimezone($this->getTimezone(), function () { return strtotime('midnight'); }); @@ -375,18 +375,19 @@ class TimeField extends TextField } /** - * Convert time localised in the current locale to ISO 8601 time + * Convert frontend time to the internal representation (ISO 8601). + * The frontend time is also in ISO 8601 when $html5=true. * * @param string $time * @return string The formatted time, or null if not a valid time */ - public function localisedToISO8601($time) + protected function frontendToInternal($time) { if (!$time) { return null; } - $fromFormatter = $this->getFormatter(); - $toFormatter = $this->getISO8601Formatter(); + $fromFormatter = $this->getFrontendFormatter(); + $toFormatter = $this->getInternalFormatter(); $timestamp = $fromFormatter->parse($time); // Try to parse time without seconds, since that's a valid HTML5 submission format @@ -405,19 +406,21 @@ class TimeField extends TextField } /** - * Format iso time to localised form + * Convert the internal time representation (ISO 8601) to a format used by the frontend, + * as defined by {@link $timeFormat}. With $html5=true, the frontend time will also be + * in ISO 8601. * * @param string $time * @return string */ - public function iso8601ToLocalised($time) + protected function internalToFrontend($time) { - $time = $this->tidyISO8601($time); + $time = $this->tidyInternal($time); if (!$time) { return null; } - $fromFormatter = $this->getISO8601Formatter(); - $toFormatter = $this->getFormatter(); + $fromFormatter = $this->getInternalFormatter(); + $toFormatter = $this->getFrontendFormatter(); $timestamp = $fromFormatter->parse($time); if ($timestamp === false) { return null; @@ -428,18 +431,19 @@ class TimeField extends TextField /** - * Tidy up iso8601-ish time, or approximation + * Tidy up the internal time representation (ISO 8601), + * and fall back to strtotime() if there's parsing errors. * - * @param string $time Time in iso8601 or approximate form - * @return string iso8601 time, or null if not valid + * @param string $time Time in ISO 8601 or approximate form + * @return string ISO 8601 time, or null if not valid */ - public function tidyISO8601($time) + protected function tidyInternal($time) { if (!$time) { return null; } // Re-run through formatter to tidy up (e.g. remove date component) - $formatter = $this->getISO8601Formatter(); + $formatter = $this->getInternalFormatter(); $timestamp = $formatter->parse($time); if ($timestamp === false) { // Fallback to strtotime diff --git a/tests/php/Forms/DateFieldTest.php b/tests/php/Forms/DateFieldTest.php index 676ae58eb..7843a02c0 100644 --- a/tests/php/Forms/DateFieldTest.php +++ b/tests/php/Forms/DateFieldTest.php @@ -94,15 +94,22 @@ class DateFieldTest extends SapphireTest $this->assertEquals('2003-03-29', $f->dataValue()); } - public function testTidyISO8601() + public function testSetValue() { - $f = new DateField('Date', 'Date'); - $this->assertEquals(null, $f->tidyISO8601('notadate')); - $this->assertEquals('2011-01-31', $f->tidyISO8601('-1 day')); - $this->assertEquals(null, $f->tidyISO8601('29/03/2003')); + $f = (new DateField('Date', 'Date'))->setValue('notadate'); + $this->assertNull($f->Value(), 'Invalid input ignored'); + + $f = (new DateField('Date', 'Date'))->setValue('-1 day'); + $this->assertEquals($f->Value(), '2011-01-31', 'Relative dates accepted'); + + $f = (new DateField('Date', 'Date'))->setValue('2011-01-31'); + $this->assertEquals($f->Value(), '2011-01-31', 'ISO format accepted'); + + $f = (new DateField('Date', 'Date'))->setValue('2011-01-31 23:59:59'); + $this->assertEquals($f->Value(), '2011-01-31', 'ISO format with time accepted'); } - public function testSetValueWithDateString() + public function testSetValueWithLocalisedDateString() { $f = new DateField('Date', 'Date'); $f->setHTML5(false);