Commit a0a530fd authored by catch's avatar catch

Issue #1920902 by neclimdul, larowlan, alexpott, chx, sun, jthorson,...

Issue #1920902 by neclimdul, larowlan, alexpott, chx, sun, jthorson, damiankloip, Berdir, Mixologic, YesCT, hussainweb, rcaracaus, cweagans, catch, jibran, msonnabaum, dawehner, webchick, anavarre, heyrocker: Add a Drupal Yaml wrapper so we can default to PECL Yaml component if it is available
parent 03fc2042
...@@ -61,7 +61,7 @@ public function findAll() { ...@@ -61,7 +61,7 @@ public function findAll() {
foreach ($provider_by_files as $file => $provider) { foreach ($provider_by_files as $file => $provider) {
// If a file is empty or its contents are commented out, return an empty // If a file is empty or its contents are commented out, return an empty
// array instead of NULL for type consistency. // array instead of NULL for type consistency.
$all[$provider] = Yaml::decode(file_get_contents($file)) ?: []; $all[$provider] = $this->decode($file);
$file_cache->set($file, $all[$provider]); $file_cache->set($file, $all[$provider]);
} }
} }
...@@ -69,6 +69,17 @@ public function findAll() { ...@@ -69,6 +69,17 @@ public function findAll() {
return $all; return $all;
} }
/**
* Decode a YAML file.
*
* @param string $file
* Yaml file path.
* @return array
*/
protected function decode($file) {
return Yaml::decode(file_get_contents($file)) ?: [];
}
/** /**
* Returns an array of file paths, keyed by provider. * Returns an array of file paths, keyed by provider.
* *
......
...@@ -2,42 +2,37 @@ ...@@ -2,42 +2,37 @@
namespace Drupal\Component\Serialization; namespace Drupal\Component\Serialization;
use Drupal\Component\Serialization\Exception\InvalidDataTypeException;
use Symfony\Component\Yaml\Parser;
use Symfony\Component\Yaml\Dumper;
/** /**
* Default serialization for YAML using the Symfony component. * Provides a YAML serialization implementation.
*
* Proxy implementation that will choose the best library based on availability.
*/ */
class Yaml implements SerializationInterface { class Yaml implements SerializationInterface {
/**
* The YAML implementation to use.
*
* @var \Drupal\Component\Serialization\SerializationInterface
*/
protected static $serializer;
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public static function encode($data) { public static function encode($data) {
try { // Instead of using \Drupal\Component\Serialization\Yaml::getSerializer(),
$yaml = new Dumper(); // always using Symfony for writing the data, to reduce the risk of having
$yaml->setIndentation(2); // differences if different environments (like production and development)
return $yaml->dump($data, PHP_INT_MAX, 0, TRUE, FALSE); // do not match in terms of what YAML implementation is available.
} return YamlSymfony::encode($data);
catch (\Exception $e) {
throw new InvalidDataTypeException($e->getMessage(), $e->getCode(), $e);
}
} }
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public static function decode($raw) { public static function decode($raw) {
try { $serializer = static::getSerializer();
$yaml = new Parser(); return $serializer::decode($raw);
// Make sure we have a single trailing newline. A very simple config like
// 'foo: bar' with no newline will fail to parse otherwise.
return $yaml->parse($raw, TRUE, FALSE);
}
catch (\Exception $e) {
throw new InvalidDataTypeException($e->getMessage(), $e->getCode(), $e);
}
} }
/** /**
...@@ -47,4 +42,23 @@ public static function getFileExtension() { ...@@ -47,4 +42,23 @@ public static function getFileExtension() {
return 'yml'; return 'yml';
} }
/**
* Determines which implementation to use for parsing YAML.
*/
protected static function getSerializer() {
if (!isset(static::$serializer)) {
// Use the PECL YAML extension if it is available. It has better
// performance for file reads and is YAML compliant.
if (extension_loaded('yaml')) {
static::$serializer = YamlPecl::class;
}
else {
// Otherwise, fallback to the Symfony implementation.
static::$serializer = YamlSymfony::class;
}
}
return static::$serializer;
}
} }
<?php
namespace Drupal\Component\Serialization;
use Drupal\Component\Serialization\Exception\InvalidDataTypeException;
/**
* Provides default serialization for YAML using the PECL extension.
*/
class YamlPecl implements SerializationInterface {
/**
* {@inheritdoc}
*/
public static function encode($data) {
static $init;
if (!isset($init)) {
ini_set('yaml.output_indent', 2);
// Do not break lines at 80 characters.
ini_set('yaml.output_width', -1);
$init = TRUE;
}
return yaml_emit($data, YAML_UTF8_ENCODING, YAML_LN_BREAK);
}
/**
* {@inheritdoc}
*/
public static function decode($raw) {
// yaml_parse() will error with an empty value.
if (!trim($raw)) {
return NULL;
}
// @todo Use ErrorExceptions when https://drupal.org/node/1247666 is in.
// yaml_parse() will throw errors instead of raising an exception. Until
// such time as Drupal supports native PHP ErrorExceptions as the error
// handler, we need to temporarily set the error handler as ::errorHandler()
// and then restore it after decoding has occurred. This allows us to turn
// parsing errors into a throwable exception.
// @see Drupal\Component\Serialization\Exception\InvalidDataTypeException
// @see http://php.net/manual/en/class.errorexception.php
set_error_handler([__CLASS__, 'errorHandler']);
$ndocs = 0;
$data = yaml_parse($raw, 0, $ndocs, [
YAML_BOOL_TAG => '\Drupal\Component\Serialization\YamlPecl::applyBooleanCallbacks',
]);
restore_error_handler();
return $data;
}
/**
* Handles errors for \Drupal\Component\Serialization\YamlPecl::decode().
*
* @param int $severity
* The severity level of the error.
* @param string $message
* The error message to display.
*
* @see \Drupal\Component\Serialization\YamlPecl::decode()
*/
public static function errorHandler($severity, $message) {
restore_error_handler();
throw new InvalidDataTypeException($message, $severity);
}
/**
* {@inheritdoc}
*/
public static function getFileExtension() {
return 'yml';
}
/**
* Applies callbacks after parsing to ignore 1.1 style booleans.
*
* @param mixed $value
* Value from YAML file.
* @param string $tag
* Tag that triggered the callback.
* @param int $flags
* Scalar entity style flags.
*
* @return string|bool
* FALSE, false, TRUE and true are returned as booleans, everything else is
* returned as a string.
*/
public static function applyBooleanCallbacks($value, $tag, $flags) {
// YAML 1.1 spec dictates that 'Y', 'N', 'y' and 'n' are booleans. But, we
// want the 1.2 behavior, so we only consider 'false', 'FALSE', 'true' and
// 'TRUE' as booleans.
if (!in_array(strtolower($value), ['false', 'true'], TRUE)) {
return $value;
}
$map = [
'false' => FALSE,
'true' => TRUE,
];
return $map[strtolower($value)];
}
}
<?php
namespace Drupal\Component\Serialization;
use Drupal\Component\Serialization\Exception\InvalidDataTypeException;
use Symfony\Component\Yaml\Parser;
use Symfony\Component\Yaml\Dumper;
/**
* Default serialization for YAML using the Symfony component.
*/
class YamlSymfony implements SerializationInterface {
/**
* {@inheritdoc}
*/
public static function encode($data) {
try {
$yaml = new Dumper();
$yaml->setIndentation(2);
return $yaml->dump($data, PHP_INT_MAX, 0, TRUE, FALSE);
}
catch (\Exception $e) {
throw new InvalidDataTypeException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* {@inheritdoc}
*/
public static function decode($raw) {
try {
$yaml = new Parser();
// Make sure we have a single trailing newline. A very simple config like
// 'foo: bar' with no newline will fail to parse otherwise.
return $yaml->parse($raw, TRUE, FALSE);
}
catch (\Exception $e) {
throw new InvalidDataTypeException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* {@inheritdoc}
*/
public static function getFileExtension() {
return 'yml';
}
}
...@@ -7,9 +7,9 @@ ...@@ -7,9 +7,9 @@
use Drupal\Core\Asset\Exception\InvalidLibraryFileException; use Drupal\Core\Asset\Exception\InvalidLibraryFileException;
use Drupal\Core\Asset\Exception\LibraryDefinitionMissingLicenseException; use Drupal\Core\Asset\Exception\LibraryDefinitionMissingLicenseException;
use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Serialization\Yaml;
use Drupal\Core\Theme\ThemeManagerInterface; use Drupal\Core\Theme\ThemeManagerInterface;
use Drupal\Component\Serialization\Exception\InvalidDataTypeException; use Drupal\Component\Serialization\Exception\InvalidDataTypeException;
use Drupal\Component\Serialization\Yaml;
use Drupal\Component\Utility\NestedArray; use Drupal\Component\Utility\NestedArray;
/** /**
......
...@@ -3,12 +3,12 @@ ...@@ -3,12 +3,12 @@
namespace Drupal\Core\Config; namespace Drupal\Core\Config;
use Drupal\Component\Diff\Diff; use Drupal\Component\Diff\Diff;
use Drupal\Component\Serialization\Yaml;
use Drupal\Core\Config\Entity\ConfigDependencyManager; use Drupal\Core\Config\Entity\ConfigDependencyManager;
use Drupal\Core\Config\Entity\ConfigEntityInterface; use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Config\Entity\ConfigEntityTypeInterface; use Drupal\Core\Config\Entity\ConfigEntityTypeInterface;
use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Serialization\Yaml;
use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface; use Drupal\Core\StringTranslation\TranslationInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface;
......
...@@ -3,8 +3,8 @@ ...@@ -3,8 +3,8 @@
namespace Drupal\Core\Config; namespace Drupal\Core\Config;
use Drupal\Component\FileCache\FileCacheFactory; use Drupal\Component\FileCache\FileCacheFactory;
use Drupal\Component\Serialization\Yaml;
use Drupal\Component\Serialization\Exception\InvalidDataTypeException; use Drupal\Component\Serialization\Exception\InvalidDataTypeException;
use Drupal\Core\Serialization\Yaml;
/** /**
* Defines the file storage. * Defines the file storage.
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
namespace Drupal\Core\DependencyInjection; namespace Drupal\Core\DependencyInjection;
use Drupal\Component\FileCache\FileCacheFactory; use Drupal\Component\FileCache\FileCacheFactory;
use Drupal\Component\Serialization\Yaml; use Drupal\Core\Serialization\Yaml;
use Symfony\Component\DependencyInjection\Alias; use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Definition;
......
<?php
namespace Drupal\Core\Discovery;
use Drupal\Component\Discovery\YamlDiscovery as ComponentYamlDiscovery;
use Drupal\Core\Serialization\Yaml;
/**
* Provides discovery for YAML files within a given set of directories.
*
* This overrides the Component file decoding with the Core YAML implementation.
*/
class YamlDiscovery extends ComponentYamlDiscovery {
/**
* {@inheritdoc}
*/
protected function decode($file) {
return Yaml::decode(file_get_contents($file)) ?: [];
}
}
...@@ -2,8 +2,8 @@ ...@@ -2,8 +2,8 @@
namespace Drupal\Core\Extension; namespace Drupal\Core\Extension;
use Drupal\Component\Serialization\Yaml;
use Drupal\Component\Serialization\Exception\InvalidDataTypeException; use Drupal\Component\Serialization\Exception\InvalidDataTypeException;
use Drupal\Core\Serialization\Yaml;
/** /**
* Parses dynamic .info.yml files that might change during the page request. * Parses dynamic .info.yml files that might change during the page request.
......
...@@ -2,12 +2,12 @@ ...@@ -2,12 +2,12 @@
namespace Drupal\Core\Extension; namespace Drupal\Core\Extension;
use Drupal\Component\Serialization\Yaml;
use Drupal\Core\Cache\Cache; use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\DrupalKernelInterface; use Drupal\Core\DrupalKernelInterface;
use Drupal\Core\Entity\EntityStorageException; use Drupal\Core\Entity\EntityStorageException;
use Drupal\Core\Entity\FieldableEntityInterface; use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Serialization\Yaml;
/** /**
* Default implementation of the module installer. * Default implementation of the module installer.
......
...@@ -3,8 +3,8 @@ ...@@ -3,8 +3,8 @@
namespace Drupal\Core\Plugin\Discovery; namespace Drupal\Core\Plugin\Discovery;
use Drupal\Component\Plugin\Discovery\DiscoveryInterface; use Drupal\Component\Plugin\Discovery\DiscoveryInterface;
use Drupal\Component\Discovery\YamlDiscovery as ComponentYamlDiscovery;
use Drupal\Component\Plugin\Discovery\DiscoveryTrait; use Drupal\Component\Plugin\Discovery\DiscoveryTrait;
use Drupal\Core\Discovery\YamlDiscovery as CoreYamlDiscovery;
use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\Core\StringTranslation\TranslatableMarkup;
/** /**
...@@ -25,7 +25,7 @@ class YamlDiscovery implements DiscoveryInterface { ...@@ -25,7 +25,7 @@ class YamlDiscovery implements DiscoveryInterface {
/** /**
* YAML file discovery and parsing handler. * YAML file discovery and parsing handler.
* *
* @var \Drupal\Component\Discovery\YamlDiscovery * @var \Drupal\Core\Discovery\YamlDiscovery
*/ */
protected $discovery; protected $discovery;
...@@ -48,7 +48,7 @@ class YamlDiscovery implements DiscoveryInterface { ...@@ -48,7 +48,7 @@ class YamlDiscovery implements DiscoveryInterface {
* An array of directories to scan. * An array of directories to scan.
*/ */
function __construct($name, array $directories) { function __construct($name, array $directories) {
$this->discovery = new ComponentYamlDiscovery($name, $directories); $this->discovery = new CoreYamlDiscovery($name, $directories);
} }
/** /**
......
...@@ -2,9 +2,9 @@ ...@@ -2,9 +2,9 @@
namespace Drupal\Core\Routing; namespace Drupal\Core\Routing;
use Drupal\Component\Discovery\YamlDiscovery;
use Drupal\Core\Access\CheckProviderInterface; use Drupal\Core\Access\CheckProviderInterface;
use Drupal\Core\Controller\ControllerResolverInterface; use Drupal\Core\Controller\ControllerResolverInterface;
use Drupal\Core\Discovery\YamlDiscovery;
use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Lock\LockBackendInterface; use Drupal\Core\Lock\LockBackendInterface;
use Drupal\Core\DestructableInterface; use Drupal\Core\DestructableInterface;
......
<?php
namespace Drupal\Core\Serialization;
use Drupal\Core\Site\Settings;
use Drupal\Component\Serialization\Yaml as ComponentYaml;
/**
* Provides a YAML serialization implementation.
*
* Allow settings to override the YAML implementation resolution.
*/
class Yaml extends ComponentYaml {
/**
* {@inheritdoc}
*/
protected static function getSerializer() {
// Allow settings.php to override the YAML serializer.
if (!isset(static::$serializer) &&
$class = Settings::get('yaml_parser_class')) {
static::$serializer = $class;
}
return parent::getSerializer();
}
}
...@@ -3,11 +3,11 @@ ...@@ -3,11 +3,11 @@
namespace Drupal\config\Controller; namespace Drupal\config\Controller;
use Drupal\Core\Archiver\ArchiveTar; use Drupal\Core\Archiver\ArchiveTar;
use Drupal\Component\Serialization\Yaml;
use Drupal\Core\Config\ConfigManagerInterface; use Drupal\Core\Config\ConfigManagerInterface;
use Drupal\Core\Config\StorageInterface; use Drupal\Core\Config\StorageInterface;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Diff\DiffFormatter; use Drupal\Core\Diff\DiffFormatter;
use Drupal\Core\Serialization\Yaml;
use Drupal\Core\Url; use Drupal\Core\Url;
use Drupal\system\FileDownloadController; use Drupal\system\FileDownloadController;
use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\ContainerInterface;
......
...@@ -2,13 +2,13 @@ ...@@ -2,13 +2,13 @@
namespace Drupal\config\Form; namespace Drupal\config\Form;
use Drupal\Component\Serialization\Yaml;
use Drupal\Core\Config\StorageInterface; use Drupal\Core\Config\StorageInterface;
use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Form\FormBase; use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormState; use Drupal\Core\Form\FormState;
use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Serialization\Yaml;
use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\Core\StringTranslation\TranslatableMarkup;
use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\ContainerInterface;
......
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
namespace Drupal\config\Form; namespace Drupal\config\Form;
use Drupal\Component\Serialization\Exception\InvalidDataTypeException; use Drupal\Component\Serialization\Exception\InvalidDataTypeException;
use Drupal\Component\Serialization\Yaml;
use Drupal\config\StorageReplaceDataWrapper; use Drupal\config\StorageReplaceDataWrapper;
use Drupal\Core\Config\ConfigImporter; use Drupal\Core\Config\ConfigImporter;
use Drupal\Core\Config\ConfigImporterException; use Drupal\Core\Config\ConfigImporterException;
...@@ -19,6 +18,7 @@ ...@@ -19,6 +18,7 @@
use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Lock\LockBackendInterface; use Drupal\Core\Lock\LockBackendInterface;
use Drupal\Core\Render\RendererInterface; use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Serialization\Yaml;
use Drupal\Core\Url; use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface;
......
...@@ -2,8 +2,8 @@ ...@@ -2,8 +2,8 @@
namespace Drupal\config\Tests; namespace Drupal\config\Tests;
use Drupal\Component\Serialization\Yaml;
use Drupal\Core\Archiver\Tar; use Drupal\Core\Archiver\Tar;
use Drupal\Core\Serialization\Yaml;
use Drupal\simpletest\WebTestBase; use Drupal\simpletest\WebTestBase;
/** /**
......
...@@ -2,8 +2,8 @@ ...@@ -2,8 +2,8 @@
namespace Drupal\config\Tests; namespace Drupal\config\Tests;
use Drupal\Component\Serialization\Yaml;
use Drupal\Core\Config\InstallStorage; use Drupal\Core\Config\InstallStorage;
use Drupal\Core\Serialization\Yaml;
use Drupal\simpletest\InstallerTestBase; use Drupal\simpletest\InstallerTestBase;
/** /**
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
namespace Drupal\config\Tests; namespace Drupal\config\Tests;
use Drupal\Component\Serialization\Yaml; use Drupal\Core\Serialization\Yaml;
use Drupal\simpletest\WebTestBase; use Drupal\simpletest\WebTestBase;
/** /**
...@@ -45,7 +45,8 @@ public function testImport() { ...@@ -45,7 +45,8 @@ public function testImport() {
]; ];
$this->drupalPostForm('admin/config/development/configuration/single/import', $edit, t('Import')); $this->drupalPostForm('admin/config/development/configuration/single/import', $edit, t('Import'));
$this->assertText('The import failed with the following message: Malformed inline YAML string ({{{) at line 1 (near &quot;{{{&quot;)'); // Assert the static portion of the error since different parsers could give different text in their error.
$this->assertText('The import failed with the following message: ');
$import = <<<EOD $import = <<<EOD
label: First label: First
...@@ -216,8 +217,8 @@ public function testExport() { ...@@ -216,8 +217,8 @@ public function testExport() {
$this->assertFieldByXPath('//select[@name="config_name"]//option[@selected="selected"]', t('Fallback date format (fallback)'), 'The fallback date format config entity is selected when specified in the URL.'); $this->assertFieldByXPath('//select[@name="config_name"]//option[@selected="selected"]', t('Fallback date format (fallback)'), 'The fallback date format config entity is selected when specified in the URL.');
$fallback_date = \Drupal::entityManager()->getStorage('date_format')->load('fallback'); $fallback_date = \Drupal::entityManager()->getStorage('date_format')->load('fallback');
$data = Yaml::encode($fallback_date->toArray()); $yaml_text = (string) $this->xpath('//textarea[@name="export"]')[0];
$this->assertFieldByXPath('//textarea[@name="export"]', $data, 'The fallback date format config entity export code is displayed.'); $this->assertEqual(Yaml::decode<