From 3c5d9155d475aa2cfe8cf8278e128b2e7b8262af Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Mon, 20 Feb 2017 07:08:19 +0100 Subject: [PATCH] deprecate implicit string casting of mapping keys --- UPGRADE-3.3.md | 31 ++++ UPGRADE-4.0.md | 30 ++++ .../Loader/YamlFileLoader.php | 2 +- .../Routing/Loader/YamlFileLoader.php | 3 +- src/Symfony/Component/Routing/composer.json | 5 +- .../Mapping/Loader/YamlFileLoader.php | 3 +- .../Component/Serializer/composer.json | 4 +- .../Translation/Loader/YamlFileLoader.php | 3 +- .../Component/Translation/composer.json | 5 +- .../Mapping/Loader/YamlFileLoader.php | 3 +- src/Symfony/Component/Validator/composer.json | 5 +- src/Symfony/Component/Yaml/CHANGELOG.md | 31 ++++ src/Symfony/Component/Yaml/Inline.php | 8 + src/Symfony/Component/Yaml/Parser.php | 7 +- .../Component/Yaml/Tests/DumperTest.php | 2 +- .../Fixtures/YtsSpecificationExamples.yml | 27 ---- .../Yaml/Tests/Fixtures/YtsTypeTransfers.yml | 14 -- .../Tests/Fixtures/booleanMappingKeys.yml | 11 ++ .../Fixtures/legacyBooleanMappingKeys.yml | 23 +++ .../Tests/Fixtures/legacyNonStringKeys.yml | 2 + .../Tests/Fixtures/legacyNullMappingKey.yml | 9 ++ .../Yaml/Tests/Fixtures/nonStringKeys.yml | 3 + .../Yaml/Tests/Fixtures/nullMappingKey.yml | 9 ++ .../Tests/Fixtures/numericMappingKeys.yml | 23 +++ .../Component/Yaml/Tests/Fixtures/sfTests.yml | 9 -- .../Component/Yaml/Tests/InlineTest.php | 56 +++++-- .../Component/Yaml/Tests/ParserTest.php | 147 ++++++++++++++---- src/Symfony/Component/Yaml/Yaml.php | 1 + 28 files changed, 367 insertions(+), 109 deletions(-) create mode 100644 src/Symfony/Component/Yaml/Tests/Fixtures/booleanMappingKeys.yml create mode 100644 src/Symfony/Component/Yaml/Tests/Fixtures/legacyBooleanMappingKeys.yml create mode 100644 src/Symfony/Component/Yaml/Tests/Fixtures/legacyNonStringKeys.yml create mode 100644 src/Symfony/Component/Yaml/Tests/Fixtures/legacyNullMappingKey.yml create mode 100644 src/Symfony/Component/Yaml/Tests/Fixtures/nonStringKeys.yml create mode 100644 src/Symfony/Component/Yaml/Tests/Fixtures/nullMappingKey.yml create mode 100644 src/Symfony/Component/Yaml/Tests/Fixtures/numericMappingKeys.yml diff --git a/UPGRADE-3.3.md b/UPGRADE-3.3.md index 8fbfebc6bf94..608474e88130 100644 --- a/UPGRADE-3.3.md +++ b/UPGRADE-3.3.md @@ -246,6 +246,37 @@ Workflow Yaml ---- + * Deprecated support for implicitly parsing non-string mapping keys as strings. Mapping keys that are no strings will + lead to a `ParseException` in Symfony 4.0. Use the `PARSE_KEYS_AS_STRINGS` flag to opt-in for keys to be parsed as + strings. + + Before: + + ```php + $yaml = <<yamlParser->parse(file_get_contents($file), Yaml::PARSE_CONSTANT | Yaml::PARSE_CUSTOM_TAGS); + $configuration = $this->yamlParser->parse(file_get_contents($file), Yaml::PARSE_CONSTANT | Yaml::PARSE_CUSTOM_TAGS | Yaml::PARSE_KEYS_AS_STRINGS); } catch (ParseException $e) { throw new InvalidArgumentException(sprintf('The file "%s" does not contain valid YAML.', $file), 0, $e); } diff --git a/src/Symfony/Component/Routing/Loader/YamlFileLoader.php b/src/Symfony/Component/Routing/Loader/YamlFileLoader.php index 31314011b95b..7ae5e84d109a 100644 --- a/src/Symfony/Component/Routing/Loader/YamlFileLoader.php +++ b/src/Symfony/Component/Routing/Loader/YamlFileLoader.php @@ -17,6 +17,7 @@ use Symfony\Component\Yaml\Exception\ParseException; use Symfony\Component\Yaml\Parser as YamlParser; use Symfony\Component\Config\Loader\FileLoader; +use Symfony\Component\Yaml\Yaml; /** * YamlFileLoader loads Yaml routing files. @@ -58,7 +59,7 @@ public function load($file, $type = null) } try { - $parsedConfig = $this->yamlParser->parse(file_get_contents($path)); + $parsedConfig = $this->yamlParser->parse(file_get_contents($path), Yaml::PARSE_KEYS_AS_STRINGS); } catch (ParseException $e) { throw new \InvalidArgumentException(sprintf('The file "%s" does not contain valid YAML.', $path), 0, $e); } diff --git a/src/Symfony/Component/Routing/composer.json b/src/Symfony/Component/Routing/composer.json index 11418c7164d8..a2a625cc5dd3 100644 --- a/src/Symfony/Component/Routing/composer.json +++ b/src/Symfony/Component/Routing/composer.json @@ -21,7 +21,7 @@ "require-dev": { "symfony/config": "~2.8|~3.0", "symfony/http-foundation": "~2.8|~3.0", - "symfony/yaml": "~2.8|~3.0", + "symfony/yaml": "~3.3", "symfony/expression-language": "~2.8|~3.0", "symfony/dependency-injection": "~2.8|~3.0", "doctrine/annotations": "~1.0", @@ -29,7 +29,8 @@ "psr/log": "~1.0" }, "conflict": { - "symfony/config": "<2.8" + "symfony/config": "<2.8", + "symfony/yaml": "<3.3" }, "suggest": { "symfony/http-foundation": "For using a Symfony Request object", diff --git a/src/Symfony/Component/Serializer/Mapping/Loader/YamlFileLoader.php b/src/Symfony/Component/Serializer/Mapping/Loader/YamlFileLoader.php index e3afa4763271..20a1d48aade6 100644 --- a/src/Symfony/Component/Serializer/Mapping/Loader/YamlFileLoader.php +++ b/src/Symfony/Component/Serializer/Mapping/Loader/YamlFileLoader.php @@ -15,6 +15,7 @@ use Symfony\Component\Serializer\Mapping\AttributeMetadata; use Symfony\Component\Serializer\Mapping\ClassMetadataInterface; use Symfony\Component\Yaml\Parser; +use Symfony\Component\Yaml\Yaml; /** * YAML File Loader. @@ -113,7 +114,7 @@ private function getClassesFromYaml() $this->yamlParser = new Parser(); } - $classes = $this->yamlParser->parse(file_get_contents($this->file)); + $classes = $this->yamlParser->parse(file_get_contents($this->file), Yaml::PARSE_KEYS_AS_STRINGS); if (empty($classes)) { return array(); diff --git a/src/Symfony/Component/Serializer/composer.json b/src/Symfony/Component/Serializer/composer.json index dc1017d5b495..37dd110bc3f2 100644 --- a/src/Symfony/Component/Serializer/composer.json +++ b/src/Symfony/Component/Serializer/composer.json @@ -19,7 +19,7 @@ "php": ">=5.5.9" }, "require-dev": { - "symfony/yaml": "~3.1", + "symfony/yaml": "~3.3", "symfony/config": "~2.8|~3.0", "symfony/property-access": "~2.8|~3.0", "symfony/http-foundation": "~2.8|~3.0", @@ -34,7 +34,7 @@ "symfony/dependency-injection": "<3.2", "symfony/property-access": ">=3.0,<3.0.4|>=2.8,<2.8.4", "symfony/property-info": "<3.1", - "symfony/yaml": "<3.1" + "symfony/yaml": "<3.3" }, "suggest": { "psr/cache-implementation": "For using the metadata cache.", diff --git a/src/Symfony/Component/Translation/Loader/YamlFileLoader.php b/src/Symfony/Component/Translation/Loader/YamlFileLoader.php index 41e390d0e967..5897767b6bf0 100644 --- a/src/Symfony/Component/Translation/Loader/YamlFileLoader.php +++ b/src/Symfony/Component/Translation/Loader/YamlFileLoader.php @@ -15,6 +15,7 @@ use Symfony\Component\Translation\Exception\LogicException; use Symfony\Component\Yaml\Parser as YamlParser; use Symfony\Component\Yaml\Exception\ParseException; +use Symfony\Component\Yaml\Yaml; /** * YamlFileLoader loads translations from Yaml files. @@ -39,7 +40,7 @@ protected function loadResource($resource) } try { - $messages = $this->yamlParser->parse(file_get_contents($resource)); + $messages = $this->yamlParser->parse(file_get_contents($resource), Yaml::PARSE_KEYS_AS_STRINGS); } catch (ParseException $e) { throw new InvalidResourceException(sprintf('Error parsing YAML, invalid file "%s"', $resource), 0, $e); } diff --git a/src/Symfony/Component/Translation/composer.json b/src/Symfony/Component/Translation/composer.json index da2089450306..e107e2538a15 100644 --- a/src/Symfony/Component/Translation/composer.json +++ b/src/Symfony/Component/Translation/composer.json @@ -22,11 +22,12 @@ "require-dev": { "symfony/config": "~2.8|~3.0", "symfony/intl": "^2.8.18|^3.2.5", - "symfony/yaml": "~2.8|~3.0", + "symfony/yaml": "~3.3", "psr/log": "~1.0" }, "conflict": { - "symfony/config": "<2.8" + "symfony/config": "<2.8", + "symfony/yaml": "<3.3" }, "suggest": { "symfony/config": "", diff --git a/src/Symfony/Component/Validator/Mapping/Loader/YamlFileLoader.php b/src/Symfony/Component/Validator/Mapping/Loader/YamlFileLoader.php index 1eabd1885fd1..f2e664750a48 100644 --- a/src/Symfony/Component/Validator/Mapping/Loader/YamlFileLoader.php +++ b/src/Symfony/Component/Validator/Mapping/Loader/YamlFileLoader.php @@ -14,6 +14,7 @@ use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Yaml\Exception\ParseException; use Symfony\Component\Yaml\Parser as YamlParser; +use Symfony\Component\Yaml\Yaml; /** * Loads validation metadata from a YAML file. @@ -115,7 +116,7 @@ protected function parseNodes(array $nodes) private function parseFile($path) { try { - $classes = $this->yamlParser->parse(file_get_contents($path)); + $classes = $this->yamlParser->parse(file_get_contents($path), Yaml::PARSE_KEYS_AS_STRINGS); } catch (ParseException $e) { throw new \InvalidArgumentException(sprintf('The file "%s" does not contain valid YAML.', $path), 0, $e); } diff --git a/src/Symfony/Component/Validator/composer.json b/src/Symfony/Component/Validator/composer.json index 939bd702cd01..41bc4d8ab1c1 100644 --- a/src/Symfony/Component/Validator/composer.json +++ b/src/Symfony/Component/Validator/composer.json @@ -23,7 +23,7 @@ "require-dev": { "symfony/http-foundation": "~2.8|~3.0", "symfony/intl": "^2.8.18|^3.2.5", - "symfony/yaml": "~2.8|~3.0", + "symfony/yaml": "~3.3", "symfony/config": "~2.8|~3.0", "symfony/expression-language": "~2.8|~3.0", "symfony/cache": "~3.1", @@ -32,7 +32,8 @@ "egulias/email-validator": "^1.2.8|~2.0" }, "conflict": { - "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0" + "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0", + "symfony/yaml": "<3.3" }, "suggest": { "psr/cache-implementation": "For using the metadata cache.", diff --git a/src/Symfony/Component/Yaml/CHANGELOG.md b/src/Symfony/Component/Yaml/CHANGELOG.md index 6951c943688f..fb62848470cf 100644 --- a/src/Symfony/Component/Yaml/CHANGELOG.md +++ b/src/Symfony/Component/Yaml/CHANGELOG.md @@ -4,6 +4,37 @@ CHANGELOG 3.3.0 ----- + * Deprecated support for implicitly parsing non-string mapping keys as strings. Mapping keys that are no strings will + lead to a `ParseException` in Symfony 4.0. Use the `PARSE_KEYS_AS_STRINGS` flag to opt-in for keys to be parsed as + strings. + + Before: + + ```php + $yaml = <<refs); try { Inline::$parsedLineNumber = $this->getRealCurrentLineNb(); - $key = Inline::parseScalar($values['key']); + $i = 0; + $key = Inline::parseScalar($values['key'], 0, null, $i, !(Yaml::PARSE_KEYS_AS_STRINGS & $flags)); } catch (ParseException $e) { $e->setParsedLine($this->getRealCurrentLineNb() + 1); $e->setSnippet($this->currentLine); @@ -188,6 +189,10 @@ public function parse($value, $flags = 0) throw $e; } + if (!(Yaml::PARSE_KEYS_AS_STRINGS & $flags) && !is_string($key)) { + @trigger_error('Implicit casting of incompatible mapping keys to strings is deprecated since version 3.3 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0. Pass the PARSE_KEYS_AS_STRING flag to explicitly enable the type casts.', E_USER_DEPRECATED); + } + // Convert float keys to strings, to avoid being converted to integers by PHP if (is_float($key)) { $key = (string) $key; diff --git a/src/Symfony/Component/Yaml/Tests/DumperTest.php b/src/Symfony/Component/Yaml/Tests/DumperTest.php index ac848a184024..87b5539e7380 100644 --- a/src/Symfony/Component/Yaml/Tests/DumperTest.php +++ b/src/Symfony/Component/Yaml/Tests/DumperTest.php @@ -125,7 +125,7 @@ public function testSpecifications() // TODO } else { eval('$expected = '.trim($test['php']).';'); - $this->assertSame($expected, $this->parser->parse($this->dumper->dump($expected, 10)), $test['test']); + $this->assertSame($expected, $this->parser->parse($this->dumper->dump($expected, 10), Yaml::PARSE_KEYS_AS_STRINGS), $test['test']); } } } diff --git a/src/Symfony/Component/Yaml/Tests/Fixtures/YtsSpecificationExamples.yml b/src/Symfony/Component/Yaml/Tests/Fixtures/YtsSpecificationExamples.yml index ac0c69da0af7..45e27b7b7410 100644 --- a/src/Symfony/Component/Yaml/Tests/Fixtures/YtsSpecificationExamples.yml +++ b/src/Symfony/Component/Yaml/Tests/Fixtures/YtsSpecificationExamples.yml @@ -556,21 +556,6 @@ php: | 'fixed' => 1230.15, ) --- -test: Miscellaneous -spec: 2.21 -yaml: | - null: ~ - true: true - false: false - string: '12345' -php: | - array( - '' => null, - 1 => true, - 0 => false, - 'string' => '12345' - ) ---- test: Timestamps todo: true spec: 2.22 @@ -1533,18 +1518,6 @@ ruby: | } --- -test: Boolean -yaml: | - false: used as key - logical: true - answer: false -php: | - array( - false => 'used as key', - 'logical' => true, - 'answer' => false - ) ---- test: Integer yaml: | canonical: 12345 diff --git a/src/Symfony/Component/Yaml/Tests/Fixtures/YtsTypeTransfers.yml b/src/Symfony/Component/Yaml/Tests/Fixtures/YtsTypeTransfers.yml index 8fefa494cfb2..5b9df73be157 100644 --- a/src/Symfony/Component/Yaml/Tests/Fixtures/YtsTypeTransfers.yml +++ b/src/Symfony/Component/Yaml/Tests/Fixtures/YtsTypeTransfers.yml @@ -210,20 +210,6 @@ php: | 'negative one-thousand' => -1000.0 ) --- -test: Integers as Map Keys -brief: > - An integer can be used a dictionary key. -yaml: | - 1: one - 2: two - 3: three -php: | - array( - 1 => 'one', - 2 => 'two', - 3 => 'three' - ) ---- test: Floats dump_skip: true brief: > diff --git a/src/Symfony/Component/Yaml/Tests/Fixtures/booleanMappingKeys.yml b/src/Symfony/Component/Yaml/Tests/Fixtures/booleanMappingKeys.yml new file mode 100644 index 000000000000..26799e8e7bff --- /dev/null +++ b/src/Symfony/Component/Yaml/Tests/Fixtures/booleanMappingKeys.yml @@ -0,0 +1,11 @@ +--- %YAML:1.0 +test: Miscellaneous +spec: 2.21 +yaml: | + true: true + false: false +php: | + array( + 'true' => true, + 'false' => false, + ) diff --git a/src/Symfony/Component/Yaml/Tests/Fixtures/legacyBooleanMappingKeys.yml b/src/Symfony/Component/Yaml/Tests/Fixtures/legacyBooleanMappingKeys.yml new file mode 100644 index 000000000000..5e8d091707d5 --- /dev/null +++ b/src/Symfony/Component/Yaml/Tests/Fixtures/legacyBooleanMappingKeys.yml @@ -0,0 +1,23 @@ +--- %YAML:1.0 +test: Miscellaneous +spec: 2.21 +yaml: | + true: true + false: false +php: | + array( + 1 => true, + 0 => false, + ) +--- +test: Boolean +yaml: | + false: used as key + logical: true + answer: false +php: | + array( + false => 'used as key', + 'logical' => true, + 'answer' => false + ) diff --git a/src/Symfony/Component/Yaml/Tests/Fixtures/legacyNonStringKeys.yml b/src/Symfony/Component/Yaml/Tests/Fixtures/legacyNonStringKeys.yml new file mode 100644 index 000000000000..4e28201856d2 --- /dev/null +++ b/src/Symfony/Component/Yaml/Tests/Fixtures/legacyNonStringKeys.yml @@ -0,0 +1,2 @@ +- legacyBooleanMappingKeys +- legacyNullMappingKey diff --git a/src/Symfony/Component/Yaml/Tests/Fixtures/legacyNullMappingKey.yml b/src/Symfony/Component/Yaml/Tests/Fixtures/legacyNullMappingKey.yml new file mode 100644 index 000000000000..551a6205e137 --- /dev/null +++ b/src/Symfony/Component/Yaml/Tests/Fixtures/legacyNullMappingKey.yml @@ -0,0 +1,9 @@ +--- %YAML:1.0 +test: Miscellaneous +spec: 2.21 +yaml: | + null: ~ +php: | + array( + '' => null, + ) diff --git a/src/Symfony/Component/Yaml/Tests/Fixtures/nonStringKeys.yml b/src/Symfony/Component/Yaml/Tests/Fixtures/nonStringKeys.yml new file mode 100644 index 000000000000..354b0791e894 --- /dev/null +++ b/src/Symfony/Component/Yaml/Tests/Fixtures/nonStringKeys.yml @@ -0,0 +1,3 @@ +- booleanMappingKeys +- numericMappingKeys +- nullMappingKey diff --git a/src/Symfony/Component/Yaml/Tests/Fixtures/nullMappingKey.yml b/src/Symfony/Component/Yaml/Tests/Fixtures/nullMappingKey.yml new file mode 100644 index 000000000000..7dcadc729968 --- /dev/null +++ b/src/Symfony/Component/Yaml/Tests/Fixtures/nullMappingKey.yml @@ -0,0 +1,9 @@ +--- %YAML:1.0 +test: Miscellaneous +spec: 2.21 +yaml: | + null: ~ +php: | + array( + 'null' => null, + ) diff --git a/src/Symfony/Component/Yaml/Tests/Fixtures/numericMappingKeys.yml b/src/Symfony/Component/Yaml/Tests/Fixtures/numericMappingKeys.yml new file mode 100644 index 000000000000..9cfb7713a5c2 --- /dev/null +++ b/src/Symfony/Component/Yaml/Tests/Fixtures/numericMappingKeys.yml @@ -0,0 +1,23 @@ +--- %YAML:1.0 +test: A sequence with an unordered array +brief: > + A sequence with an unordered array +yaml: | + 1: foo + 0: bar +php: | + array(1 => 'foo', 0 => 'bar') +--- +test: Integers as Map Keys +brief: > + An integer can be used as dictionary key. +yaml: | + 1: one + 2: two + 3: three +php: | + array( + 1 => 'one', + 2 => 'two', + 3 => 'three' + ) diff --git a/src/Symfony/Component/Yaml/Tests/Fixtures/sfTests.yml b/src/Symfony/Component/Yaml/Tests/Fixtures/sfTests.yml index a427be1c8469..2a0b9c8741cc 100644 --- a/src/Symfony/Component/Yaml/Tests/Fixtures/sfTests.yml +++ b/src/Symfony/Component/Yaml/Tests/Fixtures/sfTests.yml @@ -96,15 +96,6 @@ yaml: | php: | array('foo', array('bar' => array('bar' => 'foo'))) --- -test: A sequence with an unordered array -brief: > - A sequence with an unordered array -yaml: | - 1: foo - 0: bar -php: | - array(1 => 'foo', 0 => 'bar') ---- test: Octal brief: as in spec example 2.19, octal value is converted yaml: | diff --git a/src/Symfony/Component/Yaml/Tests/InlineTest.php b/src/Symfony/Component/Yaml/Tests/InlineTest.php index 7dd1c2158e64..a26ffbf9e44c 100644 --- a/src/Symfony/Component/Yaml/Tests/InlineTest.php +++ b/src/Symfony/Component/Yaml/Tests/InlineTest.php @@ -21,17 +21,17 @@ class InlineTest extends TestCase /** * @dataProvider getTestsForParse */ - public function testParse($yaml, $value) + public function testParse($yaml, $value, $flags = 0) { - $this->assertSame($value, Inline::parse($yaml), sprintf('::parse() converts an inline YAML to a PHP structure (%s)', $yaml)); + $this->assertSame($value, Inline::parse($yaml, $flags), sprintf('::parse() converts an inline YAML to a PHP structure (%s)', $yaml)); } /** * @dataProvider getTestsForParseWithMapObjects */ - public function testParseWithMapObjects($yaml, $value) + public function testParseWithMapObjects($yaml, $value, $flags = Yaml::PARSE_OBJECT_FOR_MAP) { - $actual = Inline::parse($yaml, Yaml::PARSE_OBJECT_FOR_MAP); + $actual = Inline::parse($yaml, $flags); $this->assertSame(serialize($value), serialize($actual)); } @@ -88,11 +88,11 @@ public function testParseWithMapObjectsPassingTrue($yaml, $value) /** * @dataProvider getTestsForDump */ - public function testDump($yaml, $value) + public function testDump($yaml, $value, $parseFlags = 0) { $this->assertEquals($yaml, Inline::dump($value), sprintf('::dump() converts a PHP structure to an inline YAML (%s)', $yaml)); - $this->assertSame($value, Inline::parse(Inline::dump($value)), 'check consistency'); + $this->assertSame($value, Inline::parse(Inline::dump($value), $parseFlags), 'check consistency'); } public function testDumpNumericValueWithLocale() @@ -385,8 +385,8 @@ public function getTestsForParse() array('[\'foo,bar\', \'foo bar\']', array('foo,bar', 'foo bar')), // mappings - array('{foo: bar,bar: foo,false: false,null: null,integer: 12}', array('foo' => 'bar', 'bar' => 'foo', 'false' => false, 'null' => null, 'integer' => 12)), - array('{ foo : bar, bar : foo, false : false, null : null, integer : 12 }', array('foo' => 'bar', 'bar' => 'foo', 'false' => false, 'null' => null, 'integer' => 12)), + array('{foo: bar,bar: foo,false: false,null: null,integer: 12}', array('foo' => 'bar', 'bar' => 'foo', 'false' => false, 'null' => null, 'integer' => 12), Yaml::PARSE_KEYS_AS_STRINGS), + array('{ foo : bar, bar : foo, false : false, null : null, integer : 12 }', array('foo' => 'bar', 'bar' => 'foo', 'false' => false, 'null' => null, 'integer' => 12), Yaml::PARSE_KEYS_AS_STRINGS), array('{foo: \'bar\', bar: \'foo: bar\'}', array('foo' => 'bar', 'bar' => 'foo: bar')), array('{\'foo\': \'bar\', "bar": \'foo: bar\'}', array('foo' => 'bar', 'bar' => 'foo: bar')), array('{\'foo\'\'\': \'bar\', "bar\"": \'foo: bar\'}', array('foo\'' => 'bar', 'bar"' => 'foo: bar')), @@ -454,8 +454,8 @@ public function getTestsForParseWithMapObjects() array('[\'foo,bar\', \'foo bar\']', array('foo,bar', 'foo bar')), // mappings - array('{foo: bar,bar: foo,false: false,null: null,integer: 12}', (object) array('foo' => 'bar', 'bar' => 'foo', 'false' => false, 'null' => null, 'integer' => 12)), - array('{ foo : bar, bar : foo, false : false, null : null, integer : 12 }', (object) array('foo' => 'bar', 'bar' => 'foo', 'false' => false, 'null' => null, 'integer' => 12)), + array('{foo: bar,bar: foo,false: false,null: null,integer: 12}', (object) array('foo' => 'bar', 'bar' => 'foo', 'false' => false, 'null' => null, 'integer' => 12), Yaml::PARSE_OBJECT_FOR_MAP | Yaml::PARSE_KEYS_AS_STRINGS), + array('{ foo : bar, bar : foo, false : false, null : null, integer : 12 }', (object) array('foo' => 'bar', 'bar' => 'foo', 'false' => false, 'null' => null, 'integer' => 12), Yaml::PARSE_OBJECT_FOR_MAP | Yaml::PARSE_KEYS_AS_STRINGS), 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')), @@ -534,7 +534,7 @@ public function getTestsForDump() array('[\'foo,bar\', \'foo bar\']', array('foo,bar', 'foo bar')), // mappings - array('{ foo: bar, bar: foo, \'false\': false, \'null\': null, integer: 12 }', array('foo' => 'bar', 'bar' => 'foo', 'false' => false, 'null' => null, 'integer' => 12)), + array('{ foo: bar, bar: foo, \'false\': false, \'null\': null, integer: 12 }', array('foo' => 'bar', 'bar' => 'foo', 'false' => false, 'null' => null, 'integer' => 12), Yaml::PARSE_KEYS_AS_STRINGS), array('{ foo: bar, bar: \'foo: bar\' }', array('foo' => 'bar', 'bar' => 'foo: bar')), // nested sequences and mappings @@ -550,7 +550,7 @@ 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')), - array('{ foo: { bar: { 1: 2, baz: 3 } } }', array('foo' => array('bar' => array(1 => 2, 'baz' => 3)))), + array('{ foo: { bar: { 1: 2, baz: 3 } } }', array('foo' => array('bar' => array(1 => 2, 'baz' => 3))), Yaml::PARSE_KEYS_AS_STRINGS), ); } @@ -719,14 +719,36 @@ public function getTestsForNullValues() ); } - public function testBooleanMappingKeysAreConvertedToStrings() + public function testTheEmptyStringIsAValidMappingKey() { - $this->assertSame(array('false' => 'foo'), Inline::parse('{false: foo}')); - $this->assertSame(array('true' => 'foo'), Inline::parse('{true: foo}')); + $this->assertSame(array('' => 'foo'), Inline::parse('{ "": foo }')); } - public function testTheEmptyStringIsAValidMappingKey() + /** + * @group legacy + * @expectedDeprecation Implicit casting of incompatible mapping keys to strings is deprecated since version 3.3 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0. Pass the PARSE_KEYS_AS_STRING flag to explicitly enable the type casts. + * @dataProvider getNotPhpCompatibleMappingKeyData + */ + public function testImplicitStringCastingOfMappingKeysIsDeprecated($yaml, $expected) { - $this->assertSame(array('' => 'foo'), Inline::parse('{ "": foo }')); + $this->assertSame($expected, Inline::parse($yaml)); + } + + /** + * @dataProvider getNotPhpCompatibleMappingKeyData + */ + public function testExplicitStringCastingOfMappingKeys($yaml, $expected) + { + $this->assertSame($expected, Inline::parse($yaml, Yaml::PARSE_KEYS_AS_STRINGS)); + } + + public function getNotPhpCompatibleMappingKeyData() + { + return array( + 'boolean-true' => array('{true: "foo"}', array('true' => 'foo')), + 'boolean-false' => array('{false: "foo"}', array('false' => 'foo')), + 'null' => array('{null: "foo"}', array('null' => 'foo')), + 'float' => array('{0.25: "foo"}', array('0.25' => 'foo')), + ); } } diff --git a/src/Symfony/Component/Yaml/Tests/ParserTest.php b/src/Symfony/Component/Yaml/Tests/ParserTest.php index c4958f8456f2..01b17a147dbf 100644 --- a/src/Symfony/Component/Yaml/Tests/ParserTest.php +++ b/src/Symfony/Component/Yaml/Tests/ParserTest.php @@ -34,7 +34,7 @@ protected function tearDown() /** * @dataProvider getDataFormSpecifications */ - public function testSpecifications($file, $expected, $yaml, $comment, $deprecated) + public function testSpecifications($expected, $yaml, $comment, $deprecated) { $deprecations = array(); @@ -66,32 +66,34 @@ public function testSpecifications($file, $expected, $yaml, $comment, $deprecate public function getDataFormSpecifications() { - $parser = new Parser(); - $path = __DIR__.'/Fixtures'; - - $tests = array(); - $files = $parser->parse(file_get_contents($path.'/index.yml')); - foreach ($files as $file) { - $yamls = file_get_contents($path.'/'.$file.'.yml'); + return $this->loadTestsFromFixtureFiles('index.yml'); + } - // split YAMLs documents - foreach (preg_split('/^---( %YAML\:1\.0)?/m', $yamls) as $yaml) { - if (!$yaml) { - continue; - } + /** + * @dataProvider getNonStringMappingKeysData + */ + public function testNonStringMappingKeys($expected, $yaml, $comment) + { + $this->assertSame($expected, var_export($this->parser->parse($yaml, Yaml::PARSE_KEYS_AS_STRINGS), true), $comment); + } - $test = $parser->parse($yaml); - if (isset($test['todo']) && $test['todo']) { - // TODO - } else { - eval('$expected = '.trim($test['php']).';'); + public function getNonStringMappingKeysData() + { + return $this->loadTestsFromFixtureFiles('nonStringKeys.yml'); + } - $tests[] = array($file, var_export($expected, true), $test['yaml'], $test['test'], isset($test['deprecated']) ? $test['deprecated'] : false); - } - } - } + /** + * @group legacy + * @dataProvider getLegacyNonStringMappingKeysData + */ + public function testLegacyNonStringMappingKeys($expected, $yaml, $comment) + { + $this->assertSame($expected, var_export($this->parser->parse($yaml), true), $comment); + } - return $tests; + public function getLegacyNonStringMappingKeysData() + { + return $this->loadTestsFromFixtureFiles('legacyNonStringKeys.yml'); } public function testTabsInYaml() @@ -507,9 +509,15 @@ public function testObjectSupportDisabledButNoExceptions($input) /** * @dataProvider getObjectForMapTests */ - public function testObjectForMap($yaml, $expected) + public function testObjectForMap($yaml, $expected, $explicitlyParseKeysAsStrings = false) { - $this->assertEquals($expected, $this->parser->parse($yaml, Yaml::PARSE_OBJECT_FOR_MAP)); + $flags = Yaml::PARSE_OBJECT_FOR_MAP; + + if ($explicitlyParseKeysAsStrings) { + $flags |= Yaml::PARSE_KEYS_AS_STRINGS; + } + + $this->assertEquals($expected, $this->parser->parse($yaml, $flags)); } /** @@ -568,7 +576,7 @@ public function getObjectForMapTests() $expected->map = new \stdClass(); $expected->map->{1} = 'one'; $expected->map->{2} = 'two'; - $tests['numeric-keys'] = array($yaml, $expected); + $tests['numeric-keys'] = array($yaml, $expected, true); $yaml = <<<'YAML' map: @@ -579,7 +587,7 @@ public function getObjectForMapTests() $expected->map = new \stdClass(); $expected->map->{0} = 'one'; $expected->map->{1} = 'two'; - $tests['zero-indexed-numeric-keys'] = array($yaml, $expected); + $tests['zero-indexed-numeric-keys'] = array($yaml, $expected, true); return $tests; } @@ -1059,6 +1067,10 @@ public function testYamlDirective() $this->assertEquals(array('foo' => 1, 'bar' => 2), $this->parser->parse($yaml)); } + /** + * @group legacy + * @expectedDeprecation Implicit casting of incompatible mapping keys to strings is deprecated since version 3.3 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0. Pass the PARSE_KEYS_AS_STRING flag to explicitly enable the type casts. + */ public function testFloatKeys() { $yaml = <<<'EOF' @@ -1077,6 +1089,58 @@ public function testFloatKeys() $this->assertEquals($expected, $this->parser->parse($yaml)); } + /** + * @group legacy + * @expectedDeprecation Implicit casting of incompatible mapping keys to strings is deprecated since version 3.3 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0. Pass the PARSE_KEYS_AS_STRING flag to explicitly enable the type casts. + */ + public function testBooleanKeys() + { + $yaml = <<<'EOF' +true: foo +false: bar +EOF; + + $expected = array( + 1 => 'foo', + 0 => 'bar', + ); + + $this->assertEquals($expected, $this->parser->parse($yaml)); + } + + public function testExplicitStringCastingOfFloatKeys() + { + $yaml = <<<'EOF' +foo: + 1.2: "bar" + 1.3: "baz" +EOF; + + $expected = array( + 'foo' => array( + '1.2' => 'bar', + '1.3' => 'baz', + ), + ); + + $this->assertEquals($expected, $this->parser->parse($yaml, Yaml::PARSE_KEYS_AS_STRINGS)); + } + + public function testExplicitStringCastingOfBooleanKeys() + { + $yaml = <<<'EOF' +true: foo +false: bar +EOF; + + $expected = array( + 'true' => 'foo', + 'false' => 'bar', + ); + + $this->assertEquals($expected, $this->parser->parse($yaml, Yaml::PARSE_KEYS_AS_STRINGS)); + } + /** * @expectedException \Symfony\Component\Yaml\Exception\ParseException * @expectedExceptionMessage A colon cannot be used in an unquoted mapping value @@ -1575,6 +1639,35 @@ public function testExceptionWhenUsingUnsuportedBuiltInTags() { $this->parser->parse('!!foo'); } + + private function loadTestsFromFixtureFiles($testsFile) + { + $parser = new Parser(); + + $tests = array(); + $files = $parser->parse(file_get_contents(__DIR__.'/Fixtures/'.$testsFile)); + foreach ($files as $file) { + $yamls = file_get_contents(__DIR__.'/Fixtures/'.$file.'.yml'); + + // split YAMLs documents + foreach (preg_split('/^---( %YAML\:1\.0)?/m', $yamls) as $yaml) { + if (!$yaml) { + continue; + } + + $test = $parser->parse($yaml); + if (isset($test['todo']) && $test['todo']) { + // TODO + } else { + eval('$expected = '.trim($test['php']).';'); + + $tests[] = array(var_export($expected, true), $test['yaml'], $test['test'], isset($test['deprecated']) ? $test['deprecated'] : false); + } + } + } + + return $tests; + } } class B diff --git a/src/Symfony/Component/Yaml/Yaml.php b/src/Symfony/Component/Yaml/Yaml.php index f88f0a51c379..8ac6ecb967b7 100644 --- a/src/Symfony/Component/Yaml/Yaml.php +++ b/src/Symfony/Component/Yaml/Yaml.php @@ -30,6 +30,7 @@ class Yaml const DUMP_MULTI_LINE_LITERAL_BLOCK = 128; const PARSE_CONSTANT = 256; const DUMP_EMPTY_ARRAY_AS_SEQUENCE = 1024; + const PARSE_KEYS_AS_STRINGS = 2048; /** * @experimental in version 3.3