diff --git a/src/Symfony/Component/Yaml/Parser.php b/src/Symfony/Component/Yaml/Parser.php index 9539e36aa3c7f..0577b5e8853bf 100644 --- a/src/Symfony/Component/Yaml/Parser.php +++ b/src/Symfony/Component/Yaml/Parser.php @@ -66,6 +66,7 @@ public function parse($value, $exceptionOnInvalidType = false, $objectSupport = $data = array(); $context = null; + $allowOverwrite = false; while ($this->moveToNextLine()) { if ($this->isCurrentLineEmpty()) { continue; @@ -76,7 +77,7 @@ public function parse($value, $exceptionOnInvalidType = false, $objectSupport = throw new ParseException('A YAML file cannot contain tabs as indentation.', $this->getRealCurrentLineNb() + 1, $this->currentLine); } - $isRef = $isInPlace = $isProcessed = false; + $isRef = $mergeNode = false; if (preg_match('#^\-((?P\s+)(?P.+?))?\s*$#u', $this->currentLine, $values)) { if ($context && 'mapping' == $context) { throw new ParseException('You cannot define a sequence item when in a mapping'); @@ -132,10 +133,24 @@ public function parse($value, $exceptionOnInvalidType = false, $objectSupport = } if ('<<' === $key) { + $mergeNode = true; + $allowOverwrite = true; if (isset($values['value']) && 0 === strpos($values['value'], '*')) { - $isInPlace = substr($values['value'], 1); - if (!array_key_exists($isInPlace, $this->refs)) { - throw new ParseException(sprintf('Reference "%s" does not exist.', $isInPlace), $this->getRealCurrentLineNb() + 1, $this->currentLine); + $refName = substr($values['value'], 1); + if (!array_key_exists($refName, $this->refs)) { + throw new ParseException(sprintf('Reference "%s" does not exist.', $refName), $this->getRealCurrentLineNb() + 1, $this->currentLine); + } + + $refValue = $this->refs[$refName]; + + if (!is_array($refValue)) { + throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine); + } + + foreach ($refValue as $key => $value) { + if (!isset($data[$key])) { + $data[$key] = $value; + } } } else { if (isset($values['value']) && $values['value'] !== '') { @@ -148,40 +163,49 @@ public function parse($value, $exceptionOnInvalidType = false, $objectSupport = $parser->refs =& $this->refs; $parsed = $parser->parse($value, $exceptionOnInvalidType, $objectSupport); - $merged = array(); if (!is_array($parsed)) { throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine); - } elseif (isset($parsed[0])) { - // Numeric array, merge individual elements - foreach (array_reverse($parsed) as $parsedItem) { + } + + if (isset($parsed[0])) { + // If the value associated with the merge key is a sequence, then this sequence is expected to contain mapping nodes + // and each of these nodes is merged in turn according to its order in the sequence. Keys in mapping nodes earlier + // in the sequence override keys specified in later mapping nodes. + foreach ($parsed as $parsedItem) { if (!is_array($parsedItem)) { throw new ParseException('Merge items must be arrays.', $this->getRealCurrentLineNb() + 1, $parsedItem); } - $merged = array_merge($parsedItem, $merged); + + foreach ($parsedItem as $key => $value) { + if (!isset($data[$key])) { + $data[$key] = $value; + } + } } } else { - // Associative array, merge - $merged = array_merge($merged, $parsed); + // If the value associated with the key is a single mapping node, each of its key/value pairs is inserted into the + // current mapping, unless the key already exists in it. + foreach ($parsed as $key => $value) { + if (!isset($data[$key])) { + $data[$key] = $value; + } + } } - - $isProcessed = $merged; } } elseif (isset($values['value']) && preg_match('#^&(?P[^ ]+) *(?P.*)#u', $values['value'], $matches)) { $isRef = $matches['ref']; $values['value'] = $matches['value']; } - if ($isProcessed) { + if ($mergeNode) { // Merge keys - $data = $isProcessed; - // hash } elseif (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#')) { + // hash // if next line is less indented or equal, then it means that the current value is null if (!$this->isNextLineIndented() && !$this->isNextLineUnIndentedCollection()) { // Spec: Keys MUST be unique; first one wins. - // Parser cannot abort this mapping earlier, since lines - // are processed sequentially. - if (!isset($data[$key])) { + // But overwriting is allowed when a merge node is used in current block. + if ($allowOverwrite || !isset($data[$key])) { $data[$key] = null; } } else { @@ -190,23 +214,17 @@ public function parse($value, $exceptionOnInvalidType = false, $objectSupport = $parser->refs =& $this->refs; $value = $parser->parse($this->getNextEmbedBlock(), $exceptionOnInvalidType, $objectSupport); // Spec: Keys MUST be unique; first one wins. - // Parser cannot abort this mapping earlier, since lines - // are processed sequentially. - if (!isset($data[$key])) { + // But overwriting is allowed when a merge node is used in current block. + if ($allowOverwrite || !isset($data[$key])) { $data[$key] = $value; } } } else { - if ($isInPlace) { - $data = $this->refs[$isInPlace]; - } else { - $value = $this->parseValue($values['value'], $exceptionOnInvalidType, $objectSupport);; - // Spec: Keys MUST be unique; first one wins. - // Parser cannot abort this mapping earlier, since lines - // are processed sequentially. - if (!isset($data[$key])) { - $data[$key] = $value; - } + $value = $this->parseValue($values['value'], $exceptionOnInvalidType, $objectSupport); + // 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 { diff --git a/src/Symfony/Component/Yaml/Tests/Fixtures/sfMergeKey.yml b/src/Symfony/Component/Yaml/Tests/Fixtures/sfMergeKey.yml index 3eec4f877daac..fd9910174dade 100644 --- a/src/Symfony/Component/Yaml/Tests/Fixtures/sfMergeKey.yml +++ b/src/Symfony/Component/Yaml/Tests/Fixtures/sfMergeKey.yml @@ -10,9 +10,19 @@ yaml: | a: Steve b: Clark c: Brian - bar: &bar + bar: + a: before + d: other <<: *foo + b: new x: Oren + c: + foo: bar + foo: ignore + bar: foo + duplicate: + foo: bar + foo: ignore foo2: &foo2 a: Ballmer ding: &dong [ fi, fei, fo, fam] @@ -24,4 +34,12 @@ yaml: | head: <<: [ *foo , *dong , *foo2 ] php: | - array('foo' => array('a' => 'Steve', 'b' => 'Clark', 'c' => 'Brian'), 'bar' => array('a' => 'Steve', 'b' => 'Clark', 'c' => 'Brian', 'x' => 'Oren'), 'foo2' => array('a' => 'Ballmer'), 'ding' => array('fi', 'fei', 'fo', 'fam'), 'check' => array('a' => 'Steve', 'b' => 'Clark', 'c' => 'Brian', 'fi', 'fei', 'fo', 'fam', 'isit' => 'tested'), 'head' => array('a' => 'Ballmer', 'b' => 'Clark', 'c' => 'Brian', 'fi', 'fei', 'fo', 'fam')) + array( + 'foo' => array('a' => 'Steve', 'b' => 'Clark', 'c' => 'Brian'), + 'bar' => array('a' => 'before', 'd' => 'other', 'b' => 'new', 'c' => array('foo' => 'bar', 'bar' => 'foo'), 'x' => 'Oren'), + 'duplicate' => array('foo' => 'bar'), + 'foo2' => array('a' => 'Ballmer'), + 'ding' => array('fi', 'fei', 'fo', 'fam'), + 'check' => array('a' => 'Steve', 'b' => 'Clark', 'c' => 'Brian', 'fi', 'fei', 'fo', 'fam', 'isit' => 'tested'), + 'head' => array('a' => 'Steve', 'b' => 'Clark', 'c' => 'Brian', 'fi', 'fei', 'fo', 'fam') + )