From 7e1c6c4871936935303bc63d9513e469388bddfe Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Wed, 17 Feb 2016 22:09:28 +0100 Subject: [PATCH] [Yaml] support to parse and dump DateTime objects --- src/Symfony/Component/Yaml/CHANGELOG.md | 8 +++ src/Symfony/Component/Yaml/Inline.php | 44 +++++++++------ .../Component/Yaml/Tests/InlineTest.php | 53 ++++++++++++++++++- src/Symfony/Component/Yaml/Yaml.php | 1 + 4 files changed, 88 insertions(+), 18 deletions(-) diff --git a/src/Symfony/Component/Yaml/CHANGELOG.md b/src/Symfony/Component/Yaml/CHANGELOG.md index 23b9f6f6b9..63d68b0d5e 100644 --- a/src/Symfony/Component/Yaml/CHANGELOG.md +++ b/src/Symfony/Component/Yaml/CHANGELOG.md @@ -4,6 +4,14 @@ CHANGELOG 3.1.0 ----- + * Added support for parsing timestamps as `\DateTime` objects: + + ```php + Yaml::parse('2001-12-15 21:59:43.10 -5', Yaml::PARSE_DATETIME); + ``` + + * `\DateTime` and `\DateTimeImmutable` objects are dumped as YAML timestamps. + * Deprecated usage of `%` at the beginning of an unquoted string. * Added support for customizing the YAML parser behavior through an optional bit field: diff --git a/src/Symfony/Component/Yaml/Inline.php b/src/Symfony/Component/Yaml/Inline.php index 4907e695c1..b11e03147c 100644 --- a/src/Symfony/Component/Yaml/Inline.php +++ b/src/Symfony/Component/Yaml/Inline.php @@ -92,15 +92,15 @@ class Inline $i = 0; switch ($value[0]) { case '[': - $result = self::parseSequence($value, $i, $references); + $result = self::parseSequence($value, $flags, $i, $references); ++$i; break; case '{': - $result = self::parseMapping($value, $i, $references); + $result = self::parseMapping($value, $flags, $i, $references); ++$i; break; default: - $result = self::parseScalar($value, null, array('"', "'"), $i, true, $references); + $result = self::parseScalar($value, $flags, null, array('"', "'"), $i, true, $references); } // some comments are allowed at the end @@ -152,6 +152,8 @@ class Inline } return 'null'; + case $value instanceof \DateTimeInterface: + return $value->format('c'); case is_object($value): if (Yaml::DUMP_OBJECT & $flags) { return '!php/object:'.serialize($value); @@ -243,6 +245,7 @@ class Inline * Parses a scalar to a YAML string. * * @param string $scalar + * @param int $flags * @param string $delimiters * @param array $stringDelimiters * @param int &$i @@ -255,7 +258,7 @@ class Inline * * @internal */ - public static function parseScalar($scalar, $delimiters = null, $stringDelimiters = array('"', "'"), &$i = 0, $evaluate = true, $references = array()) + public static function parseScalar($scalar, $flags = 0, $delimiters = null, $stringDelimiters = array('"', "'"), &$i = 0, $evaluate = true, $references = array()) { if (in_array($scalar[$i], $stringDelimiters)) { // quoted scalar @@ -294,7 +297,7 @@ class Inline } if ($evaluate) { - $output = self::evaluateScalar($output, $references); + $output = self::evaluateScalar($output, $flags, $references); } } @@ -335,6 +338,7 @@ class Inline * Parses a sequence to a YAML string. * * @param string $sequence + * @param int $flags * @param int &$i * @param array $references * @@ -342,7 +346,7 @@ class Inline * * @throws ParseException When malformed inline YAML string is parsed */ - private static function parseSequence($sequence, &$i = 0, $references = array()) + private static function parseSequence($sequence, $flags, &$i = 0, $references = array()) { $output = array(); $len = strlen($sequence); @@ -353,11 +357,11 @@ class Inline switch ($sequence[$i]) { case '[': // nested sequence - $output[] = self::parseSequence($sequence, $i, $references); + $output[] = self::parseSequence($sequence, $flags, $i, $references); break; case '{': // nested mapping - $output[] = self::parseMapping($sequence, $i, $references); + $output[] = self::parseMapping($sequence, $flags, $i, $references); break; case ']': return $output; @@ -366,14 +370,14 @@ class Inline break; default: $isQuoted = in_array($sequence[$i], array('"', "'")); - $value = self::parseScalar($sequence, array(',', ']'), array('"', "'"), $i, true, $references); + $value = self::parseScalar($sequence, $flags, array(',', ']'), array('"', "'"), $i, true, $references); // the value can be an array if a reference has been resolved to an array var if (!is_array($value) && !$isQuoted && false !== strpos($value, ': ')) { // embedded mapping? try { $pos = 0; - $value = self::parseMapping('{'.$value.'}', $pos, $references); + $value = self::parseMapping('{'.$value.'}', $flags, $pos, $references); } catch (\InvalidArgumentException $e) { // no, it's not } @@ -394,6 +398,7 @@ class Inline * Parses a mapping to a YAML string. * * @param string $mapping + * @param int $flags * @param int &$i * @param array $references * @@ -401,7 +406,7 @@ class Inline * * @throws ParseException When malformed inline YAML string is parsed */ - private static function parseMapping($mapping, &$i = 0, $references = array()) + private static function parseMapping($mapping, $flags, &$i = 0, $references = array()) { $output = array(); $len = strlen($mapping); @@ -423,7 +428,7 @@ class Inline } // key - $key = self::parseScalar($mapping, array(':', ' '), array('"', "'"), $i, false); + $key = self::parseScalar($mapping, $flags, array(':', ' '), array('"', "'"), $i, false); // value $done = false; @@ -432,7 +437,7 @@ class Inline switch ($mapping[$i]) { case '[': // nested sequence - $value = self::parseSequence($mapping, $i, $references); + $value = self::parseSequence($mapping, $flags, $i, $references); // Spec: Keys MUST be unique; first one wins. // Parser cannot abort this mapping earlier, since lines // are processed sequentially. @@ -443,7 +448,7 @@ class Inline break; case '{': // nested mapping - $value = self::parseMapping($mapping, $i, $references); + $value = self::parseMapping($mapping, $flags, $i, $references); // Spec: Keys MUST be unique; first one wins. // Parser cannot abort this mapping earlier, since lines // are processed sequentially. @@ -456,7 +461,7 @@ class Inline case ' ': break; default: - $value = self::parseScalar($mapping, array(',', '}'), array('"', "'"), $i, true, $references); + $value = self::parseScalar($mapping, $flags, array(',', '}'), array('"', "'"), $i, true, $references); // Spec: Keys MUST be unique; first one wins. // Parser cannot abort this mapping earlier, since lines // are processed sequentially. @@ -482,13 +487,14 @@ class Inline * Evaluates scalars and replaces magic values. * * @param string $scalar + * @param int $flags * @param array $references * * @return string A YAML string * * @throws ParseException when object parsing support was disabled and the parser detected a PHP object or when a reference could not be resolved */ - private static function evaluateScalar($scalar, $references = array()) + private static function evaluateScalar($scalar, $flags, $references = array()) { $scalar = trim($scalar); $scalarLower = strtolower($scalar); @@ -527,7 +533,7 @@ class Inline case 0 === strpos($scalar, '!str'): return (string) substr($scalar, 5); case 0 === strpos($scalar, '! '): - return (int) self::parseScalar(substr($scalar, 2)); + return (int) self::parseScalar(substr($scalar, 2), $flags); case 0 === strpos($scalar, '!php/object:'): if (self::$objectSupport) { return unserialize(substr($scalar, 12)); @@ -573,6 +579,10 @@ class Inline case preg_match('/^(-|\+)?[0-9,]+(\.[0-9]+)?$/', $scalar): return (float) str_replace(',', '', $scalar); case preg_match(self::getTimestampRegex(), $scalar): + if (Yaml::PARSE_DATETIME & $flags) { + return new \DateTime($scalar,new \DateTimeZone('UTC')); + } + $timeZone = date_default_timezone_get(); date_default_timezone_set('UTC'); $time = strtotime($scalar); diff --git a/src/Symfony/Component/Yaml/Tests/InlineTest.php b/src/Symfony/Component/Yaml/Tests/InlineTest.php index c23c78bd99..c6ac51c09c 100644 --- a/src/Symfony/Component/Yaml/Tests/InlineTest.php +++ b/src/Symfony/Component/Yaml/Tests/InlineTest.php @@ -373,7 +373,7 @@ class InlineTest extends \PHPUnit_Framework_TestCase array("'#cfcfcf'", '#cfcfcf'), array('::form_base.html.twig', '::form_base.html.twig'), - array('2007-10-30', mktime(0, 0, 0, 10, 30, 2007)), + array('2007-10-30', gmmktime(0, 0, 0, 10, 30, 2007)), array('2007-10-30T02:59:43Z', gmmktime(2, 59, 43, 10, 30, 2007)), array('2007-10-30 02:59:43 Z', gmmktime(2, 59, 43, 10, 30, 2007)), array('1960-10-30 02:59:43 Z', gmmktime(2, 59, 43, 10, 30, 1960)), @@ -481,4 +481,55 @@ class InlineTest extends \PHPUnit_Framework_TestCase array('[foo, \'@foo.baz\', { \'%foo%\': \'foo is %foo%\', bar: \'%foo%\' }, true, \'@service_container\']', array('foo', '@foo.baz', array('%foo%' => 'foo is %foo%', 'bar' => '%foo%'), true, '@service_container')), ); } + + /** + * @dataProvider getTimestampTests + */ + public function testParseTimestampAsUnixTimestampByDefault($yaml, $year, $month, $day, $hour, $minute, $second) + { + $this->assertSame(gmmktime($hour, $minute, $second, $month, $day, $year), Inline::parse($yaml)); + } + + /** + * @dataProvider getTimestampTests + */ + public function testParseTimestampAsDateTimeObject($yaml, $year, $month, $day, $hour, $minute, $second) + { + $expected = new \DateTime('now', new \DateTimeZone('UTC')); + $expected->setDate($year, $month, $day); + $expected->setTime($hour, $minute, $second); + + $this->assertEquals($expected, Inline::parse($yaml, Yaml::PARSE_DATETIME)); + } + + public function getTimestampTests() + { + return array( + 'canonical' => array('2001-12-15T02:59:43.1Z', 2001, 12, 15, 2, 59, 43), + 'ISO-8601' => array('2001-12-15t21:59:43.10-05:00', 2001, 12, 16, 2, 59, 43), + 'spaced' => array('2001-12-15 21:59:43.10 -5', 2001, 12, 16, 2, 59, 43), + 'date' => array('2001-12-15', 2001, 12, 15, 0, 0, 0), + ); + } + + /** + * @dataProvider getDateTimeDumpTests + */ + public function testDumpDateTime($dateTime, $expected) + { + $this->assertSame($expected, Inline::dump($dateTime)); + } + + public function getDateTimeDumpTests() + { + $tests = array(); + + $dateTime = new \DateTime('2001-12-15 21:59:43', new \DateTimeZone('UTC')); + $tests['date-time-utc'] = array($dateTime, '2001-12-15T21:59:43+00:00'); + + $dateTime = new \DateTimeImmutable('2001-07-15 21:59:43', new \DateTimeZone('Europe/Berlin')); + $tests['immutable-date-time-europe-berlin'] = array($dateTime, '2001-07-15T21:59:43+02:00'); + + return $tests; + } } diff --git a/src/Symfony/Component/Yaml/Yaml.php b/src/Symfony/Component/Yaml/Yaml.php index 1fc6e09a96..9c321a8132 100644 --- a/src/Symfony/Component/Yaml/Yaml.php +++ b/src/Symfony/Component/Yaml/Yaml.php @@ -25,6 +25,7 @@ class Yaml const PARSE_OBJECT = 4; const PARSE_OBJECT_FOR_MAP = 8; const DUMP_EXCEPTION_ON_INVALID_TYPE = 16; + const PARSE_DATETIME = 32; /** * Parses YAML into a PHP value.