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.2.0
13: * @license https://opensource.org/licenses/mit-license.php MIT License
14: */
15: namespace Cake\I18n;
16:
17: use Cake\Chronos\Date as ChronosDate;
18: use Cake\Chronos\MutableDate;
19: use IntlDateFormatter;
20: use RuntimeException;
21:
22: /**
23: * Trait for date formatting methods shared by both Time & Date.
24: *
25: * This trait expects that the implementing class define static::$_toStringFormat.
26: */
27: trait DateFormatTrait
28: {
29: /**
30: * The default locale to be used for displaying formatted date strings.
31: *
32: * @var string
33: * @deprecated 3.2.9 Use static::setDefaultLocale() and static::getDefaultLocale() instead.
34: */
35: public static $defaultLocale;
36:
37: /**
38: * In-memory cache of date formatters
39: *
40: * @var \IntlDateFormatter[]
41: */
42: protected static $_formatters = [];
43:
44: /**
45: * The format to use when when converting this object to json
46: *
47: * The format should be either the formatting constants from IntlDateFormatter as
48: * described in (https://secure.php.net/manual/en/class.intldateformatter.php) or a pattern
49: * as specified in (http://www.icu-project.org/apiref/icu4c/classSimpleDateFormat.html#details)
50: *
51: * It is possible to provide an array of 2 constants. In this case, the first position
52: * will be used for formatting the date part of the object and the second position
53: * will be used to format the time part.
54: *
55: * @var string|array|int
56: * @see \Cake\I18n\Time::i18nFormat()
57: */
58: protected static $_jsonEncodeFormat = "yyyy-MM-dd'T'HH':'mm':'ssxxx";
59:
60: /**
61: * Caches whether or not this class is a subclass of a Date or MutableDate
62: *
63: * @var bool
64: */
65: protected static $_isDateInstance;
66:
67: /**
68: * Gets the default locale.
69: *
70: * @return string|null The default locale string to be used or null.
71: */
72: public static function getDefaultLocale()
73: {
74: return static::$defaultLocale;
75: }
76:
77: /**
78: * Sets the default locale.
79: *
80: * @param string|null $locale The default locale string to be used or null.
81: * @return void
82: */
83: public static function setDefaultLocale($locale = null)
84: {
85: static::$defaultLocale = $locale;
86: }
87:
88: /**
89: * Returns a nicely formatted date string for this object.
90: *
91: * The format to be used is stored in the static property `Time::niceFormat`.
92: *
93: * @param string|\DateTimeZone|null $timezone Timezone string or DateTimeZone object
94: * in which the date will be displayed. The timezone stored for this object will not
95: * be changed.
96: * @param string|null $locale The locale name in which the date should be displayed (e.g. pt-BR)
97: * @return string Formatted date string
98: */
99: public function nice($timezone = null, $locale = null)
100: {
101: return $this->i18nFormat(static::$niceFormat, $timezone, $locale);
102: }
103:
104: /**
105: * Returns a formatted string for this time object using the preferred format and
106: * language for the specified locale.
107: *
108: * It is possible to specify the desired format for the string to be displayed.
109: * You can either pass `IntlDateFormatter` constants as the first argument of this
110: * function, or pass a full ICU date formatting string as specified in the following
111: * resource: http://www.icu-project.org/apiref/icu4c/classSimpleDateFormat.html#details.
112: *
113: * Additional to `IntlDateFormatter` constants and date formatting string you can use
114: * Time::UNIX_TIMESTAMP_FORMAT to get a unix timestamp
115: *
116: * ### Examples
117: *
118: * ```
119: * $time = new Time('2014-04-20 22:10');
120: * $time->i18nFormat(); // outputs '4/20/14, 10:10 PM' for the en-US locale
121: * $time->i18nFormat(\IntlDateFormatter::FULL); // Use the full date and time format
122: * $time->i18nFormat([\IntlDateFormatter::FULL, \IntlDateFormatter::SHORT]); // Use full date but short time format
123: * $time->i18nFormat('yyyy-MM-dd HH:mm:ss'); // outputs '2014-04-20 22:10'
124: * $time->i18nFormat(Time::UNIX_TIMESTAMP_FORMAT); // outputs '1398031800'
125: * ```
126: *
127: * You can control the default format used through `Time::setToStringFormat()`.
128: *
129: * You can read about the available IntlDateFormatter constants at
130: * https://secure.php.net/manual/en/class.intldateformatter.php
131: *
132: * If you need to display the date in a different timezone than the one being used for
133: * this Time object without altering its internal state, you can pass a timezone
134: * string or object as the second parameter.
135: *
136: * Finally, should you need to use a different locale for displaying this time object,
137: * pass a locale string as the third parameter to this function.
138: *
139: * ### Examples
140: *
141: * ```
142: * $time = new Time('2014-04-20 22:10');
143: * $time->i18nFormat(null, null, 'de-DE');
144: * $time->i18nFormat(\IntlDateFormatter::FULL, 'Europe/Berlin', 'de-DE');
145: * ```
146: *
147: * You can control the default locale used through `Time::setDefaultLocale()`.
148: * If empty, the default will be taken from the `intl.default_locale` ini config.
149: *
150: * @param string|int|null $format Format string.
151: * @param string|\DateTimeZone|null $timezone Timezone string or DateTimeZone object
152: * in which the date will be displayed. The timezone stored for this object will not
153: * be changed.
154: * @param string|null $locale The locale name in which the date should be displayed (e.g. pt-BR)
155: * @return string|int Formatted and translated date string
156: */
157: public function i18nFormat($format = null, $timezone = null, $locale = null)
158: {
159: if ($format === Time::UNIX_TIMESTAMP_FORMAT) {
160: return $this->getTimestamp();
161: }
162:
163: $time = $this;
164:
165: if ($timezone) {
166: // Handle the immutable and mutable object cases.
167: $time = clone $this;
168: $time = $time->timezone($timezone);
169: }
170:
171: $format = $format !== null ? $format : static::$_toStringFormat;
172: $locale = $locale ?: static::$defaultLocale;
173:
174: return $this->_formatObject($time, $format, $locale);
175: }
176:
177: /**
178: * Returns a translated and localized date string.
179: * Implements what IntlDateFormatter::formatObject() is in PHP 5.5+
180: *
181: * @param \DateTime $date Date.
182: * @param string|int|array $format Format.
183: * @param string|null $locale The locale name in which the date should be displayed.
184: * @return string
185: */
186: protected function _formatObject($date, $format, $locale)
187: {
188: $pattern = $dateFormat = $timeFormat = $calendar = null;
189:
190: if (is_array($format)) {
191: list($dateFormat, $timeFormat) = $format;
192: } elseif (is_numeric($format)) {
193: $dateFormat = $format;
194: } else {
195: $dateFormat = $timeFormat = IntlDateFormatter::FULL;
196: $pattern = $format;
197: }
198:
199: if (preg_match('/@calendar=(japanese|buddhist|chinese|persian|indian|islamic|hebrew|coptic|ethiopic)/', $locale)) {
200: $calendar = IntlDateFormatter::TRADITIONAL;
201: } else {
202: $calendar = IntlDateFormatter::GREGORIAN;
203: }
204:
205: if ($locale === null) {
206: $locale = I18n::getLocale();
207: }
208:
209: $timezone = $date->getTimezone()->getName();
210: $key = "{$locale}.{$dateFormat}.{$timeFormat}.{$timezone}.{$calendar}.{$pattern}";
211:
212: if (!isset(static::$_formatters[$key])) {
213: if ($timezone === '+00:00' || $timezone === 'Z') {
214: $timezone = 'UTC';
215: } elseif ($timezone[0] === '+' || $timezone[0] === '-') {
216: $timezone = 'GMT' . $timezone;
217: }
218: $formatter = datefmt_create(
219: $locale,
220: $dateFormat,
221: $timeFormat,
222: $timezone,
223: $calendar,
224: $pattern
225: );
226: if (!$formatter) {
227: throw new RuntimeException(
228: 'Your version of icu does not support creating a date formatter for ' .
229: "`$key`. You should try to upgrade libicu and the intl extension."
230: );
231: }
232: static::$_formatters[$key] = $formatter;
233: }
234:
235: return static::$_formatters[$key]->format($date->format('U'));
236: }
237:
238: /**
239: * {@inheritDoc}
240: */
241: public function __toString()
242: {
243: return $this->i18nFormat();
244: }
245:
246: /**
247: * Resets the format used to the default when converting an instance of this type to
248: * a string
249: *
250: * @return void
251: */
252: public static function resetToStringFormat()
253: {
254: static::setToStringFormat([IntlDateFormatter::SHORT, IntlDateFormatter::SHORT]);
255: }
256:
257: /**
258: * Sets the default format used when type converting instances of this type to string
259: *
260: * The format should be either the formatting constants from IntlDateFormatter as
261: * described in (https://secure.php.net/manual/en/class.intldateformatter.php) or a pattern
262: * as specified in (http://www.icu-project.org/apiref/icu4c/classSimpleDateFormat.html#details)
263: *
264: * It is possible to provide an array of 2 constants. In this case, the first position
265: * will be used for formatting the date part of the object and the second position
266: * will be used to format the time part.
267: *
268: * @param string|array|int $format Format.
269: * @return void
270: */
271: public static function setToStringFormat($format)
272: {
273: static::$_toStringFormat = $format;
274: }
275:
276: /**
277: * Sets the default format used when converting this object to json
278: *
279: * The format should be either the formatting constants from IntlDateFormatter as
280: * described in (https://secure.php.net/manual/en/class.intldateformatter.php) or a pattern
281: * as specified in (http://www.icu-project.org/apiref/icu4c/classSimpleDateFormat.html#details)
282: *
283: * It is possible to provide an array of 2 constants. In this case, the first position
284: * will be used for formatting the date part of the object and the second position
285: * will be used to format the time part.
286: *
287: * @see \Cake\I18n\Time::i18nFormat()
288: * @param string|array|int $format Format.
289: * @return void
290: */
291: public static function setJsonEncodeFormat($format)
292: {
293: static::$_jsonEncodeFormat = $format;
294: }
295:
296: /**
297: * Returns a new Time object after parsing the provided time string based on
298: * the passed or configured date time format. This method is locale dependent,
299: * Any string that is passed to this function will be interpreted as a locale
300: * dependent string.
301: *
302: * When no $format is provided, the `toString` format will be used.
303: *
304: * If it was impossible to parse the provided time, null will be returned.
305: *
306: * Example:
307: *
308: * ```
309: * $time = Time::parseDateTime('10/13/2013 12:54am');
310: * $time = Time::parseDateTime('13 Oct, 2013 13:54', 'dd MMM, y H:mm');
311: * $time = Time::parseDateTime('10/10/2015', [IntlDateFormatter::SHORT, -1]);
312: * ```
313: *
314: * @param string $time The time string to parse.
315: * @param string|array|null $format Any format accepted by IntlDateFormatter.
316: * @return static|null
317: */
318: public static function parseDateTime($time, $format = null)
319: {
320: $dateFormat = $format ?: static::$_toStringFormat;
321: $timeFormat = $pattern = null;
322:
323: if (is_array($dateFormat)) {
324: list($newDateFormat, $timeFormat) = $dateFormat;
325: $dateFormat = $newDateFormat;
326: } else {
327: $pattern = $dateFormat;
328: $dateFormat = null;
329: }
330:
331: if (static::$_isDateInstance === null) {
332: static::$_isDateInstance =
333: is_subclass_of(static::class, ChronosDate::class) ||
334: is_subclass_of(static::class, MutableDate::class);
335: }
336:
337: $defaultTimezone = static::$_isDateInstance ? 'UTC' : date_default_timezone_get();
338: $formatter = datefmt_create(
339: static::$defaultLocale,
340: $dateFormat,
341: $timeFormat,
342: $defaultTimezone,
343: null,
344: $pattern
345: );
346: $time = $formatter->parse($time);
347: if ($time !== false) {
348: $result = new static('@' . $time);
349:
350: return static::$_isDateInstance ? $result : $result->setTimezone($defaultTimezone);
351: }
352:
353: return null;
354: }
355:
356: /**
357: * Returns a new Time object after parsing the provided $date string based on
358: * the passed or configured date time format. This method is locale dependent,
359: * Any string that is passed to this function will be interpreted as a locale
360: * dependent string.
361: *
362: * When no $format is provided, the `wordFormat` format will be used.
363: *
364: * If it was impossible to parse the provided time, null will be returned.
365: *
366: * Example:
367: *
368: * ```
369: * $time = Time::parseDate('10/13/2013');
370: * $time = Time::parseDate('13 Oct, 2013', 'dd MMM, y');
371: * $time = Time::parseDate('13 Oct, 2013', IntlDateFormatter::SHORT);
372: * ```
373: *
374: * @param string $date The date string to parse.
375: * @param string|int|null $format Any format accepted by IntlDateFormatter.
376: * @return static|null
377: */
378: public static function parseDate($date, $format = null)
379: {
380: if (is_int($format)) {
381: $format = [$format, -1];
382: }
383: $format = $format ?: static::$wordFormat;
384:
385: return static::parseDateTime($date, $format);
386: }
387:
388: /**
389: * Returns a new Time object after parsing the provided $time string based on
390: * the passed or configured date time format. This method is locale dependent,
391: * Any string that is passed to this function will be interpreted as a locale
392: * dependent string.
393: *
394: * When no $format is provided, the IntlDateFormatter::SHORT format will be used.
395: *
396: * If it was impossible to parse the provided time, null will be returned.
397: *
398: * Example:
399: *
400: * ```
401: * $time = Time::parseTime('11:23pm');
402: * ```
403: *
404: * @param string $time The time string to parse.
405: * @param string|int|null $format Any format accepted by IntlDateFormatter.
406: * @return static|null
407: */
408: public static function parseTime($time, $format = null)
409: {
410: if (is_int($format)) {
411: $format = [-1, $format];
412: }
413: $format = $format ?: [-1, IntlDateFormatter::SHORT];
414:
415: return static::parseDateTime($time, $format);
416: }
417:
418: /**
419: * Returns a string that should be serialized when converting this object to json
420: *
421: * @return string
422: */
423: public function jsonSerialize()
424: {
425: return $this->i18nFormat(static::$_jsonEncodeFormat);
426: }
427:
428: /**
429: * Get the difference formatter instance or overwrite the current one.
430: *
431: * @param \Cake\I18n\RelativeTimeFormatter|null $formatter The formatter instance when setting.
432: * @return \Cake\I18n\RelativeTimeFormatter The formatter instance.
433: */
434: public static function diffFormatter($formatter = null)
435: {
436: if ($formatter === null) {
437: // Use the static property defined in chronos.
438: if (static::$diffFormatter === null) {
439: static::$diffFormatter = new RelativeTimeFormatter();
440: }
441:
442: return static::$diffFormatter;
443: }
444:
445: return static::$diffFormatter = $formatter;
446: }
447:
448: /**
449: * Returns the data that should be displayed when debugging this object
450: *
451: * @return array
452: */
453: public function __debugInfo()
454: {
455: return [
456: 'time' => $this->format('Y-m-d H:i:s.uP'),
457: 'timezone' => $this->getTimezone()->getName(),
458: 'fixedNowTime' => static::hasTestNow() ? static::getTestNow()->format('Y-m-d H:i:s.uP') : false
459: ];
460: }
461: }
462: