Commit 26bd9acb authored by webchick's avatar webchick

Issue #1802278 by KarenS, andypost: Added a Date component to core.

parent 58ed8c89
......@@ -5,6 +5,7 @@
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\Database\Database;
use Drupal\Core\Template\Attribute;
......@@ -1997,26 +1998,12 @@ function format_date($timestamp, $type = 'medium', $format = '', $timezone = NUL
break;
}
// Create a DateTime object from the timestamp.
$date_time = date_create('@' . $timestamp);
// Set the time zone for the DateTime object.
date_timezone_set($date_time, $timezones[$timezone]);
// Encode markers that should be translated. 'A' becomes '\xEF\AA\xFF'.
// xEF and xFF are invalid UTF-8 sequences, and we assume they are not in the
// input string.
// Paired backslashes are isolated to prevent errors in read-ahead evaluation.
// The read-ahead expression ensures that A matches, but not \A.
$format = preg_replace(array('/\\\\\\\\/', '/(?<!\\\\)([AaeDlMTF])/'), array("\xEF\\\\\\\\\xFF", "\xEF\\\\\$1\$1\xFF"), $format);
// Create a DrupalDateTime object from the timestamp and timezone.
$date_time = new DrupalDateTime($timestamp, $timezones[$timezone]);
// Call date_format().
$format = date_format($date_time, $format);
// Pass the langcode to _format_date_callback().
_format_date_callback(NULL, $langcode);
// Translate the marked sequences.
return preg_replace_callback('/\xEF([AaeDlMTF]?)(.*?)\xFF/', '_format_date_callback', $format);
$settings = array('langcode' => $langcode);
return $date_time->format($format, $settings);
}
/**
......@@ -4996,7 +4983,8 @@ function drupal_page_set_cache($body) {
$cache->data['headers'][$header_names[$name_lower]] = $value;
if ($name_lower == 'expires') {
// Use the actual timestamp from an Expires header if available.
$cache->expire = strtotime($value);
$date = new DrupalDateTime($value);
$cache->expire = $date->getTimestamp();
}
}
......
This diff is collapsed.
<?php
/**
* @file
* Definition of Drupal\Core\Datetime\DrupalDateTime.
*/
namespace Drupal\Core\Datetime;
use Drupal\Component\Datetime\DateTimePlus;
/**
* Extends DateTimePlus().
*
* This class extends the basic component and adds in Drupal-specific
* handling, like translation of the format() method.
*
* @see Drupal/Component/Datetime/DateTimePlus.php
*/
class DrupalDateTime extends DateTimePlus {
/**
* Constructs a date object.
*
* @param mixed $time
* A DateTime object, a date/input_time_adjusted string, a unix timestamp,
* or an array of date parts, like ('year' => 2014, 'month => 4).
* Defaults to 'now'.
* @param mixed $timezone
* PHP DateTimeZone object, string or NULL allowed.
* Defaults to NULL.
* @param string $format
* PHP date() type format for parsing the input. This is recommended
* to use things like negative years, which php's parser fails on, or
* any other specialized input with a known format. If provided the
* date will be created using the createFromFormat() method.
* Defaults to NULL.
* @see http://us3.php.net/manual/en/datetime.createfromformat.php
* @param array $settings
* - validate_format: (optional) Boolean choice to validate the
* created date using the input format. The format used in
* createFromFormat() allows slightly different values than format().
* Using an input format that works in both functions makes it
* possible to a validation step to confirm that the date created
* from a format string exactly matches the input. This option
* indicates the format can be used for validation. Defaults to TRUE.
* - langcode: (optional) String two letter language code to construct
* the locale string by the intlDateFormatter class. Used to control
* the result of the format() method if that class is available.
* Defaults to NULL.
* - country: (optional) String two letter country code to construct
* the locale string by the intlDateFormatter class. Used to control
* the result of the format() method if that class is available.
* Defaults to NULL.
* - calendar: (optional) String calendar name to use for the date.
* Defaults to DateTimePlus::CALENDAR.
* - debug: (optional) Boolean choice to leave debug values in the
* date object for debugging purposes. Defaults to FALSE.
*/
public function __construct($time = 'now', $timezone = NULL, $format = NULL, $settings = array()) {
// We can set the langcode and country using Drupal values.
$settings['langcode'] = !empty($settings['langcode']) ? $settings['langcode'] : language(LANGUAGE_TYPE_INTERFACE)->langcode;
$settings['country'] = !empty($settings['country']) ? $settings['country'] : variable_get('site_default_country');
// Instantiate the parent class.
parent::__construct($time, $timezone, $format, $settings);
}
/**
* Overrides prepareTimezone().
*
* Override basic component timezone handling to use Drupal's
* knowledge of the preferred user timezone.
*/
protected function prepareTimezone($timezone) {
$user_timezone = drupal_get_user_timezone();
if (empty($timezone) && !empty($user_timezone)) {
$timezone = $user_timezone;
}
parent::prepareTimezone($timezone);
}
/**
* Overrides format().
*
* Uses the IntlDateFormatter to display the format, if possible.
* Adds an optional array of settings that provides the information
* the IntlDateFormatter will need.
*
* @param string $format
* A format string using either PHP's date() or the
* IntlDateFormatter() format.
* @param array $settings
* - format_string_type: (optional) DateTimePlus::PHP or
* DateTimePlus::INTL. Identifies the pattern used by the format
* string. When using the Intl formatter, the format string must
* use the Intl pattern, which is different from the pattern used
* by the DateTime format function. Defaults to DateTimePlus::PHP.
* - timezone: (optional) String timezone name. Defaults to the timezone
* of the date object.
* - langcode: (optional) String two letter language code to construct the
* locale string by the intlDateFormatter class. Used to control the
* result of the format() method if that class is available. Defaults
* to NULL.
* - country: (optional) String two letter country code to construct the
* locale string by the intlDateFormatter class. Used to control the
* result of the format() method if that class is available. Defaults
* to NULL.
* - calendar: (optional) String calendar name to use for the date,
* Defaults to DateTimePlus::CALENDAR.
* - date_type: (optional) Integer date type to use in the formatter,
* defaults to IntlDateFormatter::FULL.
* - time_type: (optional) Integer date type to use in the formatter,
* defaults to IntlDateFormatter::FULL.
* - lenient: (optional) Boolean choice of whether or not to use lenient
* processing in the intl formatter. Defaults to FALSE;
*
* @return string
* The formatted value of the date.
*/
public function format($format, $settings = array()) {
$format_string_type = isset($settings['format_string_type']) ? $settings['format_string_type'] : static::PHP;
$settings['langcode'] = !empty($settings['langcode']) ? $settings['langcode'] : $this->langcode;
$settings['country'] = !empty($settings['country']) ? $settings['country'] : $this->country;
// Format the date and catch errors.
try {
// If we have what we need to use the IntlDateFormatter, do so.
if ($this->canUseIntl() && $format_string_type == parent::INTL) {
$value = parent::format($format, $settings);
}
// Otherwise, use the default Drupal method.
else {
// Encode markers that should be translated. 'A' becomes
// '\xEF\AA\xFF'. xEF and xFF are invalid UTF-8 sequences,
// and we assume they are not in the input string.
// Paired backslashes are isolated to prevent errors in
// read-ahead evaluation. The read-ahead expression ensures that
// A matches, but not \A.
$format = preg_replace(array('/\\\\\\\\/', '/(?<!\\\\)([AaeDlMTF])/'), array("\xEF\\\\\\\\\xFF", "\xEF\\\\\$1\$1\xFF"), $format);
// Call date_format().
$format = parent::format($format);
// Pass the langcode to _format_date_callback().
_format_date_callback(NULL, $settings['langcode']);
// Translate the marked sequences.
$value = preg_replace_callback('/\xEF([AaeDlMTF]?)(.*?)\xFF/', '_format_date_callback', $format);
}
}
catch (\Exception $e) {
$this->errors[] = $e->getMessage();
}
return $value;
}
}
......@@ -7,16 +7,17 @@
namespace Drupal\Core\TypedData\Type;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\TypedData\TypedDataInterface;
use DateTime;
use InvalidArgumentException;
/**
* The date data type.
*
* The plain value of a date is an instance of the DateTime class. For setting
* the value an instance of the DateTime class, any string supported by
* DateTime::__construct(), or a timestamp as integer may be passed.
* The plain value of a date is an instance of the DrupalDateTime class. For setting
* the value any value supported by the __construct() of the DrupalDateTime
* class will work, including a DateTime object, a timestamp, a string
* date, or an array of date parts.
*/
class Date extends TypedData implements TypedDataInterface {
......@@ -31,18 +32,17 @@ class Date extends TypedData implements TypedDataInterface {
* Implements TypedDataInterface::setValue().
*/
public function setValue($value) {
if ($value instanceof DateTime || !isset($value)) {
// Don't try to create a date from an empty value.
// It would default to the current time.
if (!isset($value)) {
$this->value = $value;
}
// Treat integer values as timestamps, even if supplied as PHP string.
elseif ((string) (int) $value === (string) $value) {
$this->value = new DateTime('@' . $value);
}
elseif (is_string($value)) {
$this->value = new DateTime($value);
}
else {
throw new InvalidArgumentException("Invalid date format given.");
$this->value = $value instanceOf DrupalDateTime ? $value : new DrupalDateTime($value);
if ($this->value->hasErrors()) {
throw new InvalidArgumentException("Invalid date format given.");
}
}
}
......@@ -50,7 +50,7 @@ public function setValue($value) {
* Implements TypedDataInterface::getString().
*/
public function getString() {
return (string) $this->getValue()->format(DateTime::ISO8601);
return (string) $this->getValue();
}
/**
......
......@@ -7,6 +7,7 @@
namespace Drupal\comment;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityFormController;
......@@ -234,7 +235,8 @@ public function validate(array $form, array &$form_state) {
$account = user_load_by_name($form_state['values']['name']);
$form_state['values']['uid'] = $account ? $account->uid : 0;
if ($form_state['values']['date'] && strtotime($form_state['values']['date']) === FALSE) {
$date = new DrupalDateTime($form_state['values']['date']);
if ($date->hasErrors()) {
form_set_error('date', t('You have to specify a valid date.'));
}
if ($form_state['values']['name'] && !$form_state['values']['is_anonymous'] && !$account) {
......@@ -269,7 +271,8 @@ public function submit(array $form, array &$form_state) {
if (empty($comment->date)) {
$comment->date = 'now';
}
$comment->created = strtotime($comment->date);
$date = new DrupalDateTime($comment->date);
$comment->created = $date->getTimestamp();
$comment->changed = REQUEST_TIME;
// If the comment was posted by a registered user, assign the author's ID.
......
......@@ -7,6 +7,7 @@
namespace Drupal\node;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityFormController;
......@@ -288,7 +289,8 @@ public function validate(array $form, array &$form_state) {
}
// Validate the "authored on" field.
if (!empty($node->date) && strtotime($node->date) === FALSE) {
$date = new DrupalDateTime($node->date);
if ($date->hasErrors()) {
form_set_error('date', t('You have to specify a valid date.'));
}
......
......@@ -14,6 +14,7 @@
use Drupal\Core\Database\Query\AlterableInterface;
use Drupal\Core\Database\Query\SelectExtender;
use Drupal\Core\Database\Query\SelectInterface;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\Template\Attribute;
use Drupal\node\Plugin\Core\Entity\Node;
use Drupal\file\Plugin\Core\Entity\File;
......@@ -1027,7 +1028,14 @@ function node_submit(Node $node) {
$node->revision_uid = $user->uid;
}
$node->created = !empty($node->date) ? strtotime($node->date) : REQUEST_TIME;
if (!empty($node->date)) {
$node_created = new DrupalDateTime($node->date);
$node->created = $node_created->getTimestamp();
}
else {
$node->created = REQUEST_TIME;
}
$node->validated = TRUE;
return $node;
......
<?php
/**
* @file
* Definition of Drupal\system\Tests\Datetime\DateTimePlusTest.
*/
namespace Drupal\system\Tests\Datetime;
use Drupal\simpletest\UnitTestBase;
use Drupal\Component\Datetime\DateTimePlus;
use DateTimeZone;
class DateTimePlusTest extends UnitTestBase {
/**
* Test information.
*/
public static function getInfo() {
return array(
'name' => 'DateTimePlus',
'description' => 'Test DateTimePlus functionality.',
'group' => 'Datetime',
);
}
/**
* Set up required modules.
*/
public static $modules = array();
/**
* Test creating dates from string input.
*/
public function testDateStrings() {
// Create date object from datetime string.
$input = '2009-03-07 10:30';
$timezone = 'America/Chicago';
$date = new DateTimePlus($input, $timezone);
$value = $date->format('c');
$expected = '2009-03-07T10:30:00-06:00';
$this->assertEqual($expected, $value, "Test new DateTimePlus($input, $timezone): should be $expected, found $value.");
// Same during daylight savings time.
$input = '2009-06-07 10:30';
$timezone = 'America/Chicago';
$date = new DateTimePlus($input, $timezone);
$value = $date->format('c');
$expected = '2009-06-07T10:30:00-05:00';
$this->assertEqual($expected, $value, "Test new DateTimePlus($input, $timezone): should be $expected, found $value.");
// Create date object from date string.
$input = '2009-03-07';
$timezone = 'America/Chicago';
$date = new DateTimePlus($input, $timezone);
$value = $date->format('c');
$expected = '2009-03-07T00:00:00-06:00';
$this->assertEqual($expected, $value, "Test new DateTimePlus($input, $timezone): should be $expected, found $value.");
// Same during daylight savings time.
$input = '2009-06-07';
$timezone = 'America/Chicago';
$date = new DateTimePlus($input, $timezone);
$value = $date->format('c');
$expected = '2009-06-07T00:00:00-05:00';
$this->assertEqual($expected, $value, "Test new DateTimePlus($input, $timezone): should be $expected, found $value.");
// Create date object from date string.
$input = '2009-03-07 10:30';
$timezone = 'Australia/Canberra';
$date = new DateTimePlus($input, $timezone);
$value = $date->format('c');
$expected = '2009-03-07T10:30:00+11:00';
$this->assertEqual($expected, $value, "Test new DateTimePlus($input, $timezone): should be $expected, found $value.");
// Same during daylight savings time.
$input = '2009-06-07 10:30';
$timezone = 'Australia/Canberra';
$date = new DateTimePlus($input, $timezone);
$value = $date->format('c');
$expected = '2009-06-07T10:30:00+10:00';
$this->assertEqual($expected, $value, "Test new DateTimePlus($input, $timezone): should be $expected, found $value.");
}
/**
* Test creating dates from arrays of date parts.
*/
function testDateArrays() {
// Create date object from date array, date only.
$input = array('year' => 2010, 'month' => 2, 'day' => 28);
$timezone = 'America/Chicago';
$date = new DateTimePlus($input, $timezone);
$value = $date->format('c');
$expected = '2010-02-28T00:00:00-06:00';
$this->assertEqual($expected, $value, "Test new DateTimePlus(array('year' => 2010, 'month' => 2, 'day' => 28), $timezone): should be $expected, found $value.");
// Create date object from date array with hour.
$input = array('year' => 2010, 'month' => 2, 'day' => 28, 'hour' => 10);
$timezone = 'America/Chicago';
$date = new DateTimePlus($input, $timezone);
$value = $date->format('c');
$expected = '2010-02-28T10:00:00-06:00';
$this->assertEqual($expected, $value, "Test new DateTimePlus(array('year' => 2010, 'month' => 2, 'day' => 28, 'hour' => 10), $timezone): should be $expected, found $value.");
// Create date object from date array, date only.
$input = array('year' => 2010, 'month' => 2, 'day' => 28);
$timezone = 'Europe/Berlin';
$date = new DateTimePlus($input, $timezone);
$value = $date->format('c');
$expected = '2010-02-28T00:00:00+01:00';
$this->assertEqual($expected, $value, "Test new DateTimePlus(array('year' => 2010, 'month' => 2, 'day' => 28), $timezone): should be $expected, found $value.");
// Create date object from date array with hour.
$input = array('year' => 2010, 'month' => 2, 'day' => 28, 'hour' => 10);
$timezone = 'Europe/Berlin';
$date = new DateTimePlus($input, $timezone);
$value = $date->format('c');
$expected = '2010-02-28T10:00:00+01:00';
$this->assertEqual($expected, $value, "Test new DateTimePlus(array('year' => 2010, 'month' => 2, 'day' => 28, 'hour' => 10), $timezone): should be $expected, found $value.");
}
/**
* Test creating dates from timestamps.
*/
function testDateTimestamp() {
// Create date object from a unix timestamp and display it in
// local time.
$input = 0;
$timezone = 'UTC';
$date = new DateTimePlus($input, $timezone);
$value = $date->format('c');
$expected = '1970-01-01T00:00:00+00:00';
$this->assertEqual($expected, $value, "Test new DateTimePlus($input, $timezone): should be $expected, found $value.");
$expected = 'UTC';
$value = $date->getTimeZone()->getName();
$this->assertEqual($expected, $value, "The current timezone is $value: should be $expected.");
$expected = 0;
$value = $date->getOffset();
$this->assertEqual($expected, $value, "The current offset is $value: should be $expected.");
$timezone = 'America/Los_Angeles';
$date->setTimezone(new DateTimeZone($timezone));
$value = $date->format('c');
$expected = '1969-12-31T16:00:00-08:00';
$this->assertEqual($expected, $value, "Test \$date->setTimezone(new DateTimeZone($timezone)): should be $expected, found $value.");
$expected = 'America/Los_Angeles';
$value = $date->getTimeZone()->getName();
$this->assertEqual($expected, $value, "The current timezone should be $expected, found $value.");
$expected = '-28800';
$value = $date->getOffset();
$this->assertEqual($expected, $value, "The current offset should be $expected, found $value.");
// Create a date using the timestamp of zero, then display its
// value both in UTC and the local timezone.
$input = 0;
$timezone = 'America/Los_Angeles';
$date = new DateTimePlus($input, $timezone);
$offset = $date->getOffset();
$value = $date->format('c');
$expected = '1969-12-31T16:00:00-08:00';
$this->assertEqual($expected, $value, "Test new DateTimePlus($input, $timezone): should be $expected, found $value.");
$expected = 'America/Los_Angeles';
$value = $date->getTimeZone()->getName();
$this->assertEqual($expected, $value, "The current timezone should be $expected, found $value.");
$expected = '-28800';
$value = $date->getOffset();
$this->assertEqual($expected, $value, "The current offset should be $expected, found $value.");
$timezone = 'UTC';
$date->setTimezone(new DateTimeZone($timezone));
$value = $date->format('c');
$expected = '1970-01-01T00:00:00+00:00';
$this->assertEqual($expected, $value, "Test \$date->setTimezone(new DateTimeZone($timezone)): should be $expected, found $value.");
$expected = 'UTC';
$value = $date->getTimeZone()->getName();
$this->assertEqual($expected, $value, "The current timezone should be $expected, found $value.");
$expected = '0';
$value = $date->getOffset();
$this->assertEqual($expected, $value, "The current offset should be $expected, found $value.");
}
/**
* Test timezone manipulation.
*/
function testTimezoneConversion() {
// Create date object from datetime string in UTC, and convert
// it to a local date.
$input = '1970-01-01 00:00:00';
$timezone = 'UTC';
$date = new DateTimePlus($input, $timezone);
$value = $date->format('c');
$expected = '1970-01-01T00:00:00+00:00';
$this->assertEqual($expected, $value, "Test new DateTimePlus('$input', '$timezone'): should be $expected, found $value.");
$expected = 'UTC';
$value = $date->getTimeZone()->getName();
$this->assertEqual($expected, $value, "The current timezone is $value: should be $expected.");
$expected = 0;
$value = $date->getOffset();
$this->assertEqual($expected, $value, "The current offset is $value: should be $expected.");
$timezone = 'America/Los_Angeles';
$date->setTimezone(new DateTimeZone($timezone));
$value = $date->format('c');
$expected = '1969-12-31T16:00:00-08:00';
$this->assertEqual($expected, $value, "Test \$date->setTimezone(new DateTimeZone($timezone)): should be $expected, found $value.");
$expected = 'America/Los_Angeles';
$value = $date->getTimeZone()->getName();
$this->assertEqual($expected, $value, "The current timezone should be $expected, found $value.");
$expected = '-28800';
$value = $date->getOffset();
$this->assertEqual($expected, $value, "The current offset should be $expected, found $value.");
// Convert the local time to UTC using string input.
$input = '1969-12-31 16:00:00';
$timezone = 'America/Los_Angeles';
$date = new DateTimePlus($input, $timezone);
$offset = $date->getOffset();
$value = $date->format('c');
$expected = '1969-12-31T16:00:00-08:00';
$this->assertEqual($expected, $value, "Test new DateTimePlus('$input', '$timezone'): should be $expected, found $value.");
$expected = 'America/Los_Angeles';
$value = $date->getTimeZone()->getName();
$this->assertEqual($expected, $value, "The current timezone should be $expected, found $value.");
$expected = '-28800';
$value = $date->getOffset();
$this->assertEqual($expected, $value, "The current offset should be $expected, found $value.");
$timezone = 'UTC';
$date->setTimezone(new DateTimeZone($timezone));
$value = $date->format('c');
$expected = '1970-01-01T00:00:00+00:00';
$this->assertEqual($expected, $value, "Test \$date->setTimezone(new DateTimeZone($timezone)): should be $expected, found $value.");
$expected = 'UTC';
$value = $date->getTimeZone()->getName();
$this->assertEqual($expected, $value, "The current timezone should be $expected, found $value.");
$expected = '0';