silverstripe-framework/src/ORM/FieldType/DBDate.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;
}
}