Skip to content

[Yaml] Support parsing YAML timestamps as DateTime #14420

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 12 commits into from
10 changes: 10 additions & 0 deletions UPGRADE-2.8.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
UPGRADE FROM 2.7 to 2.8
=======================

Yaml
-----

* The ability to pass $timestampAsDateTime = false to the Yaml::parse method is
deprecated since version 2.8. The argument will be removed in 3.0. Pass true instead.
* The ability to pass $dateTimeSupport = false to the Yaml::dump method is deprecated
since version 2.8. The argument will be removed in 3.0. Pass true instead.
9 changes: 9 additions & 0 deletions src/Symfony/Component/Yaml/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,12 @@ CHANGELOG

* Yaml::parse() does not evaluate loaded files as PHP files by default
anymore (call Yaml::enablePhpParsing() to get back the old behavior)

2.8
---

* Added a $timestampAsDateTime argument to the Yaml::parse() and Yaml::dump() methods.
* The ability to pass $timestampAsDateTime = false to the Yaml::parse method is
deprecated since version 2.8. The argument will be removed in 3.0. Pass true instead.
* The ability to pass $timestampAsDateTime = false to the Yaml::dump method is deprecated
since version 2.8. The argument will be removed in 3.0. Pass true instead.
9 changes: 5 additions & 4 deletions src/Symfony/Component/Yaml/Dumper.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,17 @@ public function setIndentation($num)
* @param int $indent The level of indentation (used internally)
* @param bool $exceptionOnInvalidType true if an exception must be thrown on invalid types (a PHP resource or object), false otherwise
* @param bool $objectSupport true if object support is enabled, false otherwise
* @param bool $timestampAsDateTime true if DateTime objects must be dumped as YAML timestamps, false if DateTime objects are not supported
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we really need such parameter ? I suggest dumping DateTime objects as Yaml timestamps all the time (instead of rejecting them as done currently)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently DateTime are handled as other objects.

I think we need this parameter to keep consistency with parsing.
If $timestampAsDateTime, timestamps are parsed as DateTime and DateTime dumped as timestamps,
else timestamps are parsed as integer and DateTime dumped as datetime objects (if $objectSupport is enabled), so reparsing as DateTime.

*
* @return string The YAML representation of the PHP value
*/
public function dump($input, $inline = 0, $indent = 0, $exceptionOnInvalidType = false, $objectSupport = false)
public function dump($input, $inline = 0, $indent = 0, $exceptionOnInvalidType = false, $objectSupport = false, $timestampAsDateTime = false)
{
$output = '';
$prefix = $indent ? str_repeat(' ', $indent) : '';

if ($inline <= 0 || !is_array($input) || empty($input)) {
$output .= $prefix.Inline::dump($input, $exceptionOnInvalidType, $objectSupport);
$output .= $prefix.Inline::dump($input, $exceptionOnInvalidType, $objectSupport, $timestampAsDateTime);
} else {
$isAHash = array_keys($input) !== range(0, count($input) - 1);

Expand All @@ -61,9 +62,9 @@ public function dump($input, $inline = 0, $indent = 0, $exceptionOnInvalidType =

$output .= sprintf('%s%s%s%s',
$prefix,
$isAHash ? Inline::dump($key, $exceptionOnInvalidType, $objectSupport).':' : '-',
$isAHash ? Inline::dump($key, $exceptionOnInvalidType, $objectSupport, $timestampAsDateTime).':' : '-',
$willBeInlined ? ' ' : "\n",
$this->dump($value, $inline - 1, $willBeInlined ? 0 : $indent + $this->indentation, $exceptionOnInvalidType, $objectSupport)
$this->dump($value, $inline - 1, $willBeInlined ? 0 : $indent + $this->indentation, $exceptionOnInvalidType, $objectSupport, $timestampAsDateTime)
).($willBeInlined ? "\n" : '');
}
}
Expand Down
26 changes: 24 additions & 2 deletions src/Symfony/Component/Yaml/Inline.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class Inline
private static $exceptionOnInvalidType = false;
private static $objectSupport = false;
private static $objectForMap = false;
private static $timestampAsDateTime = false;

/**
* Converts a YAML string to a PHP array.
Expand All @@ -35,16 +36,18 @@ class Inline
* @param bool $objectSupport true if object support is enabled, false otherwise
* @param bool $objectForMap true if maps should return a stdClass instead of array()
* @param array $references Mapping of variable names to values
* @param bool $timestampAsDateTime true if timestamps must be parsed as DateTime objects rather than Unix timestamps (integers)
*
* @return array A PHP array representing the YAML string
*
* @throws ParseException
*/
public static function parse($value, $exceptionOnInvalidType = false, $objectSupport = false, $objectForMap = false, $references = array())
public static function parse($value, $exceptionOnInvalidType = false, $objectSupport = false, $objectForMap = false, $references = array(), $timestampAsDateTime = false)
{
self::$exceptionOnInvalidType = $exceptionOnInvalidType;
self::$objectSupport = $objectSupport;
self::$objectForMap = $objectForMap;
self::$timestampAsDateTime = $timestampAsDateTime;

$value = trim($value);

Expand Down Expand Up @@ -89,13 +92,16 @@ public static function parse($value, $exceptionOnInvalidType = false, $objectSup
* @param mixed $value The PHP variable to convert
* @param bool $exceptionOnInvalidType true if an exception must be thrown on invalid types (a PHP resource or object), false otherwise
* @param bool $objectSupport true if object support is enabled, false otherwise
* @param bool $timestampAsDateTime true if DateTime objects must be dumped as YAML timestamps, false if DateTime objects are not supported
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you don't need this $timestampAsDateTime argument when dumping, given that you don't respect it anyway

*
* @return string The YAML string representing the PHP array
*
* @throws DumpException When trying to dump PHP resource
*/
public static function dump($value, $exceptionOnInvalidType = false, $objectSupport = false)
public static function dump($value, $exceptionOnInvalidType = false, $objectSupport = false, $timestampAsDateTime = false)
{
self::$timestampAsDateTime = $timestampAsDateTime;

switch (true) {
case is_resource($value):
if ($exceptionOnInvalidType) {
Expand All @@ -104,6 +110,18 @@ public static function dump($value, $exceptionOnInvalidType = false, $objectSupp

return 'null';
case is_object($value):
if (self::$timestampAsDateTime && ($value instanceof \DateTime || $value instanceof \DateTimeImmutable)) {
if ($value->getTimezone()->getName() === date_default_timezone_get()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about this. The code parsing the date might have a different default timezone. It is better to always include all info IMO

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, if the TimeZone has been intentionally defined as for exemple +1 (in the YAML file parsed or in the PHP datas), it dumps it (because the server server timestamp is for example Europe/Paris and not +1).
If it was not defined manually, it has the default TimeZone and so the Yaml component doesn't dump the timestamp (letting it with default value in some ways).
I think personally it's the best behaviour.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if it was explicitly defined as UTC and the server default timezone is also UTC ? You have no way to know whether the UTC timezone was explicitly chosen or no. But even worse, you have no idea how this will be parsed. So IMO, you should not drop information when dumping, assuming that the server parsing your YAML will have the same config

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok.

if ('000000' === $value->format('His')) {
return $value->format('Y-m-d');
}

return $value->format('Y-m-d H:i:s');
}

return $value->format(\DateTime::W3C);
}

if ($objectSupport) {
return '!!php/object:'.serialize($value);
}
Expand Down Expand Up @@ -502,6 +520,10 @@ private static function evaluateScalar($scalar, $references = array())
case preg_match('/^(-|\+)?[0-9,]+(\.[0-9]+)?$/', $scalar):
return (float) str_replace(',', '', $scalar);
case preg_match(self::getTimestampRegex(), $scalar):
if (self::$timestampAsDateTime) {
return new \DateTime($scalar);
}

return strtotime($scalar);
}
default:
Expand Down
28 changes: 15 additions & 13 deletions src/Symfony/Component/Yaml/Parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,13 @@ public function __construct($offset = 0)
* @param bool $exceptionOnInvalidType true if an exception must be thrown on invalid types (a PHP resource or object), false otherwise
* @param bool $objectSupport true if object support is enabled, false otherwise
* @param bool $objectForMap true if maps should return a stdClass instead of array()
* @param bool $timestampAsDateTime true if timestamps must be parsed as DateTime objects rather than Unix timestamps (integers)
*
* @return mixed A PHP value
*
* @throws ParseException If the YAML is not valid
*/
public function parse($value, $exceptionOnInvalidType = false, $objectSupport = false, $objectForMap = false)
public function parse($value, $exceptionOnInvalidType = false, $objectSupport = false, $objectForMap = false, $timestampAsDateTime = false)
{
if (!preg_match('//u', $value)) {
throw new ParseException('The YAML value does not appear to be valid UTF-8.');
Expand Down Expand Up @@ -95,7 +96,7 @@ public function parse($value, $exceptionOnInvalidType = false, $objectSupport =
$c = $this->getRealCurrentLineNb() + 1;
$parser = new self($c);
$parser->refs = &$this->refs;
$data[] = $parser->parse($this->getNextEmbedBlock(null, true), $exceptionOnInvalidType, $objectSupport, $objectForMap);
$data[] = $parser->parse($this->getNextEmbedBlock(null, true), $exceptionOnInvalidType, $objectSupport, $objectForMap, $timestampAsDateTime);
} else {
if (isset($values['leadspaces'])
&& preg_match('#^(?P<key>'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\{\[].*?) *\:(\s+(?P<value>.+?))?\s*$#u', $values['value'], $matches)
Expand All @@ -110,9 +111,9 @@ public function parse($value, $exceptionOnInvalidType = false, $objectSupport =
$block .= "\n".$this->getNextEmbedBlock($this->getCurrentLineIndentation() + strlen($values['leadspaces']) + 1);
}

$data[] = $parser->parse($block, $exceptionOnInvalidType, $objectSupport, $objectForMap);
$data[] = $parser->parse($block, $exceptionOnInvalidType, $objectSupport, $objectForMap, $timestampAsDateTime);
} else {
$data[] = $this->parseValue($values['value'], $exceptionOnInvalidType, $objectSupport, $objectForMap);
$data[] = $this->parseValue($values['value'], $exceptionOnInvalidType, $objectSupport, $objectForMap, $timestampAsDateTime);
}
}
} elseif (preg_match('#^(?P<key>'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\[\{].*?) *\:(\s+(?P<value>.+?))?\s*$#u', $this->currentLine, $values) && (false === strpos($values['key'], ' #') || in_array($values['key'][0], array('"', "'")))) {
Expand All @@ -122,7 +123,7 @@ public function parse($value, $exceptionOnInvalidType = false, $objectSupport =
$context = 'mapping';

// force correct settings
Inline::parse(null, $exceptionOnInvalidType, $objectSupport, $objectForMap, $this->refs);
Inline::parse(null, $exceptionOnInvalidType, $objectSupport, $objectForMap, null, $timestampAsDateTime);
try {
$key = Inline::parseScalar($values['key']);
} catch (ParseException $e) {
Expand Down Expand Up @@ -161,7 +162,7 @@ public function parse($value, $exceptionOnInvalidType = false, $objectSupport =
$c = $this->getRealCurrentLineNb() + 1;
$parser = new self($c);
$parser->refs = &$this->refs;
$parsed = $parser->parse($value, $exceptionOnInvalidType, $objectSupport, $objectForMap);
$parsed = $parser->parse($value, $exceptionOnInvalidType, $objectSupport, $objectForMap, $timestampAsDateTime);

if (!is_array($parsed)) {
throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
Expand Down Expand Up @@ -212,15 +213,15 @@ public function parse($value, $exceptionOnInvalidType = false, $objectSupport =
$c = $this->getRealCurrentLineNb() + 1;
$parser = new self($c);
$parser->refs = &$this->refs;
$value = $parser->parse($this->getNextEmbedBlock(), $exceptionOnInvalidType, $objectSupport, $objectForMap);
$value = $parser->parse($this->getNextEmbedBlock(), $exceptionOnInvalidType, $objectSupport, $objectForMap, $timestampAsDateTime);
// Spec: Keys MUST be unique; first one wins.
// But overwriting is allowed when a merge node is used in current block.
if ($allowOverwrite || !isset($data[$key])) {
$data[$key] = $value;
}
}
} else {
$value = $this->parseValue($values['value'], $exceptionOnInvalidType, $objectSupport, $objectForMap);
$value = $this->parseValue($values['value'], $exceptionOnInvalidType, $objectSupport, $objectForMap, $timestampAsDateTime);
// Spec: Keys MUST be unique; first one wins.
// But overwriting is allowed when a merge node is used in current block.
if ($allowOverwrite || !isset($data[$key])) {
Expand All @@ -236,7 +237,7 @@ public function parse($value, $exceptionOnInvalidType = false, $objectSupport =
// 1-liner optionally followed by newline(s)
if ($this->lines[0] === trim($value)) {
try {
$value = Inline::parse($this->lines[0], $exceptionOnInvalidType, $objectSupport, $objectForMap, $this->refs);
$value = Inline::parse($this->lines[0], $exceptionOnInvalidType, $objectSupport, $objectForMap, $this->refs, $timestampAsDateTime);
} catch (ParseException $e) {
$e->setParsedLine($this->getRealCurrentLineNb() + 1);
$e->setSnippet($this->currentLine);
Expand Down Expand Up @@ -433,15 +434,16 @@ private function moveToPreviousLine()
* Parses a YAML value.
*
* @param string $value A YAML value
* @param bool $exceptionOnInvalidType True if an exception must be thrown on invalid types false otherwise
* @param bool $objectSupport True if object support is enabled, false otherwise
* @param bool $exceptionOnInvalidType true if an exception must be thrown on invalid types false otherwise
* @param bool $objectSupport true if object support is enabled, false otherwise
* @param bool $objectForMap true if maps should return a stdClass instead of array()
* @param bool $timestampAsDateTime true if timestamps must be parsed as DateTime objects rather than Unix timestamps (integers)
*
* @return mixed A PHP value
*
* @throws ParseException When reference does not exist
*/
private function parseValue($value, $exceptionOnInvalidType, $objectSupport, $objectForMap)
private function parseValue($value, $exceptionOnInvalidType, $objectSupport, $objectForMap, $timestampAsDateTime)
{
if (0 === strpos($value, '*')) {
if (false !== $pos = strpos($value, '#')) {
Expand All @@ -464,7 +466,7 @@ private function parseValue($value, $exceptionOnInvalidType, $objectSupport, $ob
}

try {
return Inline::parse($value, $exceptionOnInvalidType, $objectSupport, $objectForMap, $this->refs);
return Inline::parse($value, $exceptionOnInvalidType, $objectSupport, $objectForMap, $this->refs, $timestampAsDateTime);
} catch (ParseException $e) {
$e->setParsedLine($this->getRealCurrentLineNb() + 1);
$e->setSnippet($this->currentLine);
Expand Down
41 changes: 40 additions & 1 deletion src/Symfony/Component/Yaml/Tests/InlineTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,16 @@ public function testParseWithMapObjects($yaml, $value)
$this->assertSame(serialize($value), serialize($actual));
}

/**
* @dataProvider getTestsForParseWithTimestampAsDateTime
*/
public function testParseTimestampAsDateTime($yamlTimestamp)
{
$expected = serialize(new \DateTime($yamlTimestamp));
$actual = serialize(Inline::parse($yamlTimestamp, false, false, false, null, true));
$this->assertSame($expected, $actual);
}

/**
* @dataProvider getTestsForDump
*/
Expand All @@ -43,6 +53,14 @@ public function testDump($yaml, $value)
$this->assertSame($value, Inline::parse(Inline::dump($value)), 'check consistency');
}

/**
* @dataProvider getTestsForDumpWithDateTimeSupport
*/
public function testDumpWithDateTimeSupport($yaml, $value)
{
$this->assertSame($yaml, Inline::dump($value, false, false, true));
}

public function testDumpNumericValueWithLocale()
{
$locale = setlocale(LC_NUMERIC, 0);
Expand Down Expand Up @@ -290,7 +308,7 @@ public function getTestsForParseWithMapObjects()
array('{foo: \'bar\', bar: \'foo: bar\'}', (object) array('foo' => 'bar', 'bar' => 'foo: bar')),
array('{\'foo\': \'bar\', "bar": \'foo: bar\'}', (object) array('foo' => 'bar', 'bar' => 'foo: bar')),
array('{\'foo\'\'\': \'bar\', "bar\"": \'foo: bar\'}', (object) array('foo\'' => 'bar', "bar\"" => 'foo: bar')),
array('{\'foo: \': \'bar\', "bar: ": \'foo: bar\'}', (object) array('foo: ' => 'bar', "bar: " => 'foo: bar')),
array('{\'foo: \': \'bar\', "bar: ": \'foo: bar\'}', (object) array('foo: ' => 'bar', 'bar: ' => 'foo: bar')),

// nested sequences and mappings
array('[foo, [bar, foo]]', array('foo', array('bar', 'foo'))),
Expand Down Expand Up @@ -323,6 +341,17 @@ public function getTestsForParseWithMapObjects()
);
}

public function getTestsForParseWithTimestampAsDateTime()
{
return array(
array('2007-10-30'),
array('2007-10-30T02:59:43Z'),
array('2007-10-30 02:59:43 Z'),
array('1960-10-30 02:59:43 Z'),
array('1730-10-30T02:59:43Z'),
);
}

public function getTestsForDump()
{
return array(
Expand Down Expand Up @@ -377,4 +406,14 @@ public function getTestsForDump()
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')),
);
}

public function getTestsForDumpWithDateTimeSupport () {
return array(
array('2015-04-21T05:30:30-08:00', new \DateTime('2015-04-21 05:30:30 -8')),
array('2015-04-21T00:00:00-02:00', new \DateTime('2015-04-21 -2')),
array('2015-04-21T00:00:00+00:00', new \DateTime('2015-04-21 -0')),
array('2015-04-21 05:30:30', new \DateTime('2015-04-21 05:30:30')),
array('2015-04-21', new \DateTime('2015-04-21')),
);
}
}
Loading