1: <?php
2: /**
3: * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
4: * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
5: *
6: * Licensed under The MIT License
7: * For full copyright and license information, please see the LICENSE.txt
8: * Redistributions of files must retain the above copyright notice.
9: *
10: * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
11: * @link https://cakephp.org CakePHP(tm) Project
12: * @since 3.0.0
13: * @license https://opensource.org/licenses/mit-license.php MIT License
14: */
15: namespace Cake\Database\Type;
16:
17: use Cake\Database\Driver;
18: use Cake\Database\Type;
19: use Cake\Database\TypeInterface;
20: use Cake\Database\Type\BatchCastingInterface;
21: use DateTimeImmutable;
22: use DateTimeInterface;
23: use DateTimeZone;
24: use Exception;
25: use PDO;
26: use RuntimeException;
27:
28: /**
29: * Datetime type converter.
30: *
31: * Use to convert datetime instances to strings & back.
32: */
33: class DateTimeType extends Type implements TypeInterface, BatchCastingInterface
34: {
35: /**
36: * Identifier name for this type.
37: *
38: * (This property is declared here again so that the inheritance from
39: * Cake\Database\Type can be removed in the future.)
40: *
41: * @var string|null
42: */
43: protected $_name;
44:
45: /**
46: * The class to use for representing date objects
47: *
48: * This property can only be used before an instance of this type
49: * class is constructed. After that use `useMutable()` or `useImmutable()` instead.
50: *
51: * @var string
52: * @deprecated 3.2.0 Use DateTimeType::useMutable() or DateTimeType::useImmutable() instead.
53: */
54: public static $dateTimeClass = 'Cake\I18n\Time';
55:
56: /**
57: * Whether or not we want to override the time of the converted Time objects
58: * so it points to the start of the day.
59: *
60: * This is primarily to avoid subclasses needing to re-implement the same functionality.
61: *
62: * @var bool
63: */
64: protected $setToDateStart = false;
65:
66: /**
67: * String format to use for DateTime parsing
68: *
69: * @var string|array
70: */
71: protected $_format = [
72: 'Y-m-d H:i:s',
73: 'Y-m-d\TH:i:sP',
74: ];
75:
76: /**
77: * Whether dates should be parsed using a locale aware parser
78: * when marshalling string inputs.
79: *
80: * @var bool
81: */
82: protected $_useLocaleParser = false;
83:
84: /**
85: * The date format to use for parsing incoming dates for marshalling.
86: *
87: * @var string|array|int
88: */
89: protected $_localeFormat;
90:
91: /**
92: * An instance of the configured dateTimeClass, used to quickly generate
93: * new instances without calling the constructor.
94: *
95: * @var \DateTime
96: */
97: protected $_datetimeInstance;
98:
99: /**
100: * The classname to use when creating objects.
101: *
102: * @var string
103: */
104: protected $_className;
105:
106: /**
107: * Timezone instance.
108: *
109: * @var \DateTimeZone|null
110: */
111: protected $dbTimezone;
112:
113: /**
114: * {@inheritDoc}
115: */
116: public function __construct($name = null)
117: {
118: $this->_name = $name;
119:
120: $this->_setClassName(static::$dateTimeClass, 'DateTime');
121: }
122:
123: /**
124: * Convert DateTime instance into strings.
125: *
126: * @param string|int|\DateTime|\DateTimeImmutable $value The value to convert.
127: * @param \Cake\Database\Driver $driver The driver instance to convert with.
128: * @return string|null
129: */
130: public function toDatabase($value, Driver $driver)
131: {
132: if ($value === null || is_string($value)) {
133: return $value;
134: }
135: if (is_int($value)) {
136: $class = $this->_className;
137: $value = new $class('@' . $value);
138: }
139:
140: $format = (array)$this->_format;
141:
142: if ($this->dbTimezone !== null
143: && $this->dbTimezone->getName() !== $value->getTimezone()->getName()
144: ) {
145: if (!$value instanceof DateTimeImmutable) {
146: $value = clone $value;
147: }
148: $value = $value->setTimezone($this->dbTimezone);
149: }
150:
151: return $value->format(array_shift($format));
152: }
153:
154: /**
155: * Set database timezone.
156: *
157: * Specified timezone will be set for DateTime objects before generating
158: * datetime string for saving to database. If `null` no timezone conversion
159: * will be done.
160: *
161: * @param string|\DateTimeZone|null $timezone Database timezone.
162: * @return $this
163: */
164: public function setTimezone($timezone)
165: {
166: if (is_string($timezone)) {
167: $timezone = new DateTimeZone($timezone);
168: }
169: $this->dbTimezone = $timezone;
170:
171: return $this;
172: }
173:
174: /**
175: * Convert strings into DateTime instances.
176: *
177: * @param string $value The value to convert.
178: * @param \Cake\Database\Driver $driver The driver instance to convert with.
179: * @return \Cake\I18n\Time|\DateTime|null
180: */
181: public function toPHP($value, Driver $driver)
182: {
183: if ($value === null || strpos($value, '0000-00-00') === 0) {
184: return null;
185: }
186:
187: $instance = clone $this->_datetimeInstance;
188: $instance = $instance->modify($value);
189:
190: if ($this->setToDateStart) {
191: $instance = $instance->setTime(0, 0, 0);
192: }
193:
194: return $instance;
195: }
196:
197: /**
198: * {@inheritDoc}
199: *
200: * @return array
201: */
202: public function manyToPHP(array $values, array $fields, Driver $driver)
203: {
204: foreach ($fields as $field) {
205: if (!isset($values[$field])) {
206: continue;
207: }
208:
209: if (strpos($values[$field], '0000-00-00') === 0) {
210: $values[$field] = null;
211: continue;
212: }
213:
214: $instance = clone $this->_datetimeInstance;
215: $instance = $instance->modify($values[$field]);
216:
217: if ($this->setToDateStart) {
218: $instance = $instance->setTime(0, 0, 0);
219: }
220:
221: $values[$field] = $instance;
222: }
223:
224: return $values;
225: }
226:
227: /**
228: * Convert request data into a datetime object.
229: *
230: * @param mixed $value Request data
231: * @return \DateTimeInterface|null
232: */
233: public function marshal($value)
234: {
235: if ($value instanceof DateTimeInterface) {
236: return $value;
237: }
238:
239: $class = $this->_className;
240: try {
241: $compare = $date = false;
242: if ($value === '' || $value === null || $value === false || $value === true) {
243: return null;
244: }
245: $isString = is_string($value);
246: if (ctype_digit($value)) {
247: $date = new $class('@' . $value);
248: } elseif ($isString && $this->_useLocaleParser) {
249: return $this->_parseValue($value);
250: } elseif ($isString) {
251: $date = new $class($value);
252: $compare = true;
253: }
254: if ($compare && $date && !$this->_compare($date, $value)) {
255: return $value;
256: }
257: if ($date) {
258: return $date;
259: }
260: } catch (Exception $e) {
261: return $value;
262: }
263:
264: if (is_array($value) && implode('', $value) === '') {
265: return null;
266: }
267: $value += ['hour' => 0, 'minute' => 0, 'second' => 0];
268:
269: $format = '';
270: if (isset($value['year'], $value['month'], $value['day']) &&
271: (is_numeric($value['year']) && is_numeric($value['month']) && is_numeric($value['day']))
272: ) {
273: $format .= sprintf('%d-%02d-%02d', $value['year'], $value['month'], $value['day']);
274: }
275:
276: if (isset($value['meridian']) && (int)$value['hour'] === 12) {
277: $value['hour'] = 0;
278: }
279: if (isset($value['meridian'])) {
280: $value['hour'] = strtolower($value['meridian']) === 'am' ? $value['hour'] : $value['hour'] + 12;
281: }
282: $format .= sprintf(
283: '%s%02d:%02d:%02d',
284: empty($format) ? '' : ' ',
285: $value['hour'],
286: $value['minute'],
287: $value['second']
288: );
289: $tz = isset($value['timezone']) ? $value['timezone'] : null;
290:
291: return new $class($format, $tz);
292: }
293:
294: /**
295: * @param \Cake\I18n\Time|\DateTime $date DateTime object
296: * @param mixed $value Request data
297: * @return bool
298: */
299: protected function _compare($date, $value)
300: {
301: foreach ((array)$this->_format as $format) {
302: if ($date->format($format) === $value) {
303: return true;
304: }
305: }
306:
307: return false;
308: }
309:
310: /**
311: * Sets whether or not to parse dates passed to the marshal() function
312: * by using a locale aware parser.
313: *
314: * @param bool $enable Whether or not to enable
315: * @return $this
316: */
317: public function useLocaleParser($enable = true)
318: {
319: if ($enable === false) {
320: $this->_useLocaleParser = $enable;
321:
322: return $this;
323: }
324: if (method_exists($this->_className, 'parseDateTime')) {
325: $this->_useLocaleParser = $enable;
326:
327: return $this;
328: }
329: throw new RuntimeException(
330: sprintf('Cannot use locale parsing with the %s class', $this->_className)
331: );
332: }
333:
334: /**
335: * Sets the format string to use for parsing dates in this class. The formats
336: * that are accepted are documented in the `Cake\I18n\Time::parseDateTime()`
337: * function.
338: *
339: * @param string|array $format The format in which the string are passed.
340: * @see \Cake\I18n\Time::parseDateTime()
341: * @return $this
342: */
343: public function setLocaleFormat($format)
344: {
345: $this->_localeFormat = $format;
346:
347: return $this;
348: }
349:
350: /**
351: * Change the preferred class name to the FrozenTime implementation.
352: *
353: * @return $this
354: */
355: public function useImmutable()
356: {
357: $this->_setClassName('Cake\I18n\FrozenTime', 'DateTimeImmutable');
358:
359: return $this;
360: }
361:
362: /**
363: * Set the classname to use when building objects.
364: *
365: * @param string $class The classname to use.
366: * @param string $fallback The classname to use when the preferred class does not exist.
367: * @return void
368: */
369: protected function _setClassName($class, $fallback)
370: {
371: if (!class_exists($class)) {
372: $class = $fallback;
373: }
374: $this->_className = $class;
375: $this->_datetimeInstance = new $this->_className();
376: }
377:
378: /**
379: * Get the classname used for building objects.
380: *
381: * @return string
382: */
383: public function getDateTimeClassName()
384: {
385: return $this->_className;
386: }
387:
388: /**
389: * Change the preferred class name to the mutable Time implementation.
390: *
391: * @return $this
392: */
393: public function useMutable()
394: {
395: $this->_setClassName('Cake\I18n\Time', 'DateTime');
396:
397: return $this;
398: }
399:
400: /**
401: * Converts a string into a DateTime object after parsing it using the locale
402: * aware parser with the specified format.
403: *
404: * @param string $value The value to parse and convert to an object.
405: * @return \Cake\I18n\Time|null
406: */
407: protected function _parseValue($value)
408: {
409: /* @var \Cake\I18n\Time $class */
410: $class = $this->_className;
411:
412: return $class::parseDateTime($value, $this->_localeFormat);
413: }
414:
415: /**
416: * Casts given value to Statement equivalent
417: *
418: * @param mixed $value value to be converted to PDO statement
419: * @param \Cake\Database\Driver $driver object from which database preferences and configuration will be extracted
420: *
421: * @return mixed
422: */
423: public function toStatement($value, Driver $driver)
424: {
425: return PDO::PARAM_STR;
426: }
427: }
428: