Permalink
Browse files

feature #21194 [Yaml] Add tags support (GuilhemN)

This PR was squashed before being merged into the 3.3-dev branch (closes #21194).

Discussion
----------

[Yaml] Add tags support

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | yes
| Tests pass?   | yes
| Fixed tickets | #21185
| License       | MIT
| Doc PR        |

This PR adds custom tags support to the Yaml component.
Symfony tags (`!!binary`, `!str`, etc.) are still managed in the parser to have a lighter diff but we'll be able to convert them later if we want to.

The primary addition of this PR is the `TagInterface`:
```php
interface TagInterface
{
    public function construct(mixed $value): mixed;
}
```

It can be used to register custom tags. An example that could be used to convert [the syntax `=iterator`](#20907 (comment)) to a tag:
```php
final class IteratorTag implements TagInterface
{
    public function construct(mixed $value): mixed
    {
        return new IteratorArgument($value);
    }
}

$parser = new Parser(['iterator' => new IteratorTag()]);
```

If you think this is too complex, @nicolas-grekas [proposed an alternative](#21185 (comment)) to my proposal externalizing this support by introducing a new class `TaggedValue`.

Commits
-------

4744107 [Yaml] Add tags support
  • Loading branch information...
2 parents 10c3fc2 + 4744107 commit 5a388042bba6d69dd6b3337240d86b3358870b11 @nicolas-grekas nicolas-grekas committed Feb 10, 2017
@@ -12,7 +12,9 @@
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\ArgumentInterface;
use Symfony\Component\DependencyInjection\Argument\ClosureProxyArgument;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -251,10 +253,16 @@ private function dumpCallable($callable)
*/
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 ArgumentInterface) {
+ if ($value instanceof IteratorArgument) {
+ $tag = 'iterator';
+ } elseif ($value instanceof ClosureProxyArgument) {
+ $tag = 'closure_proxy';
+ } else {
+ throw new RuntimeException(sprintf('Unspecified Yaml tag for type "%s".', get_class($value)));
+ }
+
+ return new TaggedValue($tag, $this->dumpValue($value->getValues()));
}
if (is_array($value)) {
@@ -23,6 +23,7 @@
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;
@@ -516,7 +517,7 @@ protected function loadFile($file)
}
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);
}
@@ -567,42 +568,42 @@ private function validate($content, $file)
/**
* 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 (!is_array($value = $value['=iterator'])) {
- throw new InvalidArgumentException('Arguments typed "=iterator" must be arrays.');
- }
- $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 ($value instanceof TaggedValue) {
+ $argument = $value->getValue();
+ if ('iterator' === $value->getTag()) {
+ if (!is_array($argument)) {
+ throw new InvalidArgumentException('"!iterator" tag only accepts sequences.');
}
- 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].');
+
+ 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].');
}
- 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, '@')) {
@@ -14,6 +14,7 @@
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 @@ public function testDumpAutowireData()
$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);
}
}
@@ -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'
@@ -20,7 +20,7 @@
"psr/container": "^1.0"
},
"require-dev": {
- "symfony/yaml": "~3.2",
+ "symfony/yaml": "~3.3",
"symfony/config": "~3.3",
"symfony/expression-language": "~2.8|~3.0"
},
@@ -31,8 +31,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"
},
"provide": {
"psr/container-implementation": "1.0"
Oops, something went wrong.

0 comments on commit 5a38804

Please sign in to comment.