silverstripe-framework/src/ORM/FieldType/DBDatetime.php

332 lines
9.5 KiB
PHP

<?php
namespace SilverStripe\ORM\FieldType;
use Exception;
use IntlDateFormatter;
use InvalidArgumentException;
use SilverStripe\Forms\DatetimeField;
use SilverStripe\ORM\DB;
use SilverStripe\Security\Member;
use SilverStripe\Security\Security;
use SilverStripe\View\TemplateGlobalProvider;
/**
* Represents a date-time field.
* The field currently supports New Zealand date format (DD/MM/YYYY),
* or an ISO 8601 formatted date and time (Y-m-d H:i:s).
* Alternatively you can set a timestamp that is evaluated through
* PHP's built-in date() and strtotime() function according to your system locale.
*
* For all computations involving the current date and time,
* please use {@link DBDatetime::now()} instead of PHP's built-in date() and time()
* methods. This ensures that all time-based computations are testable with mock dates
* through {@link DBDatetime::set_mock_now()}.
*
* Example definition via {@link DataObject::$db}:
* <code>
* static $db = array(
* "Expires" => "DBDatetime",
* );
* </code>
*
* @todo Add localization support, see http://open.silverstripe.com/ticket/2931
* @skipUpgrade
*/
class DBDatetime extends DBDate implements TemplateGlobalProvider
{
/**
* Standard ISO format string for date and time in CLDR standard format,
* with a whitespace separating date and time (common database representation, e.g. in MySQL).
*/
const ISO_DATETIME = 'y-MM-dd HH:mm:ss';
/**
* Standard ISO format string for date and time in CLDR standard format,
* with a "T" separator between date and time (W3C standard, e.g. for HTML5 datetime-local fields).
*/
const ISO_DATETIME_NORMALISED = 'y-MM-dd\'T\'HH:mm:ss';
/**
* Flag idicating if this field is considered immutable
* when this is enabled setting the value of this field will return a new field instance
* instead updatin the old one
*
* @var bool
*/
protected $immutable = false;
/**
* @param bool $immutable
* @return $this
*/
public function setImmutable(bool $immutable): self
{
$this->immutable = $immutable;
return $this;
}
public function setValue($value, $record = null, $markChanged = true)
{
if ($this->immutable) {
// This field is set as immutable so we have to create a new field instance
// instead of just updating the value
$field = clone $this;
return $field
// This field will inherit the immutable status but we have to disable it before setting the value
// to avoid recursion
->setImmutable(false)
// Update the value so the new field contains the desired value
->setValue($value, $record, $markChanged)
// Return the immutable flag to the correct state
->setImmutable(true);
}
return parent::setValue($value, $record, $markChanged);
}
/**
* Returns the standard localised date
*
* @return string Formatted date.
*/
public function Date()
{
$formatter = $this->getFormatter(IntlDateFormatter::MEDIUM, IntlDateFormatter::NONE);
return $formatter->format($this->getTimestamp());
}
/**
* Returns the standard localised time
*
* @return string Formatted time.
*/
public function Time()
{
$formatter = $this->getFormatter(IntlDateFormatter::NONE, IntlDateFormatter::MEDIUM);
return $formatter->format($this->getTimestamp());
}
/**
* Returns the time in 12-hour format using the format string 'h:mm a' e.g. '1:32 pm'.
*
* @return string Formatted time.
*/
public function Time12()
{
return $this->Format('h:mm a');
}
/**
* Returns the time in 24-hour format using the format string 'H:mm' e.g. '13:32'.
*
* @return string Formatted time.
*/
public function Time24()
{
return $this->Format('H:mm');
}
/**
* Return a date and time formatted as per a CMS user's settings.
*
* @param Member $member
* @return boolean|string A time and date pair formatted as per user-defined settings.
*/
public function FormatFromSettings($member = null)
{
if (!$member) {
$member = Security::getCurrentUser();
}
// Fall back to nice
if (!$member) {
return $this->Nice();
}
$dateFormat = $member->getDateFormat();
$timeFormat = $member->getTimeFormat();
// Get user format
return $this->Format($dateFormat . ' ' . $timeFormat, $member->getLocale());
}
public function requireField()
{
$parts = [
'datatype' => 'datetime',
'arrayValue' => $this->arrayValue
];
$values = [
'type' => 'datetime',
'parts' => $parts
];
DB::require_field($this->tableName, $this->name, $values);
}
/**
* Returns the url encoded date and time in ISO 6801 format using format
* string 'y-MM-dd%20HH:mm:ss' e.g. '2014-02-28%2013:32:22'.
*
* @return string Formatted date and time.
*/
public function URLDatetime()
{
return rawurlencode($this->Format(self::ISO_DATETIME, self::ISO_LOCALE) ?? '');
}
public function scaffoldFormField($title = null, $params = null)
{
$field = DatetimeField::create($this->name, $title);
$dateTimeFormat = $field->getDatetimeFormat();
$locale = $field->getLocale();
// Set date formatting hints and example
$date = static::now()->Format($dateTimeFormat, $locale);
$field
->setDescription(_t(
'SilverStripe\\Forms\\FormField.EXAMPLE',
'e.g. {format}',
'Example format',
['format' => $date]
))
->setAttribute('placeholder', $dateTimeFormat);
return $field;
}
/**
*
*/
protected static $mock_now = null;
/**
* Returns either the current system date as determined
* by date(), or a mocked date through {@link set_mock_now()}.
*
* @return static
*/
public static function now()
{
$time = self::$mock_now ? self::$mock_now->Value : time();
/** @var DBDatetime $now */
$now = DBField::create_field('Datetime', $time);
return $now;
}
/**
* Mock the system date temporarily, which is useful for time-based unit testing.
* Use {@link clear_mock_now()} to revert to the current system date.
* Caution: This sets a fixed date that doesn't increment with time.
*
* @param DBDatetime|string $datetime Either in object format, or as a DBDatetime compatible string.
* @throws Exception
*/
public static function set_mock_now($datetime)
{
if (!$datetime instanceof DBDatetime) {
$value = $datetime;
$datetime = DBField::create_field('Datetime', $datetime);
if ($datetime === false) {
throw new InvalidArgumentException('DBDatetime::set_mock_now(): Wrong format: ' . $value);
}
}
self::$mock_now = $datetime;
}
/**
* Clear any mocked date, which causes
* {@link Now()} to return the current system date.
*/
public static function clear_mock_now()
{
self::$mock_now = null;
}
/**
* Run a callback with specific time, original mock value is retained after callback
*
* @param DBDatetime|string $time
* @param callable $callback
* @return mixed
* @throws Exception
*/
public static function withFixedNow($time, $callback)
{
$original = self::$mock_now;
try {
self::set_mock_now($time);
return $callback();
} finally {
self::$mock_now = $original;
}
}
public static function get_template_global_variables()
{
return [
'Now' => ['method' => 'now', 'casting' => 'Datetime'],
];
}
/**
* Get date / time formatter for the current locale
*
* @param int $dateLength
* @param int $timeLength
* @return IntlDateFormatter
*/
public function getFormatter($dateLength = IntlDateFormatter::MEDIUM, $timeLength = IntlDateFormatter::SHORT)
{
return parent::getFormatter($dateLength, $timeLength);
}
/**
* Return formatter in a given locale. Useful if localising in a format other than the current locale.
*
* @param string|null $locale The current locale, or null to use default
* @param string|null $pattern Custom pattern to use for this, if required
* @param int $dateLength
* @param int $timeLength
* @return IntlDateFormatter
*/
public function getCustomFormatter(
$locale = null,
$pattern = null,
$dateLength = IntlDateFormatter::MEDIUM,
$timeLength = IntlDateFormatter::MEDIUM
) {
return parent::getCustomFormatter($locale, $pattern, $dateLength, $timeLength);
}
/**
* Formatter used internally
*
* @internal
* @return IntlDateFormatter
*/
protected function getInternalFormatter()
{
$formatter = $this->getCustomFormatter(DBDate::ISO_LOCALE, DBDatetime::ISO_DATETIME);
$formatter->setLenient(false);
return $formatter;
}
/**
* Get standard ISO date format string
*
* @return string
*/
public function getISOFormat()
{
return self::ISO_DATETIME;
}
}