diff --git a/core/lib/Drupal/Component/Discovery/YamlDiscovery.php b/core/lib/Drupal/Component/Discovery/YamlDiscovery.php index 5ac0b7d..cadbc9c 100644 --- a/core/lib/Drupal/Component/Discovery/YamlDiscovery.php +++ b/core/lib/Drupal/Component/Discovery/YamlDiscovery.php @@ -7,7 +7,7 @@ namespace Drupal\Component\Discovery; -use Symfony\Component\Yaml\Parser; +use Drupal\Component\Yaml\Yaml; /** * Provides discovery for YAML files within a given set of directories. @@ -29,11 +29,11 @@ class YamlDiscovery implements DiscoverableInterface { protected $directories = array(); /** - * The symfony YAML parser. + * The YAML parser. * - * @var \Symfony\Component\Yaml\Parser + * @var \Drupal\Component\Yaml\Yaml */ - protected $parser; + protected $yaml; /** * Constructs a YamlDiscovery object. @@ -54,10 +54,10 @@ public function __construct($name, array $directories) { */ public function findAll() { $all = array(); - $parser = $this->parser(); + $yaml = $this->getYaml(); foreach ($this->findFiles() as $provider => $file) { - $all[$provider] = $parser->parse(file_get_contents($file)); + $all[$provider] = $yaml->parse(file_get_contents($file)); } return $all; @@ -69,11 +69,11 @@ public function findAll() { * @return \Symfony\Component\Yaml\Parser * The symfony YAML parser. */ - protected function parser() { - if (!isset($this->parser)) { - $this->parser = new Parser(); + protected function getYaml() { + if (!isset($this->yaml)) { + $this->yaml = new Yaml(); } - return $this->parser; + return $this->yaml; } /** diff --git a/core/lib/Drupal/Component/Yaml/Pecl.php b/core/lib/Drupal/Component/Yaml/Pecl.php new file mode 100644 index 0000000..635b277 --- /dev/null +++ b/core/lib/Drupal/Component/Yaml/Pecl.php @@ -0,0 +1,42 @@ +getParser()->parse($input); + } + + /** + * {@inheritdoc} + */ + public function dump($value) { + // The level where you switch to inline YAML is set to PHP_INT_MAX to ensure + // this does not occur. + return $this->getDumper()->dump($value, PHP_INT_MAX, 0, TRUE); + } + + /** + * Gets the YAML dumper instance. + * + * @return \Symfony\Component\Yaml\Dumper + */ + protected function getDumper() { + if (!isset($this->dumper)) { + $this->dumper = new Dumper(); + // Set Yaml\Dumper's default indentation for nested nodes/collections to + // 2 spaces for consistency with Drupal coding standards. + $this->dumper->setIndentation($this->indentation); + } + return $this->dumper; + } + + /** + * Gets the YAML parser instance. + * + * @return \Symfony\Component\Yaml\Parser + */ + protected function getParser() { + if (!isset($this->parser)) { + $this->parser = new Parser(); + } + return $this->parser; + } + +} diff --git a/core/lib/Drupal/Component/Yaml/Yaml.php b/core/lib/Drupal/Component/Yaml/Yaml.php new file mode 100644 index 0000000..a632416 --- /dev/null +++ b/core/lib/Drupal/Component/Yaml/Yaml.php @@ -0,0 +1,85 @@ +determineImplementation(); + $this->implementation = new $class(); + } + + /** + * {@inheritdoc} + */ + public function parse($input) { + return $this->implementation->parse($input); + } + + /** + * {@inheritdoc} + */ + public function dump($value) { + return $this->implementation->dump($value); + } + + /** + * Determines the optimal implementation to use for encoding and parsing Yaml. + * + * The selection is made based on the enabled PHP extensions, with the most + * performant available option chosen. + * + * @return string + * The class name for the optimal YAML implementation. + */ + protected function determineImplementation() { + if (isset(static::$implementationClass)) { + return static::$implementationClass; + } + + $settings = Settings::getSingleton(); + // If there's a settings.php override, use that. + if ($settings && ($class = $settings->get('yaml_parser_class'))) { + static::$implementationClass = $class; + } + // If the PECL YAML extension installed, use that. + elseif (function_exists('yaml_emit')) { + static::$implementationClass = 'Drupal\Component\Yaml\Pecl'; + } + else { + // Otherwise, fallback to the Symfony implementation. + static::$implementationClass = 'Drupal\Component\Yaml\Symfony'; + } + + return static::$implementationClass; + } +} diff --git a/core/lib/Drupal/Component/Yaml/YamlInterface.php b/core/lib/Drupal/Component/Yaml/YamlInterface.php new file mode 100644 index 0000000..ecf03a1 --- /dev/null +++ b/core/lib/Drupal/Component/Yaml/YamlInterface.php @@ -0,0 +1,39 @@ +exists($name)) { @@ -154,51 +144,34 @@ public function rename($name, $new_name) { } /** - * Gets the YAML dumper instance. + * Gets the YAML instance. * - * @return Symfony\Component\Yaml\Dumper - */ - protected function getDumper() { - if (!isset($this->dumper)) { - $this->dumper = new Dumper(); - // Set Yaml\Dumper's default indentation for nested nodes/collections to - // 2 spaces for consistency with Drupal coding standards. - $this->dumper->setIndentation(2); - } - return $this->dumper; - } - - /** - * Gets the YAML parser instance. - * - * @return Symfony\Component\Yaml\Parser + * @return \Drupal\Component\Yaml\YamlInterface + * The YAML parser. */ - protected function getParser() { - if (!isset($this->parser)) { - $this->parser = new Parser(); + protected function getYaml() { + if (!isset($this->yaml)) { + $this->yaml = new Yaml(); } - return $this->parser; + + return $this->yaml; } /** * Implements Drupal\Core\Config\StorageInterface::encode(). - * - * @throws Symfony\Component\Yaml\Exception\DumpException */ public function encode($data) { // The level where you switch to inline YAML is set to PHP_INT_MAX to ensure // this does not occur. Also set the exceptionOnInvalidType parameter to // TRUE, so exceptions are thrown for an invalid data type. - return $this->getDumper()->dump($data, PHP_INT_MAX, 0, TRUE); + return $this->getYaml()->dump($data, PHP_INT_MAX, 0, TRUE); } /** * Implements Drupal\Core\Config\StorageInterface::decode(). - * - * @throws Symfony\Component\Yaml\Exception\ParseException */ public function decode($raw) { - $data = $this->getParser()->parse($raw); + $data = $this->getYaml()->parse($raw); // A simple string is valid YAML for any reason. if (!is_array($data)) { return FALSE; diff --git a/core/lib/Drupal/Core/Extension/InfoParser.php b/core/lib/Drupal/Core/Extension/InfoParser.php index 5bb4841..c55d1df 100644 --- a/core/lib/Drupal/Core/Extension/InfoParser.php +++ b/core/lib/Drupal/Core/Extension/InfoParser.php @@ -8,9 +8,8 @@ namespace Drupal\Core\Extension; use Drupal\Component\Utility\String; +use Drupal\Component\Yaml\Yaml; use Symfony\Component\Yaml\Exception\ParseException; -use Symfony\Component\Yaml\Parser; - /** * Class that parses Drupal module's, theme's and profile's .info.yml files. @@ -25,11 +24,11 @@ class InfoParser implements InfoParserInterface { protected $parsedInfos = array(); /** - * Symfony YAML parser object. + * The YAML object. * - * @var \Symfony\Component\Yaml\Parser + * @var \Drupal\Component\Yaml\YamlInterface */ - protected $parser; + protected $yaml; /** * {@inheritdoc} @@ -41,7 +40,7 @@ public function parse($filename) { } else { try { - $this->parsedInfos[$filename] = $this->getParser()->parse(file_get_contents($filename)); + $this->parsedInfos[$filename] = $this->getYaml()->parse(file_get_contents($filename)); } catch (ParseException $e) { $message = String::format("Unable to parse !file. Parser error !error.", array('!file' => $filename, '!error' => $e->getMessage())); @@ -63,14 +62,15 @@ public function parse($filename) { /** * Returns a parser for parsing .info.yml files. * - * @return \Symfony\Component\Yaml\Parser - * Symfony YAML parser object. + * @return \Drupal\Component\Yaml\YamlInterface + * The YAML parser. */ - protected function getParser() { - if (!$this->parser) { - $this->parser = new Parser(); + protected function getYaml() { + if (!$this->yaml) { + $this->yaml = new Yaml(); } - return $this->parser; + + return $this->yaml; } /** diff --git a/core/tests/Drupal/Tests/Component/Yaml/YamlTest.php b/core/tests/Drupal/Tests/Component/Yaml/YamlTest.php new file mode 100644 index 0000000..b0af7f9 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Yaml/YamlTest.php @@ -0,0 +1,127 @@ + 'YAML handling', + 'description' => "Test the parsing and dumping of YAML is consistent between the PECL and Symfony implementations.", + 'group' => 'YAML', + ); + } + + protected function setUp() { + if (!extension_loaded('yaml')) { + $this->markTestSkipped( + 'The PECL Yaml extension is not available.' + ); + } + } + + /** + * Tests dumping with Symfony and parsing with PECL and vice versa. + * + * @dataProvider providerYamlData + */ + public function testAlternateDumperParser($data, $parser_class, $dumper_class) { + $dumper = new $dumper_class; + $parser = new $parser_class; + + $dumped = $dumper->dump($data); + + $parsed = $parser->parse($dumped); + + $this->assertEquals($data, $parsed); + } + + /** + * Tests parsing YAML with both Symfony and PECL. + * + * @dataProvider providerYamlNodeAnchors + */ + public function testAlternateParsers($data) { + $symfony = new Symfony(); + $pecl = new Pecl(); + $this->assertEquals($symfony->parse($data), $pecl->parse($data)); + } + + /** + * Data provider for YAML instance tests. + * + * @return array + */ + public function providerYamlData() { + $object = array( + 'foo' => 'bar', + 'id' => 'schnitzel', + 'ponies' => array('nope', 'thanks'), + 'how' => array( + 'about' => 'if', + 'i' => 'ask', + 'nicely' + ), + 'the' => array( + 'answer' => array( + 'still' => 'would', + 'be' => 'nope', + ), + ), + 'how_many_times' => 123, + 'should_i_ask' => FALSE, + 1, + FALSE, + array(1, FALSE), + array(10), + array(0 => '123456'), + ); + // Test parsing with Symfony and dumping with PECL. + $data[] = array( + $object, + 'Drupal\Component\Yaml\Symfony', + 'Drupal\Component\Yaml\Pecl' + ); + + // Test parsing with PECL and dumping with Symfony. + $data[] = array( + $object, + 'Drupal\Component\Yaml\Pecl', + 'Drupal\Component\Yaml\Symfony' + ); + + return $data; + } + + /** + * Data provider for YAML instance tests. + * + * @return array + */ + public function providerYamlNodeAnchors() { + $yaml = <<