1<?php 2 3/* 4 * This file is part of the Symfony package. 5 * 6 * (c) Fabien Potencier <fabien@symfony.com> 7 * 8 * For the full copyright and license information, please view the LICENSE 9 * file that was distributed with this source code. 10 */ 11 12namespace Symfony\Component\Intl\DateFormatter; 13 14use Symfony\Component\Intl\Globals\IntlGlobals; 15use Symfony\Component\Intl\DateFormatter\DateFormat\FullTransformer; 16use Symfony\Component\Intl\Exception\MethodNotImplementedException; 17use Symfony\Component\Intl\Exception\MethodArgumentNotImplementedException; 18use Symfony\Component\Intl\Exception\MethodArgumentValueNotImplementedException; 19use Symfony\Component\Intl\Locale\Locale; 20 21/** 22 * Replacement for PHP's native {@link \IntlDateFormatter} class. 23 * 24 * The only methods currently supported in this class are: 25 * 26 * - {@link __construct} 27 * - {@link create} 28 * - {@link format} 29 * - {@link getCalendar} 30 * - {@link getDateType} 31 * - {@link getErrorCode} 32 * - {@link getErrorMessage} 33 * - {@link getLocale} 34 * - {@link getPattern} 35 * - {@link getTimeType} 36 * - {@link getTimeZoneId} 37 * - {@link isLenient} 38 * - {@link parse} 39 * - {@link setLenient} 40 * - {@link setPattern} 41 * - {@link setTimeZoneId} 42 * - {@link setTimeZone} 43 * 44 * @author Igor Wiedler <igor@wiedler.ch> 45 * @author Bernhard Schussek <bschussek@gmail.com> 46 */ 47class IntlDateFormatter 48{ 49 /** 50 * The error code from the last operation. 51 * 52 * @var int 53 */ 54 protected $errorCode = IntlGlobals::U_ZERO_ERROR; 55 56 /** 57 * The error message from the last operation. 58 * 59 * @var string 60 */ 61 protected $errorMessage = 'U_ZERO_ERROR'; 62 63 /* date/time format types */ 64 const NONE = -1; 65 const FULL = 0; 66 const LONG = 1; 67 const MEDIUM = 2; 68 const SHORT = 3; 69 70 /* calendar formats */ 71 const TRADITIONAL = 0; 72 const GREGORIAN = 1; 73 74 /** 75 * Patterns used to format the date when no pattern is provided. 76 * 77 * @var array 78 */ 79 private $defaultDateFormats = array( 80 self::NONE => '', 81 self::FULL => 'EEEE, LLLL d, y', 82 self::LONG => 'LLLL d, y', 83 self::MEDIUM => 'LLL d, y', 84 self::SHORT => 'M/d/yy', 85 ); 86 87 /** 88 * Patterns used to format the time when no pattern is provided. 89 * 90 * @var array 91 */ 92 private $defaultTimeFormats = array( 93 self::FULL => 'h:mm:ss a zzzz', 94 self::LONG => 'h:mm:ss a z', 95 self::MEDIUM => 'h:mm:ss a', 96 self::SHORT => 'h:mm a', 97 ); 98 99 /** 100 * @var int 101 */ 102 private $datetype; 103 104 /** 105 * @var int 106 */ 107 private $timetype; 108 109 /** 110 * @var string 111 */ 112 private $pattern; 113 114 /** 115 * @var \DateTimeZone 116 */ 117 private $dateTimeZone; 118 119 /** 120 * @var bool 121 */ 122 private $uninitializedTimeZoneId = false; 123 124 /** 125 * @var string 126 */ 127 private $timeZoneId; 128 129 /** 130 * Constructor. 131 * 132 * @param string $locale The locale code. The only currently supported locale is "en". 133 * @param int $datetype Type of date formatting, one of the format type constants 134 * @param int $timetype Type of time formatting, one of the format type constants 135 * @param string $timezone Timezone identifier 136 * @param int $calendar Calendar to use for formatting or parsing. The only currently 137 * supported value is IntlDateFormatter::GREGORIAN. 138 * @param string $pattern Optional pattern to use when formatting 139 * 140 * @see http://www.php.net/manual/en/intldateformatter.create.php 141 * @see http://userguide.icu-project.org/formatparse/datetime 142 * 143 * @throws MethodArgumentValueNotImplementedException When $locale different than "en" is passed 144 * @throws MethodArgumentValueNotImplementedException When $calendar different than GREGORIAN is passed 145 */ 146 public function __construct($locale, $datetype, $timetype, $timezone = null, $calendar = self::GREGORIAN, $pattern = null) 147 { 148 if ('en' !== $locale) { 149 throw new MethodArgumentValueNotImplementedException(__METHOD__, 'locale', $locale, 'Only the locale "en" is supported'); 150 } 151 152 if (self::GREGORIAN !== $calendar) { 153 throw new MethodArgumentValueNotImplementedException(__METHOD__, 'calendar', $calendar, 'Only the GREGORIAN calendar is supported'); 154 } 155 156 $this->datetype = $datetype; 157 $this->timetype = $timetype; 158 159 $this->setPattern($pattern); 160 $this->setTimeZoneId($timezone); 161 } 162 163 /** 164 * Static constructor. 165 * 166 * @param string $locale The locale code. The only currently supported locale is "en". 167 * @param int $datetype Type of date formatting, one of the format type constants 168 * @param int $timetype Type of time formatting, one of the format type constants 169 * @param string $timezone Timezone identifier 170 * @param int $calendar Calendar to use for formatting or parsing; default is Gregorian. 171 * One of the calendar constants. 172 * @param string $pattern Optional pattern to use when formatting 173 * 174 * @return IntlDateFormatter 175 * 176 * @see http://www.php.net/manual/en/intldateformatter.create.php 177 * @see http://userguide.icu-project.org/formatparse/datetime 178 * 179 * @throws MethodArgumentValueNotImplementedException When $locale different than "en" is passed 180 * @throws MethodArgumentValueNotImplementedException When $calendar different than GREGORIAN is passed 181 */ 182 public static function create($locale, $datetype, $timetype, $timezone = null, $calendar = self::GREGORIAN, $pattern = null) 183 { 184 return new self($locale, $datetype, $timetype, $timezone, $calendar, $pattern); 185 } 186 187 /** 188 * Format the date/time value (timestamp) as a string. 189 * 190 * @param int|\DateTime $timestamp The timestamp to format. \DateTime objects 191 * are supported as of PHP 5.3.4. 192 * 193 * @return string|bool The formatted value or false if formatting failed. 194 * 195 * @see http://www.php.net/manual/en/intldateformatter.format.php 196 * 197 * @throws MethodArgumentValueNotImplementedException If one of the formatting characters is not implemented 198 */ 199 public function format($timestamp) 200 { 201 // intl allows timestamps to be passed as arrays - we don't 202 if (is_array($timestamp)) { 203 $message = PHP_VERSION_ID >= 50304 ? 204 'Only integer Unix timestamps and DateTime objects are supported' : 205 'Only integer Unix timestamps are supported'; 206 207 throw new MethodArgumentValueNotImplementedException(__METHOD__, 'timestamp', $timestamp, $message); 208 } 209 210 // behave like the intl extension 211 $argumentError = null; 212 if (PHP_VERSION_ID < 50304 && !is_int($timestamp)) { 213 $argumentError = 'datefmt_format: takes either an array or an integer timestamp value '; 214 } elseif (PHP_VERSION_ID >= 50304 && !is_int($timestamp) && !$timestamp instanceof \DateTime) { 215 $argumentError = 'datefmt_format: takes either an array or an integer timestamp value or a DateTime object'; 216 if (PHP_VERSION_ID >= 50500 && !is_int($timestamp)) { 217 $argumentError = sprintf('datefmt_format: string \'%s\' is not numeric, which would be required for it to be a valid date', $timestamp); 218 } 219 } 220 221 if (null !== $argumentError) { 222 IntlGlobals::setError(IntlGlobals::U_ILLEGAL_ARGUMENT_ERROR, $argumentError); 223 $this->errorCode = IntlGlobals::getErrorCode(); 224 $this->errorMessage = IntlGlobals::getErrorMessage(); 225 226 return false; 227 } 228 229 // As of PHP 5.3.4, IntlDateFormatter::format() accepts DateTime instances 230 if (PHP_VERSION_ID >= 50304 && $timestamp instanceof \DateTime) { 231 $timestamp = $timestamp->getTimestamp(); 232 } 233 234 $transformer = new FullTransformer($this->getPattern(), $this->getTimeZoneId()); 235 $formatted = $transformer->format($this->createDateTime($timestamp)); 236 237 // behave like the intl extension 238 IntlGlobals::setError(IntlGlobals::U_ZERO_ERROR); 239 $this->errorCode = IntlGlobals::getErrorCode(); 240 $this->errorMessage = IntlGlobals::getErrorMessage(); 241 242 return $formatted; 243 } 244 245 /** 246 * Not supported. Formats an object. 247 * 248 * @param object $object 249 * @param mixed $format 250 * @param string $locale 251 * 252 * @return string The formatted value 253 * 254 * @see http://www.php.net/manual/en/intldateformatter.formatobject.php 255 * 256 * @throws MethodNotImplementedException 257 */ 258 public function formatObject($object, $format = null, $locale = null) 259 { 260 throw new MethodNotImplementedException(__METHOD__); 261 } 262 263 /** 264 * Returns the formatter's calendar. 265 * 266 * @return int The calendar being used by the formatter. Currently always returns 267 * IntlDateFormatter::GREGORIAN. 268 * 269 * @see http://www.php.net/manual/en/intldateformatter.getcalendar.php 270 */ 271 public function getCalendar() 272 { 273 return self::GREGORIAN; 274 } 275 276 /** 277 * Not supported. Returns the formatter's calendar object. 278 * 279 * @return object The calendar's object being used by the formatter 280 * 281 * @see http://www.php.net/manual/en/intldateformatter.getcalendarobject.php 282 * 283 * @throws MethodNotImplementedException 284 */ 285 public function getCalendarObject() 286 { 287 throw new MethodNotImplementedException(__METHOD__); 288 } 289 290 /** 291 * Returns the formatter's datetype. 292 * 293 * @return int The current value of the formatter 294 * 295 * @see http://www.php.net/manual/en/intldateformatter.getdatetype.php 296 */ 297 public function getDateType() 298 { 299 return $this->datetype; 300 } 301 302 /** 303 * Returns formatter's last error code. Always returns the U_ZERO_ERROR class constant value. 304 * 305 * @return int The error code from last formatter call 306 * 307 * @see http://www.php.net/manual/en/intldateformatter.geterrorcode.php 308 */ 309 public function getErrorCode() 310 { 311 return $this->errorCode; 312 } 313 314 /** 315 * Returns formatter's last error message. Always returns the U_ZERO_ERROR_MESSAGE class constant value. 316 * 317 * @return string The error message from last formatter call 318 * 319 * @see http://www.php.net/manual/en/intldateformatter.geterrormessage.php 320 */ 321 public function getErrorMessage() 322 { 323 return $this->errorMessage; 324 } 325 326 /** 327 * Returns the formatter's locale. 328 * 329 * @param int $type Not supported. The locale name type to return (Locale::VALID_LOCALE or Locale::ACTUAL_LOCALE) 330 * 331 * @return string The locale used to create the formatter. Currently always 332 * returns "en". 333 * 334 * @see http://www.php.net/manual/en/intldateformatter.getlocale.php 335 */ 336 public function getLocale($type = Locale::ACTUAL_LOCALE) 337 { 338 return 'en'; 339 } 340 341 /** 342 * Returns the formatter's pattern. 343 * 344 * @return string The pattern string used by the formatter 345 * 346 * @see http://www.php.net/manual/en/intldateformatter.getpattern.php 347 */ 348 public function getPattern() 349 { 350 return $this->pattern; 351 } 352 353 /** 354 * Returns the formatter's time type. 355 * 356 * @return string The time type used by the formatter 357 * 358 * @see http://www.php.net/manual/en/intldateformatter.gettimetype.php 359 */ 360 public function getTimeType() 361 { 362 return $this->timetype; 363 } 364 365 /** 366 * Returns the formatter's timezone identifier. 367 * 368 * @return string The timezone identifier used by the formatter 369 * 370 * @see http://www.php.net/manual/en/intldateformatter.gettimezoneid.php 371 */ 372 public function getTimeZoneId() 373 { 374 if (!$this->uninitializedTimeZoneId) { 375 return $this->timeZoneId; 376 } 377 378 // In PHP 5.5 default timezone depends on `date_default_timezone_get()` method 379 if (PHP_VERSION_ID >= 50500) { 380 return date_default_timezone_get(); 381 } 382 } 383 384 /** 385 * Not supported. Returns the formatter's timezone. 386 * 387 * @return mixed The timezone used by the formatter 388 * 389 * @see http://www.php.net/manual/en/intldateformatter.gettimezone.php 390 * 391 * @throws MethodNotImplementedException 392 */ 393 public function getTimeZone() 394 { 395 throw new MethodNotImplementedException(__METHOD__); 396 } 397 398 /** 399 * Returns whether the formatter is lenient. 400 * 401 * @return bool Currently always returns false. 402 * 403 * @see http://www.php.net/manual/en/intldateformatter.islenient.php 404 * 405 * @throws MethodNotImplementedException 406 */ 407 public function isLenient() 408 { 409 return false; 410 } 411 412 /** 413 * Not supported. Parse string to a field-based time value. 414 * 415 * @param string $value String to convert to a time value 416 * @param int $position Position at which to start the parsing in $value (zero-based). 417 * If no error occurs before $value is consumed, $parse_pos will 418 * contain -1 otherwise it will contain the position at which parsing 419 * ended. If $parse_pos > strlen($value), the parse fails immediately. 420 * 421 * @return string Localtime compatible array of integers: contains 24 hour clock value in tm_hour field 422 * 423 * @see http://www.php.net/manual/en/intldateformatter.localtime.php 424 * 425 * @throws MethodNotImplementedException 426 */ 427 public function localtime($value, &$position = 0) 428 { 429 throw new MethodNotImplementedException(__METHOD__); 430 } 431 432 /** 433 * Parse string to a timestamp value. 434 * 435 * @param string $value String to convert to a time value 436 * @param int $position Not supported. Position at which to start the parsing in $value (zero-based). 437 * If no error occurs before $value is consumed, $parse_pos will 438 * contain -1 otherwise it will contain the position at which parsing 439 * ended. If $parse_pos > strlen($value), the parse fails immediately. 440 * 441 * @return string Parsed value as a timestamp 442 * 443 * @see http://www.php.net/manual/en/intldateformatter.parse.php 444 * 445 * @throws MethodArgumentNotImplementedException When $position different than null, behavior not implemented 446 */ 447 public function parse($value, &$position = null) 448 { 449 // We don't calculate the position when parsing the value 450 if (null !== $position) { 451 throw new MethodArgumentNotImplementedException(__METHOD__, 'position'); 452 } 453 454 $dateTime = $this->createDateTime(0); 455 $transformer = new FullTransformer($this->getPattern(), $this->getTimeZoneId()); 456 457 $timestamp = $transformer->parse($dateTime, $value); 458 459 // behave like the intl extension. FullTransformer::parse() set the proper error 460 $this->errorCode = IntlGlobals::getErrorCode(); 461 $this->errorMessage = IntlGlobals::getErrorMessage(); 462 463 return $timestamp; 464 } 465 466 /** 467 * Not supported. Set the formatter's calendar. 468 * 469 * @param string $calendar The calendar to use. Default is IntlDateFormatter::GREGORIAN. 470 * 471 * @return bool true on success or false on failure 472 * 473 * @see http://www.php.net/manual/en/intldateformatter.setcalendar.php 474 * 475 * @throws MethodNotImplementedException 476 */ 477 public function setCalendar($calendar) 478 { 479 throw new MethodNotImplementedException(__METHOD__); 480 } 481 482 /** 483 * Set the leniency of the parser. 484 * 485 * Define if the parser is strict or lenient in interpreting inputs that do not match the pattern 486 * exactly. Enabling lenient parsing allows the parser to accept otherwise flawed date or time 487 * patterns, parsing as much as possible to obtain a value. Extra space, unrecognized tokens, or 488 * invalid values ("February 30th") are not accepted. 489 * 490 * @param bool $lenient Sets whether the parser is lenient or not. Currently 491 * only false (strict) is supported. 492 * 493 * @return bool true on success or false on failure 494 * 495 * @see http://www.php.net/manual/en/intldateformatter.setlenient.php 496 * 497 * @throws MethodArgumentValueNotImplementedException When $lenient is true 498 */ 499 public function setLenient($lenient) 500 { 501 if ($lenient) { 502 throw new MethodArgumentValueNotImplementedException(__METHOD__, 'lenient', $lenient, 'Only the strict parser is supported'); 503 } 504 505 return true; 506 } 507 508 /** 509 * Set the formatter's pattern. 510 * 511 * @param string $pattern A pattern string in conformance with the ICU IntlDateFormatter documentation 512 * 513 * @return bool true on success or false on failure 514 * 515 * @see http://www.php.net/manual/en/intldateformatter.setpattern.php 516 * @see http://userguide.icu-project.org/formatparse/datetime 517 */ 518 public function setPattern($pattern) 519 { 520 if (null === $pattern) { 521 $pattern = $this->getDefaultPattern(); 522 } 523 524 $this->pattern = $pattern; 525 526 return true; 527 } 528 529 /** 530 * Set the formatter's timezone identifier. 531 * 532 * @param string $timeZoneId The time zone ID string of the time zone to use. 533 * If NULL or the empty string, the default time zone for the 534 * runtime is used. 535 * 536 * @return bool true on success or false on failure 537 * 538 * @see http://www.php.net/manual/en/intldateformatter.settimezoneid.php 539 */ 540 public function setTimeZoneId($timeZoneId) 541 { 542 if (null === $timeZoneId) { 543 // In PHP 5.5 if $timeZoneId is null it fallbacks to `date_default_timezone_get()` method 544 if (PHP_VERSION_ID >= 50500) { 545 $timeZoneId = date_default_timezone_get(); 546 } else { 547 // TODO: changes were made to ext/intl in PHP 5.4.4 release that need to be investigated since it will 548 // use ini's date.timezone when the time zone is not provided. As a not well tested workaround, uses UTC. 549 // See the first two items of the commit message for more information: 550 // https://github.com/php/php-src/commit/eb346ef0f419b90739aadfb6cc7b7436c5b521d9 551 $timeZoneId = getenv('TZ') ?: 'UTC'; 552 } 553 554 $this->uninitializedTimeZoneId = true; 555 } 556 557 // Backup original passed time zone 558 $timeZone = $timeZoneId; 559 560 // Get an Etc/GMT time zone that is accepted for \DateTimeZone 561 if ('GMT' !== $timeZoneId && 0 === strpos($timeZoneId, 'GMT')) { 562 try { 563 $timeZoneId = DateFormat\TimeZoneTransformer::getEtcTimeZoneId($timeZoneId); 564 } catch (\InvalidArgumentException $e) { 565 // Does nothing, will fallback to UTC 566 } 567 } 568 569 try { 570 $this->dateTimeZone = new \DateTimeZone($timeZoneId); 571 } catch (\Exception $e) { 572 $this->dateTimeZone = new \DateTimeZone('UTC'); 573 } 574 575 $this->timeZoneId = $timeZone; 576 577 return true; 578 } 579 580 /** 581 * This method was added in PHP 5.5 as replacement for `setTimeZoneId()`. 582 * 583 * @param mixed $timeZone 584 * 585 * @return bool true on success or false on failure 586 * 587 * @see http://www.php.net/manual/en/intldateformatter.settimezone.php 588 */ 589 public function setTimeZone($timeZone) 590 { 591 return $this->setTimeZoneId($timeZone); 592 } 593 594 /** 595 * Create and returns a DateTime object with the specified timestamp and with the 596 * current time zone. 597 * 598 * @param int $timestamp 599 * 600 * @return \DateTime 601 */ 602 protected function createDateTime($timestamp) 603 { 604 $dateTime = new \DateTime(); 605 $dateTime->setTimestamp($timestamp); 606 $dateTime->setTimezone($this->dateTimeZone); 607 608 return $dateTime; 609 } 610 611 /** 612 * Returns a pattern string based in the datetype and timetype values. 613 * 614 * @return string 615 */ 616 protected function getDefaultPattern() 617 { 618 $patternParts = array(); 619 if (self::NONE !== $this->datetype) { 620 $patternParts[] = $this->defaultDateFormats[$this->datetype]; 621 } 622 if (self::NONE !== $this->timetype) { 623 $patternParts[] = $this->defaultTimeFormats[$this->timetype]; 624 } 625 $pattern = implode(' ', $patternParts); 626 627 return $pattern; 628 } 629} 630