diff --git a/core/includes/common.inc b/core/includes/common.inc index ab11ddf..11498af 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -13,8 +13,8 @@ use Drupal\Core\Language\Language; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\Yaml\Parser; -use Symfony\Component\Yaml\Exception\ParseException; +use Drupal\Core\Serialization\Yaml; +use Drupal\Core\Serialization\Exception\InvalidDataTypeException; use Drupal\Component\PhpStorage\PhpStorageFactory; use Drupal\Component\Utility\NestedArray; use Drupal\Core\Datetime\DrupalDateTime; @@ -2767,12 +2767,11 @@ function drupal_get_library($extension, $name = NULL) { if ($library_file && file_exists(DRUPAL_ROOT . '/' . $library_file)) { $libraries[$extension] = array(); - $parser = new Parser(); try { - $libraries[$extension] = $parser->parse(file_get_contents(DRUPAL_ROOT . '/' . $library_file)); + $libraries[$extension] = Yaml::decode(file_get_contents(DRUPAL_ROOT . '/' . $library_file)); } - catch (ParseException $e) { - // Rethrow a more helpful exception, since ParseException lacks context. + catch (InvalidDataTypeException $e) { + // Rethrow a more helpful exception to provide context. throw new \RuntimeException(sprintf('Invalid library definition in %s: %s', $library_file, $e->getMessage()), 0, $e); } // Allow modules to alter the module's registered libraries. diff --git a/core/lib/Drupal/Component/Discovery/YamlDiscovery.php b/core/lib/Drupal/Component/Discovery/YamlDiscovery.php index 5ac0b7d..8a80827 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\Core\Serialization\Yaml; /** * Provides discovery for YAML files within a given set of directories. @@ -29,13 +29,6 @@ class YamlDiscovery implements DiscoverableInterface { protected $directories = array(); /** - * The symfony YAML parser. - * - * @var \Symfony\Component\Yaml\Parser - */ - protected $parser; - - /** * Constructs a YamlDiscovery object. * * @param string $name @@ -54,29 +47,14 @@ public function __construct($name, array $directories) { */ public function findAll() { $all = array(); - $parser = $this->parser(); - foreach ($this->findFiles() as $provider => $file) { - $all[$provider] = $parser->parse(file_get_contents($file)); + $all[$provider] = Yaml::decode(file_get_contents($file)); } return $all; } /** - * Returns the YAML parser. - * - * @return \Symfony\Component\Yaml\Parser - * The symfony YAML parser. - */ - protected function parser() { - if (!isset($this->parser)) { - $this->parser = new Parser(); - } - return $this->parser; - } - - /** * Returns an array of file paths, keyed by provider. * * @return array diff --git a/core/lib/Drupal/Core/Config/FileStorage.php b/core/lib/Drupal/Core/Config/FileStorage.php index 2df9fbd..69559bb 100644 --- a/core/lib/Drupal/Core/Config/FileStorage.php +++ b/core/lib/Drupal/Core/Config/FileStorage.php @@ -8,9 +8,8 @@ namespace Drupal\Core\Config; use Drupal\Component\Utility\String; -use Symfony\Component\Yaml\Dumper; -use Symfony\Component\Yaml\Exception\DumpException; -use Symfony\Component\Yaml\Parser; +use Drupal\Core\Serialization\Yaml; +use Drupal\Core\Serialization\Exception\InvalidDataTypeException; /** * Defines the file storage controller. @@ -25,20 +24,6 @@ class FileStorage implements StorageInterface { protected $directory = ''; /** - * A shared YAML dumper instance. - * - * @var Symfony\Component\Yaml\Dumper - */ - protected $dumper; - - /** - * A shared YAML parser instance. - * - * @var Symfony\Component\Yaml\Parser - */ - protected $parser; - - /** * Constructs a new FileStorage controller. * * @param string $directory @@ -78,16 +63,22 @@ public function exists($name) { /** * Implements Drupal\Core\Config\StorageInterface::read(). * - * @throws Symfony\Component\Yaml\Exception\ParseException + * @throws \Drupal\Core\Config\UnsupportedDataTypeConfigException */ public function read($name) { if (!$this->exists($name)) { return FALSE; } $data = file_get_contents($this->getFilePath($name)); - // @todo Yaml throws a ParseException on invalid data. Is it expected to be - // caught or not? - $data = $this->decode($data); + try { + $data = $this->decode($data); + } + catch (InvalidDataTypeException $e) { + throw new UnsupportedDataTypeConfigException(String::format('Invalid data type in config @name: !message', array( + '@name' => $name, + '!message' => $e->getMessage(), + ))); + } return $data; } @@ -114,8 +105,11 @@ public function write($name, array $data) { try { $data = $this->encode($data); } - catch(DumpException $e) { - throw new UnsupportedDataTypeConfigException(String::format('Invalid data type for used in config: @name', array('@name' => $name))); + catch (InvalidDataTypeException $e) { + throw new UnsupportedDataTypeConfigException(String::format('Invalid data type in config @name: !message', array( + '@name' => $name, + '!message' => $e->getMessage(), + ))); } $target = $this->getFilePath($name); @@ -154,51 +148,17 @@ public function rename($name, $new_name) { } /** - * 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(2); - } - 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; - } - - /** * 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 Yaml::encode($data); } /** * Implements Drupal\Core\Config\StorageInterface::decode(). - * - * @throws Symfony\Component\Yaml\Exception\ParseException */ public function decode($raw) { - $data = $this->getParser()->parse($raw); + $data = Yaml::decode($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 4a23ad8..f158bfb 100644 --- a/core/lib/Drupal/Core/Extension/InfoParser.php +++ b/core/lib/Drupal/Core/Extension/InfoParser.php @@ -8,8 +8,8 @@ namespace Drupal\Core\Extension; use Drupal\Component\Utility\String; -use Symfony\Component\Yaml\Exception\ParseException; -use Symfony\Component\Yaml\Parser; +use Drupal\Core\Serialization\Yaml; +use Drupal\Core\Serialization\Exception\InvalidDataTypeException; /** * Parses extension .info.yml files. @@ -24,13 +24,6 @@ class InfoParser implements InfoParserInterface { protected static $parsedInfos = array(); /** - * Symfony YAML parser object. - * - * @var \Symfony\Component\Yaml\Parser - */ - protected $parser; - - /** * {@inheritdoc} */ public function parse($filename) { @@ -40,16 +33,16 @@ public function parse($filename) { } else { try { - static::$parsedInfos[$filename] = $this->getParser()->parse(file_get_contents($filename)); + static::$parsedInfos[$filename] = Yaml::decode(file_get_contents($filename)); } - catch (ParseException $e) { - $message = String::format("Unable to parse !file. Parser error !error.", array('!file' => $filename, '!error' => $e->getMessage())); - throw new InfoParserException($message, $filename); + catch (InvalidDataTypeException $e) { + $message = String::format("Unable to parse !file: !error", array('!file' => $filename, '!error' => $e->getMessage())); + throw new InfoParserException($message); } $missing_keys = array_diff($this->getRequiredKeys(), array_keys(static::$parsedInfos[$filename])); if (!empty($missing_keys)) { $message = format_plural(count($missing_keys), 'Missing required key (!missing_keys) in !file.', 'Missing required keys (!missing_keys) in !file.', array('!missing_keys' => implode(', ', $missing_keys), '!file' => $filename)); - throw new InfoParserException($message, $filename); + throw new InfoParserException($message); } if (isset(static::$parsedInfos[$filename]['version']) && static::$parsedInfos[$filename]['version'] === 'VERSION') { static::$parsedInfos[$filename]['version'] = \Drupal::VERSION; @@ -60,19 +53,6 @@ public function parse($filename) { } /** - * Returns a parser for parsing .info.yml files. - * - * @return \Symfony\Component\Yaml\Parser - * Symfony YAML parser object. - */ - protected function getParser() { - if (!$this->parser) { - $this->parser = new Parser(); - } - return $this->parser; - } - - /** * Returns an array of keys required to exist in .info.yml file. * * @return array diff --git a/core/lib/Drupal/Core/Extension/InfoParserException.php b/core/lib/Drupal/Core/Extension/InfoParserException.php index 2804bf6..df8071f 100644 --- a/core/lib/Drupal/Core/Extension/InfoParserException.php +++ b/core/lib/Drupal/Core/Extension/InfoParserException.php @@ -10,35 +10,4 @@ * An exception thrown by the InfoParser class whilst parsing info.yml files. */ class InfoParserException extends \RuntimeException { - - /** - * The info.yml filename. - * - * @var string - */ - protected $infoFilename; - - /** - * Constructs the InfoParserException object. - * - * @param string $message - * The Exception message to throw. - * @param string $filename - * The info.yml filename. - */ - public function __construct($message, $info_filename) { - $this->infoFilename = $info_filename; - parent::__construct($message); - } - - /** - * Gets the info.yml filename. - * - * @return string - * The info.yml filename. - */ - public function getInfoFilename () { - return $this->infoFilename; - } - } diff --git a/core/lib/Drupal/Core/Serialization/Exception/InvalidDataTypeException.php b/core/lib/Drupal/Core/Serialization/Exception/InvalidDataTypeException.php new file mode 100644 index 0000000..e6b8ce6 --- /dev/null +++ b/core/lib/Drupal/Core/Serialization/Exception/InvalidDataTypeException.php @@ -0,0 +1,14 @@ +get('yaml_parser_class'))) { + static::$instance = new $class(); + } + // If the PECL YAML extension installed, use that. + elseif (extension_loaded('yaml')) { + static::$instance = new YamlPecl(); + } + else { + // Otherwise, fallback to the Symfony implementation. + static::$instance = new YamlSymfony(); + } + } + + /** + * {@inheritdoc} + */ + public static function encode($data) { + if (!isset(static::$instance)) { + new static(); + } + return static::$instance->encode($data); + } + + /** + * {@inheritdoc} + */ + public static function decode($raw) { + if (!isset(static::$instance)) { + new static(); + } + return static::$instance->decode($raw); + } + + /** + * {@inheritdoc} + */ + public static function getFileExtension() { + return 'yml'; + } + +} diff --git a/core/lib/Drupal/Core/Serialization/YamlPecl.php b/core/lib/Drupal/Core/Serialization/YamlPecl.php new file mode 100644 index 0000000..f26ce11 --- /dev/null +++ b/core/lib/Drupal/Core/Serialization/YamlPecl.php @@ -0,0 +1,60 @@ +getMessage(), $e->getCode(), $e); + } + } + + /** + * {@inheritdoc} + */ + public static function decode($raw) { + try { + return Symfony::parse($raw, TRUE); + } + catch (\Exception $e) { + throw new InvalidDataTypeException($e->getMessage(), $e->getCode(), $e); + } + } + + /** + * {@inheritdoc} + */ + public static function getFileExtension() { + return 'yml'; + } + +} diff --git a/core/modules/config/lib/Drupal/config/Tests/Storage/ConfigStorageTestBase.php b/core/modules/config/lib/Drupal/config/Tests/Storage/ConfigStorageTestBase.php index d986835..0ed768f 100644 --- a/core/modules/config/lib/Drupal/config/Tests/Storage/ConfigStorageTestBase.php +++ b/core/modules/config/lib/Drupal/config/Tests/Storage/ConfigStorageTestBase.php @@ -27,7 +27,6 @@ * Tests storage controller CRUD operations. * * @todo Coverage: Trigger PDOExceptions / Database exceptions. - * @todo Coverage: Trigger Yaml's ParseException and DumpException. */ function testCRUD() { $name = 'config_test.storage'; diff --git a/core/modules/system/lib/Drupal/system/Tests/Extension/InfoParserUnitTest.php b/core/modules/system/lib/Drupal/system/Tests/Extension/InfoParserUnitTest.php index 8be4af9..f59dad6 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Extension/InfoParserUnitTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Extension/InfoParserUnitTest.php @@ -90,7 +90,7 @@ public function testInfoParser() { $info_values = $this->infoParser->parse('core/modules/system/tests/fixtures/common_test.info.txt'); $this->assertEqual($info_values['simple_string'], 'A simple string', 'Simple string value was parsed correctly.', 'System'); $this->assertEqual($info_values['version'], \Drupal::VERSION, 'Constant value was parsed correctly.', 'System'); - $this->assertEqual($info_values['double_colon'], 'dummyClassName::', 'Value containing double-colon was parsed correctly.', 'System'); + $this->assertEqual($info_values['double_colon'], 'dummyClassName::foo', 'Value containing double-colon was parsed correctly.', 'System'); } } diff --git a/core/modules/system/tests/fixtures/common_test.info.txt b/core/modules/system/tests/fixtures/common_test.info.txt index 7e57dfe..ff535b1 100644 --- a/core/modules/system/tests/fixtures/common_test.info.txt +++ b/core/modules/system/tests/fixtures/common_test.info.txt @@ -4,4 +4,4 @@ type: module description: 'testing info file parsing' simple_string: 'A simple string' version: "VERSION" -double_colon: dummyClassName:: +double_colon: dummyClassName::foo diff --git a/core/tests/Drupal/Tests/Core/Serialization/YamlTest.php b/core/tests/Drupal/Tests/Core/Serialization/YamlTest.php new file mode 100644 index 0000000..2b155cb --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Serialization/YamlTest.php @@ -0,0 +1,160 @@ + 'YAML', + 'description' => "Tests encoding and decoding of YAML is consistent between PECL and Symfony implementations.", + 'group' => 'Serialization API', + ); + } + + protected function setUp() { + if (!extension_loaded('yaml')) { + $this->markTestSkipped('The PECL Yaml extension is not available.'); + } + } + + /** + * Tests encoding with Symfony and decoding with PECL and vice versa. + * + * @dataProvider providerYamlData + * @covers \Drupal\Core\Serialization\Yaml::encode + * @covers \Drupal\Core\Serialization\Yaml::decode + * @covers \Drupal\Core\Serialization\YamlPecl::encode + * @covers \Drupal\Core\Serialization\YamlPecl::decode + * @covers \Drupal\Core\Serialization\YamlSymfony::encode + * @covers \Drupal\Core\Serialization\YamlSymfony::decode + */ + public function testEncodeDecode($data, $parser_class, $dumper_class) { + $dumper = new $dumper_class; + $parser = new $parser_class; + + $dumped = $dumper->encode($data); + + $parsed = $parser->decode($dumped); + + $this->assertEquals($data, $parsed); + } + + /** + * Tests decoding YAML node anchors with both Symfony and PECL. + * + * @dataProvider providerYamlNodeAnchors + */ + public function testDecodeNodeAnchors($data) { + $symfony = new YamlSymfony(); + $pecl = new YamlPecl(); + $this->assertEquals($symfony->decode($data), $pecl->decode($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\Core\Serialization\YamlSymfony', + 'Drupal\Core\Serialization\YamlPecl' + ); + // Test parsing with PECL and dumping with Symfony. + $data[] = array( + $object, + 'Drupal\Core\Serialization\YamlPecl', + 'Drupal\Core\Serialization\YamlSymfony' + ); + return $data; + } + + /** + * Data provider for YAML instance tests. + * + * @return array + */ + public function providerYamlNodeAnchors() { + $yaml = <<assertEquals($symfony->decode($data), $pecl->decode($data)); + } + + /** + * Data provider for YAML files in core test. + * @return array + */ + public function providerYamlFilesInCore() { + $files = array(); + $dirs = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator(__DIR__ . '/../../../../../', \RecursiveDirectoryIterator::FOLLOW_SYMLINKS)); + foreach ($dirs as $dir) { + $pathname = $dir->getPathname(); + // Exclude vendor. + if ($dir->getExtension() == 'yml' && strpos($pathname, '/../../../../../vendor') === FALSE) { + $files[] = array($dir->getRealPath()); + } + } + return $files; + } + +}