Commit e2a81db3 authored by catch's avatar catch
Browse files

Issue #1844956 by msonnabaum, beejeebus: Fixed Optimize date formatting performance.

parent 18a03b1f
......@@ -3192,7 +3192,7 @@ function drupal_page_set_cache(Response $response, Request $request) {
// Use the actual timestamp from an Expires header, if available.
if ($date = $response->getExpires()) {
$date = new DrupalDateTime($date);
$date = DrupalDateTime::createFromDateTime($date);
$cache->expire = $date->getTimestamp();
}
......
......@@ -106,21 +106,85 @@ class DateTimePlus extends \DateTime {
protected $errors = array();
/**
* Constructs a date object set to a requested date and timezone.
* A boolean to store whether or not the intl php extension is available.
*/
static $intlExtentionExists = NULL;
/**
* Creates a date object from an input date object.
*
* @param mixed $time
* A DateTime object, a date/time string, a unix timestamp,
* or an array of date parts, like ('year' => 2014, 'month => 4).
* Defaults to 'now'.
* @param \DateTime $datetime
* A DateTime object.
* @param array $settings
* @see __construct()
*/
public static function createFromDateTime(\DateTime $datetime, $settings = array()) {
return new static($datetime->format(static::FORMAT), $datetime->getTimezone(), $settings);
}
/**
* Creates a date object from an array of date parts.
*
* Converts the input value into an ISO date, forcing a full ISO
* date even if some values are missing.
*
* @param array $date_parts
* An array of date parts, like ('year' => 2014, 'month => 4).
* @param mixed $timezone
* PHP DateTimeZone object, string or NULL allowed.
* Defaults to NULL.
* @see __construct()
* @param array $settings
* @see __construct()
*/
public static function createFromArray(array $date_parts, $timezone = NULL, $settings = array()) {
$date_parts = static::prepareArray($date_parts, TRUE);
if (static::checkArray($date_parts)) {
// Even with validation, we can end up with a value that the
// parent class won't handle, like a year outside the range
// of -9999 to 9999, which will pass checkdate() but
// fail to construct a date object.
$iso_date = static::arrayToISO($date_parts);
return new static($iso_date, $timezone, $settings);
}
else {
throw new \Exception('The array contains invalid values.');
}
}
/**
* Creates a date object from timestamp input.
*
* The timezone of a timestamp is always UTC. The timezone for a
* timestamp indicates the timezone used by the format() method.
*
* @param int $timestamp
* A UNIX timestamp.
* @param mixed $timezone
* @see __construct()
* @param array $settings
* @see __construct()
*/
public static function createFromTimestamp($timestamp, $timezone = NULL, $settings = array()) {
if (!is_numeric($timestamp)) {
throw new \Exception('The timestamp must be numeric.');
}
$datetime = new static('', $timezone, $settings);
$datetime->setTimestamp($timestamp);
return $datetime;
}
/**
* Creates a date object from an input format.
*
* @param string $format
* PHP date() type format for parsing the input. This is recommended
* for specialized input with a known format. If provided the
* 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 mixed $time
* @see __construct()
* @param mixed $timezone
* @see __construct()
* @param array $settings
* - validate_format: (optional) Boolean choice to validate the
* created date using the input format. The format used in
......@@ -129,6 +193,47 @@ class DateTimePlus extends \DateTime {
* 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.
* @see __construct()
*/
public static function createFromFormat($format, $time, $timezone = NULL, $settings = array()) {
if (!isset($settings['validate_format'])) {
$settings['validate_format'] = TRUE;
}
// Tries to create a date from the format and use it if possible.
// A regular try/catch won't work right here, if the value is
// invalid it doesn't return an exception.
$datetimeplus = new static('', $timezone, $settings);
$date = \DateTime::createFromFormat($format, $time, $datetimeplus->getTimezone());
if (!$date instanceOf \DateTime) {
throw new \Exception('The date cannot be created from a format.');
}
else {
$datetimeplus->setTimestamp($date->getTimestamp());
$datetimeplus->setTimezone($date->getTimezone());
// The createFromFormat function is forgiving, it might create a date that
// is not exactly a match for the provided value, so test for that. For
// instance, an input value of '11' using a format of Y (4 digits) gets
// created as '0011' instead of '2011'. Use the parent::format() because
// we do not want to use the IntlDateFormatter here.
if ($settings['validate_format'] && $date->format($format) != $time) {
throw new \Exception('The created date does not match the input value.');
}
}
return $datetimeplus;
}
/**
* Constructs a date object set to a requested date and timezone.
*
* @param string $time
* A date/time string. Defaults to 'now'.
* @param mixed $timezone
* PHP DateTimeZone object, string or NULL allowed.
* Defaults to NULL.
* @param array $settings
* - 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.
......@@ -142,66 +247,36 @@ class DateTimePlus extends \DateTime {
* - 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()) {
public function __construct($time = 'now', $timezone = NULL, $settings = array()) {
// Unpack settings.
$this->validateFormat = !empty($settings['validate_format']) ? $settings['validate_format'] : TRUE;
$this->langcode = !empty($settings['langcode']) ? $settings['langcode'] : NULL;
$this->country = !empty($settings['country']) ? $settings['country'] : NULL;
$this->calendar = !empty($settings['calendar']) ? $settings['calendar'] : static::CALENDAR;
// Store the original input so it is available for validation.
$this->inputTimeRaw = $time;
$this->inputTimeZoneRaw = $timezone;
$this->inputFormatRaw = $format;
// Massage the input values as necessary.
$this->prepareTime($time);
$this->prepareTimezone($timezone);
$this->prepareFormat($format);
// Create a date as a clone of an input DateTime object.
if ($this->inputIsObject()) {
$this->constructFromObject();
}
$prepared_time = $this->prepareTime($time);
$prepared_timezone = $this->prepareTimezone($timezone);
// Create date from array of date parts.
elseif ($this->inputIsArray()) {
$this->constructFromArray();
}
// Create a date from a Unix timestamp.
elseif ($this->inputIsTimestamp()) {
$this->constructFromTimestamp();
}
try {
if (!empty($prepared_time)) {
$test = date_parse($prepared_time);
if (!empty($test['errors'])) {
$this->errors[] = $test['errors'];
}
}
// Create a date from a time string and an expected format.
elseif ($this->inputIsFormat()) {
$this->constructFromFormat();
if (empty($this->errors)) {
parent::__construct($prepared_time, $prepared_timezone);
}
}
// Create a date from any other input.
else {
$this->constructFallback();
catch (\Exception $e) {
$this->errors[] = $e->getMessage();
}
// Clean up the error messages.
$this->checkErrors();
$this->errors = array_unique($this->errors);
// Now that we've validated the input, clean up the extra values.
if (empty($settings['debug'])) {
unset(
$this->inputTimeRaw,
$this->inputTimeAdjusted,
$this->inputTimeZoneRaw,
$this->inputTimeZoneAdjusted,
$this->inputFormatRaw,
$this->inputFormatAdjusted,
$this->validateFormat
);
}
}
/**
......@@ -228,7 +303,7 @@ public function __toString() {
* or an array of date parts.
*/
protected function prepareTime($time) {
$this->inputTimeAdjusted = $time;
return $time;
}
/**
......@@ -247,12 +322,6 @@ protected function prepareTimezone($timezone) {
$timezone_adjusted = $timezone;
}
// When the passed-in time is a DateTime object with its own
// timezone, try to use the date's timezone.
elseif (empty($timezone) && $this->inputTimeAdjusted instanceOf \DateTime) {
$timezone_adjusted = $this->inputTimeAdjusted->getTimezone();
}
// Allow string timezone input, and create a timezone from it.
elseif (!empty($timezone) && is_string($timezone)) {
$timezone_adjusted = new \DateTimeZone($timezone);
......@@ -267,7 +336,7 @@ protected function prepareTimezone($timezone) {
}
// We are finally certain that we have a usable timezone.
$this->inputTimeZoneAdjusted = $timezone_adjusted;
return $timezone_adjusted;
}
/**
......@@ -280,179 +349,10 @@ protected function prepareTimezone($timezone) {
* A PHP format string.
*/
protected function prepareFormat($format) {
$this->inputFormatAdjusted = $format;
}
/**
* Checks whether input is a DateTime object.
*
* @return boolean
* TRUE if the input time is a DateTime object.
*/
public function inputIsObject() {
return $this->inputTimeAdjusted instanceOf \DateTime;
}
/**
* Creates a date object from an input date object.
*/
protected function constructFromObject() {
try {
$this->inputTimeAdjusted = $this->inputTimeAdjusted->format(static::FORMAT);
parent::__construct($this->inputTimeAdjusted, $this->inputTimeZoneAdjusted);
}
catch (\Exception $e) {
$this->errors[] = $e->getMessage();
}
return $format;
}
/**
* Checks whether input time seems to be a timestamp.
*
* Providing an input format will prevent ISO values without separators
* from being mis-interpreted as timestamps. Providing a format can also
* avoid interpreting a value like '2010' with a format of 'Y' as a
* timestamp. The 'U' format indicates this is a timestamp.
*
* @return boolean
* TRUE if the input time is a timestamp.
*/
public function inputIsTimestamp() {
return is_numeric($this->inputTimeAdjusted) && (empty($this->inputFormatAdjusted) || $this->inputFormatAdjusted == 'U');
}
/**
* Creates a date object from timestamp input.
*
* The timezone of a timestamp is always UTC. The timezone for a
* timestamp indicates the timezone used by the format() method.
*/
protected function constructFromTimestamp() {
try {
parent::__construct('', $this->inputTimeZoneAdjusted);
$this->setTimestamp($this->inputTimeAdjusted);
}
catch (\Exception $e) {
$this->errors[] = $e->getMessage();
}
}
/**
* Checks if input is an array of date parts.
*
* @return boolean
* TRUE if the input time is a DateTime object.
*/
public function inputIsArray() {
return is_array($this->inputTimeAdjusted);
}
/**
* Creates a date object from an array of date parts.
*
* Converts the input value into an ISO date, forcing a full ISO
* date even if some values are missing.
*/
protected function constructFromArray() {
try {
parent::__construct('', $this->inputTimeZoneAdjusted);
$this->inputTimeAdjusted = static::prepareArray($this->inputTimeAdjusted, TRUE);
if (static::checkArray($this->inputTimeAdjusted)) {
// Even with validation, we can end up with a value that the
// parent class won't handle, like a year outside the range
// of -9999 to 9999, which will pass checkdate() but
// fail to construct a date object.
$this->inputTimeAdjusted = static::arrayToISO($this->inputTimeAdjusted);
parent::__construct($this->inputTimeAdjusted, $this->inputTimeZoneAdjusted);
}
else {
throw new \Exception('The array contains invalid values.');
}
}
catch (\Exception $e) {
$this->errors[] = $e->getMessage();
}
}
/**
* Checks if input is a string with an expected format.
*
* @return boolean
* TRUE if the input time is a string with an expected format.
*/
public function inputIsFormat() {
return is_string($this->inputTimeAdjusted) && !empty($this->inputFormatAdjusted);
}
/**
* Creates a date object from an input format.
*/
protected function constructFromFormat() {
// Tries to create a date from the format and use it if possible.
// A regular try/catch won't work right here, if the value is
// invalid it doesn't return an exception.
try {
parent::__construct('', $this->inputTimeZoneAdjusted);
$date = parent::createFromFormat($this->inputFormatAdjusted, $this->inputTimeAdjusted, $this->inputTimeZoneAdjusted);
if (!$date instanceOf \DateTime) {
throw new \Exception('The date cannot be created from a format.');
}
else {
$this->setTimestamp($date->getTimestamp());
$this->setTimezone($date->getTimezone());
try {
// The createFromFormat function is forgiving, it might
// create a date that is not exactly a match for the provided
// value, so test for that. For instance, an input value of
// '11' using a format of Y (4 digits) gets created as
// '0011' instead of '2011'.
// Use the parent::format() because we do not want to use
// the IntlDateFormatter here.
if ($this->validateFormat && parent::format($this->inputFormatAdjusted) != $this->inputTimeRaw) {
throw new \Exception('The created date does not match the input value.');
}
}
catch (\Exception $e) {
$this->errors[] = $e->getMessage();
}
}
}
catch (\Exception $e) {
$this->errors[] = $e->getMessage();
}
}
/**
* Creates a date when none of the other methods are appropriate.
*
* Fallback construction for values that don't match any of the
* other patterns. Lets the parent dateTime attempt to turn this string
* into a valid date.
*/
protected function constructFallback() {
try {
// One last test for invalid input before we try to construct
// a date. If the input contains totally bogus information
// it will blow up badly if we pass it to the constructor.
// The date_parse() function will tell us if the input
// makes sense.
if (!empty($this->inputTimeAdjusted)) {
$test = date_parse($this->inputTimeAdjusted);
if (!empty($test['errors'])) {
$this->errors[] = $test['errors'];
}
}
if (empty($this->errors)) {
parent::__construct($this->inputTimeAdjusted, $this->inputTimeZoneAdjusted);
}
}
catch (\Exception $e) {
$this->errors[] = $e->getMessage();
}
}
/**
* Examines getLastErrors() to see what errors to report.
......@@ -655,7 +555,14 @@ public function canUseIntl($calendar = NULL, $langcode = NULL, $country = NULL)
$country = !empty($country) ? $country : $this->country;
$calendar = !empty($calendar) ? $calendar : $this->calendar;
return class_exists('IntlDateFormatter') && !empty($calendar) && !empty($langcode) && !empty($country);
return $this->intlDateFormatterExists() && !empty($calendar) && !empty($langcode) && !empty($country);
}
public static function intlDateFormatterExists() {
if (static::$intlExtentionExists === NULL) {
static::$intlExtentionExists = class_exists('IntlDateFormatter');
}
return static::$intlExtentionExists;
}
/**
......
......@@ -39,6 +39,9 @@ class Date {
*/
protected $languageManager;
protected $country = NULL;
protected $dateFormats = array();
/**
* Constructs a Date object.
*
......@@ -97,19 +100,23 @@ public function format($timestamp, $type = 'medium', $format = '', $timezone = N
}
// Create a DrupalDateTime object from the timestamp and timezone.
$date = new DrupalDateTime($timestamp, $this->timezones[$timezone]);
$create_settings = array(
'langcode' => $langcode,
'country' => $this->country(),
);
$date = DrupalDateTime::createFromTimestamp($timestamp, $this->timezones[$timezone], $create_settings);
// Find the appropriate format type.
$key = $date->canUseIntl() ? DrupalDateTime::INTL : DrupalDateTime::PHP;
// If we have a non-custom date format use the provided date format pattern.
if ($date_format = $this->dateFormatStorage->load($type)) {
if ($date_format = $this->dateFormat($type)) {
$format = $date_format->getPattern($key);
}
// Fall back to medium if a format was not found.
if (empty($format)) {
$format = $this->dateFormatStorage->load('fallback')->getPattern($key);
$format = $this->dateFormat('fallback')->getPattern($key);
}
// Call $date->format().
......@@ -120,4 +127,24 @@ public function format($timestamp, $type = 'medium', $format = '', $timezone = N
return Xss::filter($date->format($format, $settings));
}
protected function dateFormat($format) {
if (!isset($this->dateFormats[$format])) {
$this->dateFormats[$format] = $this->dateFormatStorage->load($format);
}
return $this->dateFormats[$format];
}
/**
* Returns the default country from config.
*
* @return string
* The config setting for country.default.
*/
protected function country() {
if ($this->country === NULL) {
$this->country = config('system.date')->get('country.default');
}
return $this->country;
}
}
......@@ -29,13 +29,6 @@ class DrupalDateTime extends DateTimePlus {
* @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
......@@ -57,14 +50,18 @@ class DrupalDateTime extends DateTimePlus {
* - 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()) {
public function __construct($time = 'now', $timezone = NULL, $settings = array()) {
// We can set the langcode and country using Drupal values.
$settings['langcode'] = !empty($settings['langcode']) ? $settings['langcode'] : language(Language::TYPE_INTERFACE)->id;
$settings['country'] = !empty($settings['country']) ? $settings['country'] : config('system.date')->get('country.default');
if (!isset($settings['langcode'])) {
$settings['langcode'] = language(Language::TYPE_INTERFACE)->id;
}
if (!isset($settings['country'])) {
$settings['country'] = config('system.date')->get('country.default');
}
// Instantiate the parent class.
parent::__construct($time, $timezone, $format, $settings);
parent::__construct($time, $timezone, $settings);
}
......@@ -79,7 +76,7 @@ protected function prepareTimezone($timezone) {
if (empty($timezone) && !empty($user_timezone)) {
$timezone = $user_timezone;
}
parent::prepareTimezone($timezone);
return parent::prepareTimezone($timezone);
}
/**
......
......@@ -29,7 +29,13 @@ class DateTimeIso8601 extends String implements DateTimeInterface {
*/
public function getDateTime() {
if ($this->value) {
return new DrupalDateTime($this->value);
if (is_array($this->value)) {
$datetime = DrupalDateTime::createFromArray($this->value);
}
else {
$datetime = new DrupalDateTime($this->value);
}
return $datetime;
}
}
......
......@@ -34,7 +34,7 @@ class Timestamp extends Integer implements DateTimeInterface {
*/
public function getDateTime() {
if ($this->value) {
return new DrupalDateTime($this->value);
return DrupalDateTime::createFromTimestamp($this->value);
}
}
......
......@@ -62,7 +62,7 @@ public function form(array $form, array &$form_state) {
if ($is_admin) {
$author = $comment->name->value;
$status = (isset($comment->status->value) ? $comment->status->value : COMMENT_NOT_PUBLISHED);
$date = (!empty($comment->date) ? $comment->date : new DrupalDateTime($comment->created->value));
$date = (!empty($comment->date) ? $comment->date : DrupalDateTime::createFromTimestamp($comment->created->value));
}
else {