[Yaml] Add tags support

This commit is contained in:
Guilhem N 2017-02-06 18:23:56 +01:00 committed by Nicolas Grekas
parent 554b1a748f
commit 47441070e4
10 changed files with 362 additions and 88 deletions

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\DependencyInjection\Dumper;
use Symfony\Component\Yaml\Dumper as YmlDumper;
use Symfony\Component\Yaml\Tag\TaggedValue;
use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\Argument\ClosureProxyArgument;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
@ -251,10 +252,10 @@ class YamlDumper extends Dumper
*/
private function dumpValue($value)
{
if ($value instanceof IteratorArgument) {
$value = array('=iterator' => $value->getValues());
} elseif ($value instanceof ClosureProxyArgument) {
$value = array('=closure_proxy' => $value->getValues());
if ($value instanceof IteratorArgument || $value instanceof ClosureProxyArgument) {
$tag = $value instanceof IteratorArgument ? 'iterator' : 'closure_proxy';
return new TaggedValue($tag, $this->dumpValue($value->getValues()));
}
if (is_array($value)) {

View File

@ -23,6 +23,7 @@ use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\Yaml\Exception\ParseException;
use Symfony\Component\Yaml\Parser as YamlParser;
use Symfony\Component\Yaml\Tag\TaggedValue;
use Symfony\Component\Yaml\Yaml;
use Symfony\Component\ExpressionLanguage\Expression;
@ -506,7 +507,7 @@ class YamlFileLoader extends FileLoader
}
try {
$configuration = $this->yamlParser->parse(file_get_contents($file), Yaml::PARSE_CONSTANT);
$configuration = $this->yamlParser->parse(file_get_contents($file), Yaml::PARSE_CONSTANT | Yaml::PARSE_CUSTOM_TAGS);
} catch (ParseException $e) {
throw new InvalidArgumentException(sprintf('The file "%s" does not contain valid YAML.', $file), 0, $e);
}
@ -557,42 +558,42 @@ class YamlFileLoader extends FileLoader
/**
* Resolves services.
*
* @param string|array $value
* @param mixed $value
*
* @return array|string|Reference
* @return array|string|Reference|ArgumentInterface
*/
private function resolveServices($value)
{
if (is_array($value)) {
if (array_key_exists('=iterator', $value)) {
if (1 !== count($value)) {
throw new InvalidArgumentException('Arguments typed "=iterator" must have no sibling keys.');
if ($value instanceof TaggedValue) {
$argument = $value->getValue();
if ('iterator' === $value->getTag()) {
if (!is_array($argument)) {
throw new InvalidArgumentException('"!iterator" tag only accepts sequences.');
}
if (!is_array($value = $value['=iterator'])) {
throw new InvalidArgumentException('Arguments typed "=iterator" must be arrays.');
return new IteratorArgument(array_map(array($this, 'resolveServices'), $argument));
}
if ('closure_proxy' === $value->getTag()) {
if (!is_array($argument) || array(0, 1) !== array_keys($argument) || !is_string($argument[0]) || !is_string($argument[1]) || 0 !== strpos($argument[0], '@') || 0 === strpos($argument[0], '@@')) {
throw new InvalidArgumentException('"!closure_proxy" tagged values must be arrays of [@service, method].');
}
$value = new IteratorArgument(array_map(array($this, 'resolveServices'), $value));
} elseif (array_key_exists('=closure_proxy', $value)) {
if (1 !== count($value)) {
throw new InvalidArgumentException('Arguments typed "=closure_proxy" must have no sibling keys.');
}
if (!is_array($value = $value['=closure_proxy']) || array(0, 1) !== array_keys($value)) {
throw new InvalidArgumentException('Arguments typed "=closure_proxy" must be arrays of [@service, method].');
}
if (!is_string($value[0]) || !is_string($value[1]) || 0 !== strpos($value[0], '@') || 0 === strpos($value[0], '@@')) {
throw new InvalidArgumentException('Arguments typed "=closure_proxy" must be arrays of [@service, method].');
}
if (0 === strpos($value[0], '@?')) {
$value[0] = substr($value[0], 2);
if (0 === strpos($argument[0], '@?')) {
$argument[0] = substr($argument[0], 2);
$invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE;
} else {
$value[0] = substr($value[0], 1);
$argument[0] = substr($argument[0], 1);
$invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE;
}
$value = new ClosureProxyArgument($value[0], $value[1], $invalidBehavior);
} else {
$value = array_map(array($this, 'resolveServices'), $value);
return new ClosureProxyArgument($argument[0], $argument[1], $invalidBehavior);
}
throw new InvalidArgumentException(sprintf('Unsupported tag "!%s".', $value->getTag()));
}
if (is_array($value)) {
$value = array_map(array($this, 'resolveServices'), $value);
} elseif (is_string($value) && 0 === strpos($value, '@=')) {
return new Expression(substr($value, 2));
} elseif (is_string($value) && 0 === strpos($value, '@')) {

View File

@ -14,6 +14,7 @@ namespace Symfony\Component\DependencyInjection\Tests\Dumper;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Dumper\YamlDumper;
use Symfony\Component\Yaml\Yaml;
use Symfony\Component\Yaml\Parser;
class YamlDumperTest extends \PHPUnit_Framework_TestCase
{
@ -62,8 +63,10 @@ class YamlDumperTest extends \PHPUnit_Framework_TestCase
$this->assertStringEqualsFile(self::$fixturesPath.'/yaml/services24.yml', $dumper->dump());
}
private function assertEqualYamlStructure($yaml, $expected, $message = '')
private function assertEqualYamlStructure($expected, $yaml, $message = '')
{
$this->assertEquals(Yaml::parse($expected), Yaml::parse($yaml), $message);
$parser = new Parser();
$this->assertEquals($parser->parse($expected, Yaml::PARSE_CUSTOM_TAGS), $parser->parse($yaml, Yaml::PARSE_CUSTOM_TAGS), $message);
}
}

View File

@ -109,12 +109,12 @@ services:
factory: ['@factory_simple', getInstance]
lazy_context:
class: LazyContext
arguments: [{ '=iterator': [foo, '@foo.baz', { '%foo%': 'foo is %foo%', foobar: '%foo%' }, true, '@service_container'] }]
arguments: [!iterator [foo, '@foo.baz', { '%foo%': 'foo is %foo%', foobar: '%foo%' }, true, '@service_container']]
lazy_context_ignore_invalid_ref:
class: LazyContext
arguments: [{ '=iterator': ['@foo.baz', '@?invalid'] }]
arguments: [!iterator ['@foo.baz', '@?invalid']]
closure_proxy:
class: BarClass
arguments: [{ '=closure_proxy': ['@closure_proxy', getBaz] }]
arguments: [!closure_proxy ['@closure_proxy', getBaz]]
alias_for_foo: '@foo'
alias_for_alias: '@foo'

View File

@ -19,7 +19,7 @@
"php": ">=5.5.9"
},
"require-dev": {
"symfony/yaml": "~3.2",
"symfony/yaml": "~3.3",
"symfony/config": "~3.3",
"symfony/expression-language": "~2.8|~3.0"
},
@ -30,8 +30,8 @@
"symfony/proxy-manager-bridge": "Generate service proxies to lazy load them"
},
"conflict": {
"symfony/config": "<3.3",
"symfony/yaml": "<3.2"
"symfony/yaml": "<3.3",
"symfony/config": "<3.3"
},
"autoload": {
"psr-4": { "Symfony\\Component\\DependencyInjection\\": "" },

View File

@ -13,6 +13,7 @@ namespace Symfony\Component\Yaml;
use Symfony\Component\Yaml\Exception\ParseException;
use Symfony\Component\Yaml\Exception\DumpException;
use Symfony\Component\Yaml\Tag\TaggedValue;
/**
* Inline implements a YAML parser/dumper for the YAML inline syntax.
@ -94,7 +95,8 @@ class Inline
}
$i = 0;
switch ($value[0]) {
$tag = self::parseTag($value, $i, $flags);
switch ($value[$i]) {
case '[':
$result = self::parseSequence($value, $flags, $i, $references);
++$i;
@ -104,7 +106,11 @@ class Inline
++$i;
break;
default:
$result = self::parseScalar($value, $flags, null, $i, true, $references);
$result = self::parseScalar($value, $flags, null, $i, null === $tag, $references);
}
if (null !== $tag) {
return new TaggedValue($tag, $result);
}
// some comments are allowed at the end
@ -159,6 +165,10 @@ class Inline
case $value instanceof \DateTimeInterface:
return $value->format('c');
case is_object($value):
if ($value instanceof TaggedValue) {
return '!'.$value->getTag().' '.self::dump($value->getValue(), $flags);
}
if (Yaml::DUMP_OBJECT & $flags) {
return '!php/object:'.serialize($value);
}
@ -382,23 +392,28 @@ class Inline
// [foo, bar, ...]
while ($i < $len) {
if (']' === $sequence[$i]) {
return $output;
}
if (',' === $sequence[$i] || ' ' === $sequence[$i]) {
++$i;
continue;
}
$tag = self::parseTag($sequence, $i, $flags);
switch ($sequence[$i]) {
case '[':
// nested sequence
$output[] = self::parseSequence($sequence, $flags, $i, $references);
$value = self::parseSequence($sequence, $flags, $i, $references);
break;
case '{':
// nested mapping
$output[] = self::parseMapping($sequence, $flags, $i, $references);
break;
case ']':
return $output;
case ',':
case ' ':
$value = self::parseMapping($sequence, $flags, $i, $references);
break;
default:
$isQuoted = in_array($sequence[$i], array('"', "'"));
$value = self::parseScalar($sequence, $flags, array(',', ']'), $i, true, $references);
$value = self::parseScalar($sequence, $flags, array(',', ']'), $i, null === $tag, $references);
// the value can be an array if a reference has been resolved to an array var
if (is_string($value) && !$isQuoted && false !== strpos($value, ': ')) {
@ -411,11 +426,15 @@ class Inline
}
}
$output[] = $value;
--$i;
}
if (null !== $tag) {
$value = new TaggedValue($tag, $value);
}
$output[] = $value;
++$i;
}
@ -466,10 +485,15 @@ class Inline
@trigger_error('Using a colon that is not followed by an indication character (i.e. " ", ",", "[", "]", "{", "}" is deprecated since version 3.2 and will throw a ParseException in 4.0.', E_USER_DEPRECATED);
}
// value
$done = false;
while ($i < $len) {
if (':' === $mapping[$i] || ' ' === $mapping[$i]) {
++$i;
continue;
}
$tag = self::parseTag($mapping, $i, $flags);
$duplicate = false;
switch ($mapping[$i]) {
case '[':
// nested sequence
@ -477,12 +501,10 @@ class Inline
// Spec: Keys MUST be unique; first one wins.
// Parser cannot abort this mapping earlier, since lines
// are processed sequentially.
if (!isset($output[$key])) {
$output[$key] = $value;
} else {
if (isset($output[$key])) {
@trigger_error(sprintf('Duplicate key "%s" detected on line %d whilst parsing YAML. Silent handling of duplicate mapping keys in YAML is deprecated since version 3.2 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0.', $key, self::$parsedLineNumber + 1), E_USER_DEPRECATED);
$duplicate = true;
}
$done = true;
break;
case '{':
// nested mapping
@ -490,35 +512,33 @@ class Inline
// Spec: Keys MUST be unique; first one wins.
// Parser cannot abort this mapping earlier, since lines
// are processed sequentially.
if (!isset($output[$key])) {
$output[$key] = $value;
} else {
if (isset($output[$key])) {
@trigger_error(sprintf('Duplicate key "%s" detected on line %d whilst parsing YAML. Silent handling of duplicate mapping keys in YAML is deprecated since version 3.2 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0.', $key, self::$parsedLineNumber + 1), E_USER_DEPRECATED);
$duplicate = true;
}
$done = true;
break;
case ':':
case ' ':
break;
default:
$value = self::parseScalar($mapping, $flags, array(',', '}'), $i, true, $references);
$value = self::parseScalar($mapping, $flags, array(',', '}'), $i, null === $tag, $references);
// Spec: Keys MUST be unique; first one wins.
// Parser cannot abort this mapping earlier, since lines
// are processed sequentially.
if (!isset($output[$key])) {
$output[$key] = $value;
} else {
if (isset($output[$key])) {
@trigger_error(sprintf('Duplicate key "%s" detected on line %d whilst parsing YAML. Silent handling of duplicate mapping keys in YAML is deprecated since version 3.2 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0.', $key, self::$parsedLineNumber + 1), E_USER_DEPRECATED);
$duplicate = true;
}
$done = true;
--$i;
}
if (!$duplicate) {
if (null !== $tag) {
$output[$key] = new TaggedValue($tag, $value);
} else {
$output[$key] = $value;
}
}
++$i;
if ($done) {
continue 2;
}
continue 2;
}
}
@ -569,8 +589,7 @@ class Inline
return true;
case 'false' === $scalarLower:
return false;
// Optimise for returning strings.
case $scalar[0] === '+' || $scalar[0] === '-' || $scalar[0] === '.' || $scalar[0] === '!' || is_numeric($scalar[0]):
case $scalar[0] === '!':
switch (true) {
case 0 === strpos($scalar, '!str'):
return (string) substr($scalar, 5);
@ -613,6 +632,15 @@ class Inline
return;
case 0 === strpos($scalar, '!!float '):
return (float) substr($scalar, 8);
case 0 === strpos($scalar, '!!binary '):
return self::evaluateBinaryScalar(substr($scalar, 9));
default:
@trigger_error(sprintf('Using the unquoted scalar value "%s" is deprecated since version 3.3 and will be considered as a tagged value in 4.0. You must quote it.', $scalar), E_USER_DEPRECATED);
}
// Optimize for returning strings.
case $scalar[0] === '+' || $scalar[0] === '-' || $scalar[0] === '.' || is_numeric($scalar[0]):
switch (true) {
case preg_match('{^[+-]?[0-9][0-9_]*$}', $scalar):
$scalar = str_replace('_', '', (string) $scalar);
// omitting the break / return as integers are handled in the next case
@ -636,8 +664,6 @@ class Inline
return -log(0);
case '-.inf' === $scalarLower:
return log(0);
case 0 === strpos($scalar, '!!binary '):
return self::evaluateBinaryScalar(substr($scalar, 9));
case preg_match('/^(-|\+)?[0-9][0-9,]*(\.[0-9_]+)?$/', $scalar):
case preg_match('/^(-|\+)?[0-9][0-9_]*(\.[0-9_]+)?$/', $scalar):
if (false !== strpos($scalar, ',')) {
@ -658,9 +684,48 @@ class Inline
return $time;
}
default:
return (string) $scalar;
}
return (string) $scalar;
}
/**
* @param string $value
* @param int &$i
* @param int $flags
*
* @return null|string
*/
private static function parseTag($value, &$i, $flags)
{
if ('!' !== $value[$i]) {
return;
}
$tagLength = strcspn($value, " \t\n", $i + 1);
$tag = substr($value, $i + 1, $tagLength);
$nextOffset = $i + $tagLength + 1;
$nextOffset += strspn($value, ' ', $nextOffset);
// Is followed by a scalar
if (!isset($value[$nextOffset]) || !in_array($value[$nextOffset], array('[', '{'), true)) {
// Manage scalars in {@link self::evaluateScalar()}
return;
}
// Built-in tags
if ($tag && '!' === $tag[0]) {
throw new ParseException(sprintf('The built-in tag "!%s" is not implemented.', $tag));
}
if (Yaml::PARSE_CUSTOM_TAGS & $flags) {
$i = $nextOffset;
return $tag;
}
throw new ParseException(sprintf('Tags support is not enabled. Enable the `Yaml::PARSE_CUSTOM_TAGS` flag to use "!%s".', $tag));
}
/**

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\Yaml;
use Symfony\Component\Yaml\Exception\ParseException;
use Symfony\Component\Yaml\Tag\TaggedValue;
/**
* Parser parses YAML strings to convert them to PHP arrays.
@ -20,7 +21,7 @@ use Symfony\Component\Yaml\Exception\ParseException;
*/
class Parser
{
const TAG_PATTERN = '((?P<tag>![\w!.\/:-]+) +)?';
const TAG_PATTERN = '(?P<tag>![\w!.\/:-]+)';
const BLOCK_SCALAR_HEADER_PATTERN = '(?P<separator>\||>)(?P<modifiers>\+|\-|\d+|\+\d+|\-\d+|\d+\+|\d+\-)?(?P<comments> +#.*)?';
private $offset = 0;
@ -102,10 +103,26 @@ class Parser
mb_internal_encoding('UTF-8');
}
if (!$this->moveToNextLine()) {
return null;
}
$data = array();
$context = null;
$allowOverwrite = false;
while ($this->moveToNextLine()) {
while ($this->isCurrentLineEmpty()) {
if (!$this->moveToNextLine()) {
return null;
}
}
// Resolves the tag and returns if end of the document
if (null !== ($tag = $this->getLineTag($this->currentLine, $flags, false)) && !$this->moveToNextLine()) {
return new TaggedValue($tag, '');
}
do {
if ($this->isCurrentLineEmpty()) {
continue;
}
@ -130,9 +147,14 @@ class Parser
// array
if (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#')) {
$data[] = $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(null, true), $flags);
} elseif (null !== $subTag = $this->getLineTag(ltrim($values['value'], ' '), $flags)) {
$data[] = new TaggedValue(
$subTag,
$this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(null, true), $flags)
);
} else {
if (isset($values['leadspaces'])
&& preg_match('#^(?P<key>'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\{\[].*?) *\:(\s+(?P<value>.+?))?\s*$#u', $values['value'], $matches)
&& preg_match('#^(?P<key>'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\{\[].*?) *\:(\s+(?P<value>.+?))?\s*$#u', $this->trimTag($values['value']), $matches)
) {
// this is a compact notation element, add to next block and parse
$block = $values['value'];
@ -148,7 +170,7 @@ class Parser
if ($isRef) {
$this->refs[$isRef] = end($data);
}
} 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('"', "'")))) {
} elseif (preg_match('#^(?P<key>'.Inline::REGEX_QUOTED_STRING.'|(?:![^\s]+\s+)?[^ \'"\[\{!].*?) *\:(\s+(?P<value>.+?))?\s*$#u', $this->currentLine, $values) && (false === strpos($values['key'], ' #') || in_array($values['key'][0], array('"', "'")))) {
if ($context && 'sequence' == $context) {
throw new ParseException('You cannot define a mapping item when in a sequence', $this->currentLineNb + 1, $this->currentLine);
}
@ -233,16 +255,21 @@ class Parser
$values['value'] = $matches['value'];
}
$subTag = null;
if ($mergeNode) {
// Merge keys
} elseif (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#')) {
} elseif (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#') || (null !== $subTag = $this->getLineTag(ltrim($values['value'], ' '), $flags))) {
// 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.
// But overwriting is allowed when a merge node is used in current block.
if ($allowOverwrite || !isset($data[$key])) {
$data[$key] = null;
if (null !== $subTag) {
$data[$key] = new TaggedValue($subTag, '');
} else {
$data[$key] = null;
}
} else {
@trigger_error(sprintf('Duplicate key "%s" detected on line %d whilst parsing YAML. Silent handling of duplicate mapping keys in YAML is deprecated since version 3.2 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0.', $key, $this->getRealCurrentLineNb() + 1), E_USER_DEPRECATED);
}
@ -253,7 +280,11 @@ class Parser
// 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;
if (null !== $subTag) {
$data[$key] = new TaggedValue($subTag, $value);
} else {
$data[$key] = $value;
}
} else {
@trigger_error(sprintf('Duplicate key "%s" detected on line %d whilst parsing YAML. Silent handling of duplicate mapping keys in YAML is deprecated since version 3.2 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0.', $key, $realCurrentLineNbKey + 1), E_USER_DEPRECATED);
}
@ -354,6 +385,10 @@ class Parser
throw new ParseException($error, $this->getRealCurrentLineNb() + 1, $this->currentLine);
}
} while ($this->moveToNextLine());
if (null !== $tag) {
$data = new TaggedValue($tag, $data);
}
if (isset($mbEncoding)) {
@ -599,13 +634,17 @@ class Parser
return $this->refs[$value];
}
if (preg_match('/^'.self::TAG_PATTERN.self::BLOCK_SCALAR_HEADER_PATTERN.'$/', $value, $matches)) {
if (preg_match('/^(?:'.self::TAG_PATTERN.' +)?'.self::BLOCK_SCALAR_HEADER_PATTERN.'$/', $value, $matches)) {
$modifiers = isset($matches['modifiers']) ? $matches['modifiers'] : '';
$data = $this->parseBlockScalar($matches['separator'], preg_replace('#\d+#', '', $modifiers), (int) abs($modifiers));
if (isset($matches['tag']) && '!!binary' === $matches['tag']) {
return Inline::evaluateBinaryScalar($data);
if ('' !== $matches['tag']) {
if ('!!binary' === $matches['tag']) {
return Inline::evaluateBinaryScalar($data);
} elseif ('!' !== $matches['tag']) {
@trigger_error(sprintf('Using the custom tag "%s" for the value "%s" is deprecated since version 3.3. It will be replaced by an instance of %s in 4.0.', $matches['tag'], $data, TaggedValue::class), E_USER_DEPRECATED);
}
}
return $data;
@ -917,4 +956,43 @@ class Parser
{
return (bool) preg_match('~'.self::BLOCK_SCALAR_HEADER_PATTERN.'$~', $this->currentLine);
}
/**
* Trim the tag on top of the value.
*
* Prevent values such as `!foo {quz: bar}` to be considered as
* a mapping block.
*/
private function trimTag($value)
{
if ('!' === $value[0]) {
return ltrim(substr($value, 1, strcspn($value, " \r\n", 1)), ' ');
}
return $value;
}
private function getLineTag($value, $flags, $nextLineCheck = true)
{
if ('' === $value || '!' !== $value[0] || 1 !== preg_match('/^'.self::TAG_PATTERN.' *( +#.*)?$/', $value, $matches)) {
return;
}
if ($nextLineCheck && !$this->isNextLineIndented()) {
return;
}
$tag = substr($matches['tag'], 1);
// Built-in tags
if ($tag && '!' === $tag[0]) {
throw new ParseException(sprintf('The built-in tag "!%s" is not implemented.', $tag));
}
if (Yaml::PARSE_CUSTOM_TAGS & $flags) {
return $tag;
}
throw new ParseException(sprintf('Tags support is not enabled. You must use the flag `Yaml::PARSE_CUSTOM_TAGS` to use "%s".', $matches['tag']));
}
}

View File

@ -0,0 +1,50 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Yaml\Tag;
/**
* @author Nicolas Grekas <p@tchwork.com>
* @author Guilhem N. <egetick@gmail.com>
*
* @experimental in version 3.3
*/
final class TaggedValue
{
private $tag;
private $value;
/**
* @param string $tag
* @param mixed $value
*/
public function __construct($tag, $value)
{
$this->tag = $tag;
$this->value = $value;
}
/**
* @return string
*/
public function getTag()
{
return $this->tag;
}
/**
* @return mixed
*/
public function getValue()
{
return $this->value;
}
}

View File

@ -13,6 +13,7 @@ namespace Symfony\Component\Yaml\Tests;
use Symfony\Component\Yaml\Yaml;
use Symfony\Component\Yaml\Parser;
use Symfony\Component\Yaml\Tag\TaggedValue;
class ParserTest extends \PHPUnit_Framework_TestCase
{
@ -1491,6 +1492,76 @@ EOF;
$this->assertEquals($expected, $this->parser->parse($yaml));
}
public function testTaggedInlineMapping()
{
$this->assertEquals(new TaggedValue('foo', array('foo' => 'bar')), $this->parser->parse('!foo {foo: bar}', Yaml::PARSE_CUSTOM_TAGS));
}
/**
* @dataProvider taggedValuesProvider
*/
public function testCustomTagSupport($expected, $yaml)
{
$this->assertEquals($expected, $this->parser->parse($yaml, Yaml::PARSE_CUSTOM_TAGS));
}
public function taggedValuesProvider()
{
return array(
'sequences' => array(
array(new TaggedValue('foo', array('yaml')), new TaggedValue('quz', array('bar'))),
<<<YAML
- !foo
- yaml
- !quz [bar]
YAML
),
'mappings' => array(
new TaggedValue('foo', array('foo' => new TaggedValue('quz', array('bar')), 'quz' => new TaggedValue('foo', array('quz' => 'bar')))),
<<<YAML
!foo
foo: !quz [bar]
quz: !foo
quz: bar
YAML
),
'inline' => array(
array(new TaggedValue('foo', array('foo', 'bar')), new TaggedValue('quz', array('foo' => 'bar', 'quz' => new TaggedValue('bar', array('one' => 'bar'))))),
<<<YAML
- !foo [foo, bar]
- !quz {foo: bar, quz: !bar {one: bar}}
YAML
),
);
}
/**
* @expectedException \Symfony\Component\Yaml\Exception\ParseException
* @expectedExceptionMessage Tags support is not enabled. Enable the `Yaml::PARSE_CUSTOM_TAGS` flag to use "!iterator" at line 1 (near "!iterator [foo]").
*/
public function testCustomTagsDisabled()
{
$this->parser->parse('!iterator [foo]');
}
/**
* @group legacy
* @expectedDeprecation Using the unquoted scalar value "!iterator foo" is deprecated since version 3.3 and will be considered as a tagged value in 4.0. You must quote it.
*/
public function testUnsupportedTagWithScalar()
{
$this->assertEquals('!iterator foo', $this->parser->parse('!iterator foo'));
}
/**
* @expectedException \Symfony\Component\Yaml\Exception\ParseException
* @expectedExceptionMessage The built-in tag "!!foo" is not implemented.
*/
public function testExceptionWhenUsingUnsuportedBuiltInTags()
{
$this->parser->parse('!!foo');
}
}
class B

View File

@ -30,6 +30,11 @@ class Yaml
const DUMP_MULTI_LINE_LITERAL_BLOCK = 128;
const PARSE_CONSTANT = 256;
/**
* @experimental in version 3.3
*/
const PARSE_CUSTOM_TAGS = 512;
/**
* Parses YAML into a PHP value.
*