579 lines
16 KiB
PHP
579 lines
16 KiB
PHP
<?php
|
|
|
|
namespace SilverStripe\ORM\FieldType;
|
|
|
|
use IntlDateFormatter;
|
|
use InvalidArgumentException;
|
|
use NumberFormatter;
|
|
use SilverStripe\Forms\DateField;
|
|
use SilverStripe\i18n\i18n;
|
|
use SilverStripe\ORM\DB;
|
|
use SilverStripe\Security\Member;
|
|
|
|
/**
|
|
* Represents a date field.
|
|
* Dates should be stored using ISO 8601 formatted date (y-MM-dd).
|
|
* Alternatively you can set a timestamp that is evaluated through
|
|
* PHP's built-in date() function according to your system locale.
|
|
*
|
|
* Example definition via {@link DataObject::$db}:
|
|
* <code>
|
|
* static $db = array(
|
|
* "Expires" => "Date",
|
|
* );
|
|
* </code>
|
|
*
|
|
* Date formats all follow CLDR standard format codes
|
|
* @link http://userguide.icu-project.org/formatparse/datetime
|
|
*/
|
|
class DBDate extends DBField
|
|
{
|
|
/**
|
|
* Standard ISO format string for date in CLDR standard format
|
|
*/
|
|
const ISO_DATE = 'y-MM-dd';
|
|
|
|
public function setValue($value, $record = null, $markChanged = true)
|
|
{
|
|
$value = $this->parseDate($value);
|
|
if ($value === false) {
|
|
throw new InvalidArgumentException(
|
|
"Invalid date: '$value'. Use " . self::ISO_DATE . " to prevent this error."
|
|
);
|
|
}
|
|
$this->value = $value;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Parse timestamp or iso8601-ish date into standard iso8601 format
|
|
*
|
|
* @param mixed $value
|
|
* @return string|null|false Formatted date, null if empty but valid, or false if invalid
|
|
*/
|
|
protected function parseDate($value)
|
|
{
|
|
// Skip empty values
|
|
if (empty($value) && !is_numeric($value)) {
|
|
return null;
|
|
}
|
|
|
|
// Determine value to parse
|
|
if (is_array($value)) {
|
|
$source = $value; // parse array
|
|
} elseif (is_numeric($value)) {
|
|
$source = $value; // parse timestamp
|
|
} else {
|
|
// Convert US date -> iso, fix y2k, etc
|
|
$value = $this->fixInputDate($value);
|
|
$source = strtotime($value); // convert string to timestamp
|
|
}
|
|
if ($value === false) {
|
|
return false;
|
|
}
|
|
|
|
// Format as iso8601
|
|
$formatter = $this->getFormatter();
|
|
$formatter->setPattern($this->getISOFormat());
|
|
return $formatter->format($source);
|
|
}
|
|
|
|
/**
|
|
* Returns the standard localised medium date
|
|
*
|
|
* @return string
|
|
*/
|
|
public function Nice()
|
|
{
|
|
if (!$this->value) {
|
|
return null;
|
|
}
|
|
$formatter = $this->getFormatter();
|
|
return $formatter->format($this->getTimestamp());
|
|
}
|
|
|
|
/**
|
|
* Returns the year from the given date
|
|
*
|
|
* @return string
|
|
*/
|
|
public function Year()
|
|
{
|
|
return $this->Format('y');
|
|
}
|
|
|
|
/**
|
|
* Returns the day of the week
|
|
*
|
|
* @return string
|
|
*/
|
|
public function DayOfWeek()
|
|
{
|
|
return $this->Format('cccc');
|
|
}
|
|
|
|
/**
|
|
* Returns a full textual representation of a month, such as January.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function Month()
|
|
{
|
|
return $this->Format('LLLL');
|
|
}
|
|
|
|
/**
|
|
* Returns the short version of the month such as Jan
|
|
*
|
|
* @return string
|
|
*/
|
|
public function ShortMonth()
|
|
{
|
|
return $this->Format('LLL');
|
|
}
|
|
|
|
/**
|
|
* Returns the day of the month.
|
|
*
|
|
* @param bool $includeOrdinal Include ordinal suffix to day, e.g. "th" or "rd"
|
|
* @return string
|
|
*/
|
|
public function DayOfMonth($includeOrdinal = false)
|
|
{
|
|
$number = $this->Format('d');
|
|
if ($includeOrdinal && $number) {
|
|
$formatter = NumberFormatter::create(i18n::get_locale(), NumberFormatter::ORDINAL);
|
|
return $formatter->format((int)$number);
|
|
}
|
|
return $number;
|
|
}
|
|
|
|
/**
|
|
* Returns the date in the localised short format
|
|
*
|
|
* @return string
|
|
*/
|
|
public function Short()
|
|
{
|
|
if (!$this->value) {
|
|
return null;
|
|
}
|
|
$formatter = $this->getFormatter(IntlDateFormatter::SHORT);
|
|
return $formatter->format($this->getTimestamp());
|
|
}
|
|
|
|
/**
|
|
* Returns the date in the localised long format
|
|
*
|
|
* @return string
|
|
*/
|
|
public function Long()
|
|
{
|
|
if (!$this->value) {
|
|
return null;
|
|
}
|
|
$formatter = $this->getFormatter(IntlDateFormatter::LONG);
|
|
return $formatter->format($this->getTimestamp());
|
|
}
|
|
|
|
/**
|
|
* Returns the date in the localised full format
|
|
*
|
|
* @return string
|
|
*/
|
|
public function Full()
|
|
{
|
|
if (!$this->value) {
|
|
return null;
|
|
}
|
|
$formatter = $this->getFormatter(IntlDateFormatter::FULL);
|
|
return $formatter->format($this->getTimestamp());
|
|
}
|
|
|
|
/**
|
|
* Get date formatter
|
|
*
|
|
* @param int $dateLength
|
|
* @param int $timeLength
|
|
* @return IntlDateFormatter
|
|
*/
|
|
public function getFormatter($dateLength = IntlDateFormatter::MEDIUM, $timeLength = IntlDateFormatter::NONE)
|
|
{
|
|
return new IntlDateFormatter(i18n::get_locale(), $dateLength, $timeLength);
|
|
}
|
|
|
|
/**
|
|
* Get standard ISO date format string
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getISOFormat()
|
|
{
|
|
return self::ISO_DATE;
|
|
}
|
|
|
|
/**
|
|
* Return the date using a particular formatting string.
|
|
*
|
|
* @param string $format Format code string. See http://userguide.icu-project.org/formatparse/datetime
|
|
* @return string The date in the requested format
|
|
*/
|
|
public function Format($format)
|
|
{
|
|
if (!$this->value) {
|
|
return null;
|
|
}
|
|
$formatter = $this->getFormatter();
|
|
$formatter->setPattern($format);
|
|
return $formatter->format($this->getTimestamp());
|
|
}
|
|
|
|
/**
|
|
* Get unix timestamp for this date
|
|
*
|
|
* @return int
|
|
*/
|
|
public function getTimestamp()
|
|
{
|
|
if ($this->value) {
|
|
return strtotime($this->value);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Return a date formatted as per a CMS user's settings.
|
|
*
|
|
* @param Member $member
|
|
* @return boolean | string A date formatted as per user-defined settings.
|
|
*/
|
|
public function FormatFromSettings($member = null)
|
|
{
|
|
if (!$member) {
|
|
$member = Member::currentUser();
|
|
}
|
|
|
|
// Fall back to nice
|
|
if (!$member) {
|
|
return $this->Nice();
|
|
}
|
|
|
|
// Get user format
|
|
$format = $member->getDateFormat();
|
|
return $this->Format($format);
|
|
}
|
|
|
|
/**
|
|
* Return a string in the form "12 - 16 Sept" or "12 Aug - 16 Sept"
|
|
*
|
|
* @param DBDate $otherDateObj Another date object specifying the end of the range
|
|
* @param bool $includeOrdinals Include ordinal suffix to day, e.g. "th" or "rd"
|
|
* @return string
|
|
*/
|
|
public function RangeString($otherDateObj, $includeOrdinals = false)
|
|
{
|
|
$d1 = $this->DayOfMonth($includeOrdinals);
|
|
$d2 = $otherDateObj->DayOfMonth($includeOrdinals);
|
|
$m1 = $this->ShortMonth();
|
|
$m2 = $otherDateObj->ShortMonth();
|
|
$y1 = $this->Year();
|
|
$y2 = $otherDateObj->Year();
|
|
|
|
if ($y1 != $y2) {
|
|
return "$d1 $m1 $y1 - $d2 $m2 $y2";
|
|
} elseif ($m1 != $m2) {
|
|
return "$d1 $m1 - $d2 $m2 $y1";
|
|
} else {
|
|
return "$d1 - $d2 $m1 $y1";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return string in RFC822 format
|
|
*
|
|
* @return string
|
|
*/
|
|
public function Rfc822()
|
|
{
|
|
if ($this->value) {
|
|
return date('r', $this->getTimestamp());
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Return date in RFC2822 format
|
|
*
|
|
* @return string
|
|
*/
|
|
public function Rfc2822()
|
|
{
|
|
return $this->Format('y-MM-dd HH:mm:ss');
|
|
}
|
|
|
|
/**
|
|
* Date in RFC3339 format
|
|
*
|
|
* @return string
|
|
*/
|
|
public function Rfc3339()
|
|
{
|
|
$date = $this->Format('y-MM-dd\\THH:mm:ss');
|
|
if (!$date) {
|
|
return null;
|
|
}
|
|
|
|
$matches = array();
|
|
if (preg_match('/^([\-+])(\d{2})(\d{2})$/', date('O', $this->getTimestamp()), $matches)) {
|
|
$date .= $matches[1].$matches[2].':'.$matches[3];
|
|
} else {
|
|
$date .= 'Z';
|
|
}
|
|
|
|
return $date;
|
|
}
|
|
|
|
/**
|
|
* Returns the number of seconds/minutes/hours/days or months since the timestamp.
|
|
*
|
|
* @param boolean $includeSeconds Show seconds, or just round to "less than a minute".
|
|
* @param int $significance Minimum significant value of X for "X units ago" to display
|
|
* @return string
|
|
*/
|
|
public function Ago($includeSeconds = true, $significance = 2)
|
|
{
|
|
if (!$this->value) {
|
|
return null;
|
|
}
|
|
$timestamp = $this->getTimestamp();
|
|
$now = DBDatetime::now()->getTimestamp();
|
|
if ($timestamp <= $now) {
|
|
return _t(
|
|
'SilverStripe\\ORM\\FieldType\\DBDate.TIMEDIFFAGO',
|
|
"{difference} ago",
|
|
'Natural language time difference, e.g. 2 hours ago',
|
|
array('difference' => $this->TimeDiff($includeSeconds, $significance))
|
|
);
|
|
} else {
|
|
return _t(
|
|
'SilverStripe\\ORM\\FieldType\\DBDate.TIMEDIFFIN',
|
|
"in {difference}",
|
|
'Natural language time difference, e.g. in 2 hours',
|
|
array('difference' => $this->TimeDiff($includeSeconds, $significance))
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param boolean $includeSeconds Show seconds, or just round to "less than a minute".
|
|
* @param int $significance Minimum significant value of X for "X units ago" to display
|
|
* @return string
|
|
*/
|
|
public function TimeDiff($includeSeconds = true, $significance = 2)
|
|
{
|
|
if (!$this->value) {
|
|
return false;
|
|
}
|
|
|
|
$now = DBDatetime::now()->getTimestamp();
|
|
$time = $this->getTimestamp();
|
|
$ago = abs($time - $now);
|
|
if ($ago < 60 && !$includeSeconds) {
|
|
return _t('SilverStripe\\ORM\\FieldType\\DBDate.LessThanMinuteAgo', 'less than a minute');
|
|
} elseif ($ago < $significance * 60 && $includeSeconds) {
|
|
return $this->TimeDiffIn('seconds');
|
|
} elseif ($ago < $significance * 3600) {
|
|
return $this->TimeDiffIn('minutes');
|
|
} elseif ($ago < $significance * 86400) {
|
|
return $this->TimeDiffIn('hours');
|
|
} elseif ($ago < $significance * 86400 * 30) {
|
|
return $this->TimeDiffIn('days');
|
|
} elseif ($ago < $significance * 86400 * 365) {
|
|
return $this->TimeDiffIn('months');
|
|
} else {
|
|
return $this->TimeDiffIn('years');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the time difference, but always returns it in a certain format
|
|
*
|
|
* @param string $format The format, could be one of these:
|
|
* 'seconds', 'minutes', 'hours', 'days', 'months', 'years'.
|
|
* @return string The resulting formatted period
|
|
*/
|
|
public function TimeDiffIn($format)
|
|
{
|
|
if (!$this->value) {
|
|
return null;
|
|
}
|
|
|
|
$now = DBDatetime::now()->getTimestamp();
|
|
$time = $this->getTimestamp();
|
|
$ago = abs($time - $now);
|
|
switch ($format) {
|
|
case "seconds":
|
|
$span = $ago;
|
|
return _t(
|
|
__CLASS__.'.SECONDS_SHORT_PLURALS',
|
|
'{count} sec|{count} secs',
|
|
['count' => $span]
|
|
);
|
|
|
|
case "minutes":
|
|
$span = round($ago/60);
|
|
return _t(
|
|
__CLASS__.'.MINUTES_SHORT_PLURALS',
|
|
'{count} min|{count} mins',
|
|
['count' => $span]
|
|
);
|
|
|
|
case "hours":
|
|
$span = round($ago/3600);
|
|
return _t(
|
|
__CLASS__.'.HOURS_SHORT_PLURALS',
|
|
'{count} hour|{count} hours',
|
|
['count' => $span]
|
|
);
|
|
|
|
case "days":
|
|
$span = round($ago/86400);
|
|
return _t(
|
|
__CLASS__.'.DAYS_SHORT_PLURALS',
|
|
'{count} day|{count} days',
|
|
['count' => $span]
|
|
);
|
|
|
|
case "months":
|
|
$span = round($ago/86400/30);
|
|
return _t(
|
|
__CLASS__.'.MONTHS_SHORT_PLURALS',
|
|
'{count} month|{count} months',
|
|
['count' => $span]
|
|
);
|
|
|
|
case "years":
|
|
$span = round($ago/86400/365);
|
|
return _t(
|
|
__CLASS__.'.YEARS_SHORT_PLURALS',
|
|
'{count} year|{count} years',
|
|
['count' => $span]
|
|
);
|
|
|
|
default:
|
|
throw new \InvalidArgumentException("Invalid format $format");
|
|
}
|
|
}
|
|
|
|
public function requireField()
|
|
{
|
|
$parts=array('datatype'=>'date', 'arrayValue'=>$this->arrayValue);
|
|
$values=array('type'=>'date', 'parts'=>$parts);
|
|
DB::require_field($this->tableName, $this->name, $values);
|
|
}
|
|
|
|
/**
|
|
* Returns true if date is in the past.
|
|
* @return boolean
|
|
*/
|
|
public function InPast()
|
|
{
|
|
return strtotime($this->value) < DBDatetime::now()->getTimestamp();
|
|
}
|
|
|
|
/**
|
|
* Returns true if date is in the future.
|
|
* @return boolean
|
|
*/
|
|
public function InFuture()
|
|
{
|
|
return strtotime($this->value) > DBDatetime::now()->getTimestamp();
|
|
}
|
|
|
|
/**
|
|
* Returns true if date is today.
|
|
* @return boolean
|
|
*/
|
|
public function IsToday()
|
|
{
|
|
return $this->Format(self::ISO_DATE) === DBDatetime::now()->Format(self::ISO_DATE);
|
|
}
|
|
|
|
/**
|
|
* Returns a date suitable for insertion into a URL and use by the system.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function URLDate()
|
|
{
|
|
return rawurlencode($this->Format(self::ISO_DATE));
|
|
}
|
|
|
|
public function scaffoldFormField($title = null, $params = null)
|
|
{
|
|
$field = DateField::create($this->name, $title);
|
|
$field->setHTML5(true);
|
|
|
|
return $field;
|
|
}
|
|
|
|
/**
|
|
* Fix non-iso dates
|
|
*
|
|
* @param string $value
|
|
* @return string
|
|
*/
|
|
protected function fixInputDate($value)
|
|
{
|
|
// split
|
|
list($year, $month, $day, $time) = $this->explodeDateString($value);
|
|
|
|
// Validate date
|
|
if (!checkdate($month, $day, $year)) {
|
|
throw new InvalidArgumentException(
|
|
"Invalid date: '$value'. Use " . self::ISO_DATE . " to prevent this error."
|
|
);
|
|
}
|
|
|
|
// Convert to y-m-d
|
|
return sprintf('%d-%02d-%02d%s', $year, $month, $day, $time);
|
|
}
|
|
|
|
/**
|
|
* Attempt to split date string into year, month, day, and timestamp components.
|
|
*
|
|
* @param string $value
|
|
* @return array
|
|
*/
|
|
protected function explodeDateString($value)
|
|
{
|
|
// split on known delimiters (. / -)
|
|
if (!preg_match(
|
|
'#^(?<first>\\d+)[-/\\.](?<second>\\d+)[-/\\.](?<third>\\d+)(?<time>.*)$#',
|
|
$value,
|
|
$matches
|
|
)) {
|
|
throw new InvalidArgumentException(
|
|
"Invalid date: '$value'. Use " . self::ISO_DATE . " to prevent this error."
|
|
);
|
|
}
|
|
|
|
$parts = [
|
|
$matches['first'],
|
|
$matches['second'],
|
|
$matches['third']
|
|
];
|
|
// Flip d-m-y to y-m-d
|
|
if ($parts[0] < 1000 && $parts[2] > 1000) {
|
|
$parts = array_reverse($parts);
|
|
}
|
|
if ($parts[0] < 1000) {
|
|
throw new InvalidArgumentException(
|
|
"Invalid date: '$value'. Use " . self::ISO_DATE . " to prevent this error."
|
|
);
|
|
}
|
|
$parts[] = $matches['time'];
|
|
return $parts;
|
|
}
|
|
}
|