Files
cms-core/Classes/Domain/DateTimeFactory.php
T

148 lines
6.1 KiB
PHP

<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Domain;
use TYPO3\CMS\Core\Database\Query\QueryHelper;
use TYPO3\CMS\Core\Schema\Field\DateTimeFieldType;
use TYPO3\CMS\Core\Utility\MathUtility;
/**
* @internal
*/
final readonly class DateTimeFactory
{
public static function createFromDatabaseValue(int|string|null $value, DateTimeFieldType $fieldInformation): ?\DateTimeImmutable
{
return self::fromDatabase(
$value,
$fieldInformation->isNullable(),
$fieldInformation->getFormat(),
$fieldInformation->getPersistenceType(),
);
}
public static function createFromDatabaseValueAndTCAConfig(int|string|null $value, array $fieldConfig): ?\DateTimeImmutable
{
$persistenceType = in_array($fieldConfig['dbType'] ?? null, QueryHelper::getDateTimeTypes(), true) ? $fieldConfig['dbType'] : null;
$isNative = $persistenceType !== null;
$isNullable = (bool)($fieldConfig['nullable'] ?? $isNative);
$format = self::getFormatFromTCAConfig($fieldConfig);
return self::fromDatabase(
$value,
$isNullable,
$format,
$persistenceType
);
}
public static function getFormatFromTCAConfig(array $fieldConfig): string
{
$format = $fieldConfig['format'] ?? null;
$persistenceType = in_array($fieldConfig['dbType'] ?? null, QueryHelper::getDateTimeTypes(), true) ? $fieldConfig['dbType'] : null;
// A native time field must not be formatted as date
if (($format === 'datetime' || $format === 'datetimesec' || $format === 'date') && $persistenceType === 'time') {
return 'timesec';
}
// A native date field must not be formatted as time
if (($format === 'time' || $format === 'timesec' || $format === 'datetime' || $format === 'datetimesec') && $persistenceType === 'date') {
return 'date';
}
if (in_array($format, ['datetime', 'date', 'time', 'timesec', 'datetimesec'], true)) {
return $format;
}
if ($persistenceType !== null) {
return $persistenceType === 'time' ? 'timesec' : $persistenceType;
}
return 'datetime';
}
/**
* Create a DateTimeImmutable object from a unix timestamp in server localtime
*
* Alternative to \DateTimeImmutable('@…') which forces UTC timezone
*/
public static function createFromTimestamp(int $timestamp): \DateTimeImmutable
{
// Create a new DateTime object in current timezone
//
// Note: As documented by PHP, `\DateTime` or `\DateTimeImmutable`
// objects created from timestamps (e.g., '@12345678') as the
// first constructor argument will use UTC as timezone instead of localtime,
// therefore we must not initialize with a timestamp directly.
$datetime = new \DateTimeImmutable();
// Apply timestamp (which will not change the objects timezone)
return $datetime->setTimestamp($timestamp);
}
private static function fromDatabase(
int|string|null $value,
bool $isNullable,
string $format,
?string $persistenceType
): ?\DateTimeImmutable {
if ($value === null || $value === '') {
return null;
}
$emptyFormat = QueryHelper::getDateTimeFormats()[$persistenceType ?? '']['empty'] ?? null;
// A regular empty value is null for nullable fields
$emptyValue = $isNullable ? null : ($emptyFormat ?? 0);
// A legacy empty value is "0000-00-00" or "0000-00-00 00:00:00" stored
// in a nullable native DATE or DATETIME field (which should already use
// a proper `null` value, but still has a legacy empty value set).
$legacyEmptyValue = $persistenceType === 'date' || $persistenceType === 'datetime' ? $emptyFormat : null;
if (MathUtility::canBeInterpretedAsInteger($value)) {
$value = (int)$value;
}
if ($value === $emptyValue || $value === $legacyEmptyValue) {
return null;
}
try {
$datetime = match (true) {
is_int($value) && ($format === 'time' || $format === 'timesec') => new \DateTimeImmutable(
// time(sec) is stored as elapsed seconds in DB and has no defined date associated.
// Per convention we map to 1970-01-01 for the sake of a reliable date.
// We still want a PHP localtime timezone in the DateTime object set,
// therefore we interpret the second as UTC time on 1970-01-01T00:00:00
// and map the resulting value to PHP localtime
gmdate(DateTimeFormat::ISO8601_LOCALTIME, $value)
),
// Unix timestamp
is_int($value) => self::createFromTimestamp($value),
// The database always contains server localtime in native fields.
// The field value is something like "2016-01-01" or "2016-01-01 10:11:12.
default => new \DateTimeImmutable($value),
};
} catch (\DateMalformedStringException $e) {
throw new \InvalidArgumentException('Invalid date provided', 1743159490, $e);
}
return match ($format) {
// time(sec) is stored as elapsed seconds in DB, hence we normalize it as time on 1970-01-01 for consistency
'time' => $datetime->setDate(1970, 1, 1)->setTime((int)$datetime->format('H'), (int)$datetime->format('i'), 0),
'timesec' => $datetime->setDate(1970, 1, 1),
'date' => $datetime->setTime(0, 0, 0),
// default case also for 'datetimesec'
default => $datetime,
};
}
}