[BUGFIX] Respect DateTimeImmutable in Extbase
[Packages/TYPO3.CMS.git] / typo3 / sysext / extbase / Classes / Property / TypeConverter / DateTimeConverter.php
1 <?php
2 namespace TYPO3\CMS\Extbase\Property\TypeConverter;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 /**
18 * Converter which transforms from different input formats into DateTime objects.
19 *
20 * Source can be either a string or an array. The date string is expected to be formatted
21 * according to DEFAULT_DATE_FORMAT.
22 *
23 * But the default date format can be overridden in the initialize*Action() method like this::
24 *
25 * $this->arguments['<argumentName>']
26 * ->getPropertyMappingConfiguration()
27 * ->forProperty('<propertyName>') // this line can be skipped in order to specify the format for all properties
28 * ->setTypeConverterOption(\TYPO3\CMS\Extbase\Property\TypeConverter\DateTimeConverter::class, \TYPO3\CMS\Extbase\Property\TypeConverter\DateTimeConverter::CONFIGURATION_DATE_FORMAT, '<dateFormat>');
29 *
30 * If the source is of type array, it is possible to override the format in the source::
31 *
32 * array(
33 * 'date' => '<dateString>',
34 * 'dateFormat' => '<dateFormat>'
35 * );
36 *
37 * By using an array as source you can also override time and timezone of the created DateTime object::
38 *
39 * array(
40 * 'date' => '<dateString>',
41 * 'hour' => '<hour>', // integer
42 * 'minute' => '<minute>', // integer
43 * 'seconds' => '<seconds>', // integer
44 * 'timezone' => '<timezone>', // string, see http://www.php.net/manual/timezones.php
45 * );
46 *
47 * As an alternative to providing the date as string, you might supply day, month and year as array items each::
48 *
49 * array(
50 * 'day' => '<day>', // integer
51 * 'month' => '<month>', // integer
52 * 'year' => '<year>', // integer
53 * );
54 */
55 class DateTimeConverter extends \TYPO3\CMS\Extbase\Property\TypeConverter\AbstractTypeConverter
56 {
57 /**
58 * @var string
59 */
60 const CONFIGURATION_DATE_FORMAT = 'dateFormat';
61
62 /**
63 * The default date format is "YYYY-MM-DDT##:##:##+##:##", for example "2005-08-15T15:52:01+00:00"
64 * according to the W3C standard @see http://www.w3.org/TR/NOTE-datetime.html
65 *
66 * @var string
67 */
68 const DEFAULT_DATE_FORMAT = \DateTime::W3C;
69
70 /**
71 * @var array<string>
72 */
73 protected $sourceTypes = ['string', 'integer', 'array'];
74
75 /**
76 * @var string
77 */
78 protected $targetType = 'DateTime';
79
80 /**
81 * @var int
82 */
83 protected $priority = 10;
84
85 /**
86 * If conversion is possible.
87 *
88 * @param string $source
89 * @param string $targetType
90 * @return bool
91 * @internal only to be used within Extbase, not part of TYPO3 Core API.
92 */
93 public function canConvertFrom($source, $targetType)
94 {
95 if (!is_callable([$targetType, 'createFromFormat'])) {
96 return false;
97 }
98 if (is_array($source)) {
99 return true;
100 }
101 if (is_int($source)) {
102 return true;
103 }
104 return is_string($source);
105 }
106
107 /**
108 * Converts $source to a \DateTime using the configured dateFormat
109 *
110 * @param string|int|array $source the string to be converted to a \DateTime object
111 * @param string $targetType must be "DateTime"
112 * @param array $convertedChildProperties not used currently
113 * @param \TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationInterface $configuration
114 * @return \DateTimeInterface|\TYPO3\CMS\Extbase\Error\Error
115 * @throws \TYPO3\CMS\Extbase\Property\Exception\TypeConverterException
116 * @internal only to be used within Extbase, not part of TYPO3 Core API.
117 */
118 public function convertFrom($source, $targetType, array $convertedChildProperties = [], \TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationInterface $configuration = null)
119 {
120 $dateFormat = $this->getDefaultDateFormat($configuration);
121 if (is_string($source)) {
122 $dateAsString = $source;
123 } elseif (is_int($source)) {
124 $dateAsString = strval($source);
125 } else {
126 if (isset($source['date']) && is_string($source['date'])) {
127 $dateAsString = $source['date'];
128 } elseif (isset($source['date']) && is_int($source['date'])) {
129 $dateAsString = strval($source['date']);
130 } elseif ($this->isDatePartKeysProvided($source)) {
131 if ($source['day'] < 1 || $source['month'] < 1 || $source['year'] < 1) {
132 return new \TYPO3\CMS\Extbase\Error\Error('Could not convert the given date parts into a DateTime object because one or more parts were 0.', 1333032779);
133 }
134 $dateAsString = sprintf('%d-%d-%d', $source['year'], $source['month'], $source['day']);
135 } else {
136 throw new \TYPO3\CMS\Extbase\Property\Exception\TypeConverterException('Could not convert the given source into a DateTime object because it was not an array with a valid date as a string', 1308003914);
137 }
138 if (isset($source['dateFormat']) && $source['dateFormat'] !== '') {
139 $dateFormat = $source['dateFormat'];
140 }
141 }
142 if ($dateAsString === '') {
143 return null;
144 }
145 if (ctype_digit($dateAsString) && $configuration === null && (!is_array($source) || !isset($source['dateFormat']))) {
146 $dateFormat = 'U';
147 }
148 if (is_array($source) && isset($source['timezone']) && (string)$source['timezone'] !== '') {
149 try {
150 $timezone = new \DateTimeZone($source['timezone']);
151 } catch (\Exception $e) {
152 throw new \TYPO3\CMS\Extbase\Property\Exception\TypeConverterException('The specified timezone "' . $source['timezone'] . '" is invalid.', 1308240974);
153 }
154 $date = $targetType::createFromFormat($dateFormat, $dateAsString, $timezone);
155 } else {
156 $date = $targetType::createFromFormat($dateFormat, $dateAsString);
157 }
158 if ($date === false) {
159 return new \TYPO3\CMS\Extbase\Validation\Error('The date "%s" was not recognized (for format "%s").', 1307719788, [$dateAsString, $dateFormat]);
160 }
161 if (is_array($source)) {
162 $date = $this->overrideTimeIfSpecified($date, $source);
163 }
164 return $date;
165 }
166
167 /**
168 * Returns whether date information (day, month, year) are present as keys in $source.
169 *
170 * @param array $source
171 * @return bool
172 */
173 protected function isDatePartKeysProvided(array $source)
174 {
175 return isset($source['day']) && ctype_digit($source['day'])
176 && isset($source['month']) && ctype_digit($source['month'])
177 && isset($source['year']) && ctype_digit($source['year']);
178 }
179
180 /**
181 * Determines the default date format to use for the conversion.
182 * If no format is specified in the mapping configuration DEFAULT_DATE_FORMAT is used.
183 *
184 * @param \TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationInterface $configuration
185 * @return string
186 * @throws \TYPO3\CMS\Extbase\Property\Exception\InvalidPropertyMappingConfigurationException
187 */
188 protected function getDefaultDateFormat(\TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationInterface $configuration = null)
189 {
190 if ($configuration === null) {
191 return self::DEFAULT_DATE_FORMAT;
192 }
193 $dateFormat = $configuration->getConfigurationValue(DateTimeConverter::class, self::CONFIGURATION_DATE_FORMAT);
194 if ($dateFormat === null) {
195 return self::DEFAULT_DATE_FORMAT;
196 }
197 if ($dateFormat !== null && !is_string($dateFormat)) {
198 throw new \TYPO3\CMS\Extbase\Property\Exception\InvalidPropertyMappingConfigurationException('CONFIGURATION_DATE_FORMAT must be of type string, "' . (is_object($dateFormat) ? get_class($dateFormat) : gettype($dateFormat)) . '" given', 1307719569);
199 }
200 return $dateFormat;
201 }
202
203 /**
204 * Overrides hour, minute & second of the given date with the values in the $source array
205 *
206 * @param \DateTimeInterface $date
207 * @param array $source
208 * @return \DateTimeInterface
209 */
210 protected function overrideTimeIfSpecified(\DateTimeInterface $date, array $source): \DateTimeInterface
211 {
212 if (!isset($source['hour']) && !isset($source['minute']) && !isset($source['second'])) {
213 return $date;
214 }
215 $hour = isset($source['hour']) ? (int)$source['hour'] : 0;
216 $minute = isset($source['minute']) ? (int)$source['minute'] : 0;
217 $second = isset($source['second']) ? (int)$source['second'] : 0;
218 return $date->setTime($hour, $minute, $second);
219 }
220 }