diff --git a/core/lib/Drupal/Component/Serialization/TaggedSerializationInterface.php b/core/lib/Drupal/Component/Serialization/TaggedSerializationInterface.php index 361ea65bf5..65903e5b33 100644 --- a/core/lib/Drupal/Component/Serialization/TaggedSerializationInterface.php +++ b/core/lib/Drupal/Component/Serialization/TaggedSerializationInterface.php @@ -7,6 +7,25 @@ */ interface TaggedSerializationInterface extends SerializationInterface { + /** + * Adds a tag callback. + * + * @param string $tag + * The tag name. + * @param callable $callback + * The callback to perform on a following value when the tag is encountered. + */ + public static function addTagCallback($tag, callable $callback); + + /** + * Provides the default tag callbacks of the serializer. + * + * @return array + * An associative array where the key is the tag and the value is the + * callback. + */ + public static function getDefaultTagCallbacks(); + /** * Retrieves a map of tag callbacks. * @@ -16,13 +35,24 @@ interface TaggedSerializationInterface extends SerializationInterface { */ public static function getTagCallbacks(); + /** + * Removes a tag callback. + * + * @param string $tag + * The tag name. + * + * @return callable|null + * The callback that was assigned to $tag or NULL if $tag didn't exist. + */ + public static function removeTagCallback($tag); + /** * Sets a map of tag callbacks. * * @param array $callbacks - * An associative array where the key is the tag and the value is the - * callback. + * Optional. An associative array where the key is the tag and the value is + * the callback. If not provided, the callbacks will be reset. */ - public static function setTagCallbacks(array $callbacks); + public static function setTagCallbacks(array $callbacks = NULL); } diff --git a/core/lib/Drupal/Component/Serialization/TaggedSerializationTrait.php b/core/lib/Drupal/Component/Serialization/TaggedSerializationTrait.php index 8253479841..fb653b647e 100644 --- a/core/lib/Drupal/Component/Serialization/TaggedSerializationTrait.php +++ b/core/lib/Drupal/Component/Serialization/TaggedSerializationTrait.php @@ -12,7 +12,21 @@ trait TaggedSerializationTrait { * * @var array */ - protected static $tagCallbacks = []; + protected static $tagCallbacks; + + /** + * Adds a tag callback. + * + * @param string $tag + * The tag name. + * @param callable $callback + * The callback to perform on a following value when the tag is encountered. + */ + public static function addTagCallback($tag, callable $callback) { + $callbacks = static::getTagCallbacks(); + $callbacks[$tag] = $callback; + static::setTagCallbacks($callbacks); + } /** * Executes a tag callback. @@ -28,12 +42,28 @@ trait TaggedSerializationTrait { public static function executeTagCallback($value, $tag) { $callbacks = static::getTagCallbacks(); + // Prepend tag with ! (in cases where it's stripped from the name). + if ($tag[0] !== '!') { + $tag = "!$tag"; + } + // Immediately return the original value if there is no callback. if (!isset($callbacks[$tag])) { return $value; } - return $callbacks[$tag](...(array) $value); + return $callbacks[$tag]($value, $tag); + } + + /** + * Provides the default tag callbacks. + * + * @return array + * An associative array where the key is the tag and the value is the + * callback. + */ + public static function getDefaultTagCallbacks() { + return []; } /** @@ -44,6 +74,9 @@ public static function executeTagCallback($value, $tag) { * callback. */ public static function getTagCallbacks() { + if (!isset(static::$tagCallbacks)) { + static::$tagCallbacks = static::getDefaultTagCallbacks(); + } return static::$tagCallbacks; } @@ -62,6 +95,24 @@ protected static function mergeTagCallbacks($serializer) { } } + /** + * Removes a tag callback. + * + * @param string $tag + * The tag name. + * + * @return callable|null + * The callback that was assigned to $tag or NULL if $tag didn't exist. + */ + public static function removeTagCallback($tag) { + $callback = NULL; + if (isset(static::$tagCallbacks[$tag])) { + $callback = static::$tagCallbacks[$tag]; + unset(static::$tagCallbacks[$tag]); + } + return $callback; + } + /** * Sets a map of tag callbacks. * @@ -69,7 +120,7 @@ protected static function mergeTagCallbacks($serializer) { * An associative array where the key is the tag and the value is the * callback. */ - public static function setTagCallbacks(array $callbacks) { + public static function setTagCallbacks(array $callbacks = NULL) { static::$tagCallbacks = $callbacks; } diff --git a/core/lib/Drupal/Component/Serialization/YamlPecl.php b/core/lib/Drupal/Component/Serialization/YamlPecl.php index 651355b49b..260594db8b 100644 --- a/core/lib/Drupal/Component/Serialization/YamlPecl.php +++ b/core/lib/Drupal/Component/Serialization/YamlPecl.php @@ -57,6 +57,15 @@ public static function decode($raw) { return $data; } + /** + * {@inheritdoc} + */ + public static function getDefaultTagCallbacks() { + return [ + YAML_BOOL_TAG => 'Drupal\Component\Serialization\YamlPecl::applyBooleanCallbacks', + ]; + } + /** * Handles errors for \Drupal\Component\Serialization\YamlPecl::decode(). * @@ -79,15 +88,6 @@ public static function getFileExtension() { return 'yml'; } - /** - * {@inheritdoc} - */ - public static function getTagCallbacks() { - return [ - YAML_BOOL_TAG => static::class . '::applyBooleanCallbacks', - ]; - } - /** * Applies callbacks after parsing to ignore 1.1 style booleans. * diff --git a/core/lib/Drupal/Component/Serialization/YamlSymfony.php b/core/lib/Drupal/Component/Serialization/YamlSymfony.php index ae6fce4f8b..33a9aa7f57 100644 --- a/core/lib/Drupal/Component/Serialization/YamlSymfony.php +++ b/core/lib/Drupal/Component/Serialization/YamlSymfony.php @@ -47,7 +47,7 @@ public static function decode($raw) { // Support Symfony 3.3 TaggedValue objects. array_walk_recursive($data, function (&$item) { if ($item instanceof TaggedValue) { - $item = static::executeTagCallback($item->getValue(), '!' . $item->getTag()); + $item = static::executeTagCallback($item->getValue(), $item->getTag()); } }); diff --git a/core/lib/Drupal/Core/Serialization/Yaml.php b/core/lib/Drupal/Core/Serialization/Yaml.php index 392b6fe86b..77440e7c83 100644 --- a/core/lib/Drupal/Core/Serialization/Yaml.php +++ b/core/lib/Drupal/Core/Serialization/Yaml.php @@ -2,7 +2,6 @@ namespace Drupal\Core\Serialization; -use Drupal\Component\Serialization\TaggedSerializationInterface; use Drupal\Core\Site\Settings; use Drupal\Component\Serialization\Yaml as ComponentYaml; @@ -30,10 +29,8 @@ protected static function getSerializer() { static::$serializer = $class; - // Merge the tag callbacks from this proxy to the chosen serializer. - if (static::$serializer instanceof TaggedSerializationInterface) { - static::mergeTagCallbacks(static::$serializer); - } + // Merge any tag callbacks from this proxy to the chosen serializer. + static::mergeTagCallbacks(static::$serializer); } return parent::getSerializer(); } @@ -54,7 +51,7 @@ protected static function getTranslation() { /** * {@inheritdoc} */ - public static function getTagCallbacks() { + public static function getDefaultTagCallbacks() { return [ '!translate' => static::class . '::applyTranslateCallback', ]; @@ -63,14 +60,16 @@ public static function getTagCallbacks() { /** * Callback for applying the !translate tag. * - * @param mixed ... - * The arguments to pass to the Translation manager. + * @param string|string[] $value + * The tag value. + * @param string $tag + * The tag name. * * @return \Drupal\Core\StringTranslation\TranslatableMarkup * A new TranslatableMarkup object. */ - public static function applyTranslateCallback(...$args) { - return static::getTranslation()->translate(...$args); + public static function applyTranslateCallback($value, $tag) { + return static::getTranslation()->translate(...(array) $value); } } diff --git a/core/tests/Drupal/KernelTests/Core/Serialization/YamlTest.php b/core/tests/Drupal/KernelTests/Core/Serialization/YamlTest.php new file mode 100644 index 0000000000..d2a62f2798 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Serialization/YamlTest.php @@ -0,0 +1,40 @@ +assertInstanceOf(TranslatableMarkup::class, $label); + + // Ensure that the argument was set properly. + $this->assertEquals(['@arg' => 'value'], $label->getArguments()); + + // Ensure that the context option was set properly. + $this->assertEquals('Something', $label->getOption('context')); + } + +} diff --git a/core/tests/Drupal/Tests/Component/Serialization/YamlPeclTest.php b/core/tests/Drupal/Tests/Component/Serialization/YamlPeclTest.php index 910a6356b8..cdd978f077 100644 --- a/core/tests/Drupal/Tests/Component/Serialization/YamlPeclTest.php +++ b/core/tests/Drupal/Tests/Component/Serialization/YamlPeclTest.php @@ -96,4 +96,17 @@ public function testError() { YamlPecl::decode('foo: [ads'); } + /** + * Tests YAML 1.2 tag support (!tag). + * + * @covers ::addTagCallback + * @covers ::getDefaultTagCallbacks + * @covers ::getTagCallbacks + * @covers ::removeTagCallback + * @covers ::setTagCallbacks + */ + public function testTagSupport() { + $this->assertYamlTags(YamlPecl::class); + } + } diff --git a/core/tests/Drupal/Tests/Component/Serialization/YamlSymfonyTest.php b/core/tests/Drupal/Tests/Component/Serialization/YamlSymfonyTest.php index eeedf538d6..9b99d84599 100644 --- a/core/tests/Drupal/Tests/Component/Serialization/YamlSymfonyTest.php +++ b/core/tests/Drupal/Tests/Component/Serialization/YamlSymfonyTest.php @@ -87,4 +87,17 @@ public function testObjectSupportDisabled() { YamlSymfony::encode([$object]); } + /** + * Tests YAML 1.2 tag support (!tag). + * + * @covers ::addTagCallback + * @covers ::getDefaultTagCallbacks + * @covers ::getTagCallbacks + * @covers ::removeTagCallback + * @covers ::setTagCallbacks + */ + public function testTagSupport() { + $this->assertYamlTags(YamlSymfony::class); + } + } diff --git a/core/tests/Drupal/Tests/Component/Serialization/YamlTestBase.php b/core/tests/Drupal/Tests/Component/Serialization/YamlTestBase.php index 82226d0d34..1c9e25848e 100644 --- a/core/tests/Drupal/Tests/Component/Serialization/YamlTestBase.php +++ b/core/tests/Drupal/Tests/Component/Serialization/YamlTestBase.php @@ -2,6 +2,7 @@ namespace Drupal\Tests\Component\Serialization; +use Drupal\Component\Serialization\TaggedSerializationInterface; use PHPUnit\Framework\TestCase; /** @@ -9,6 +10,55 @@ */ abstract class YamlTestBase extends TestCase { + /** + * Asserts that a serializer can support YAML 1.2 tags. + * + * @param string $serializer + * The class name of the serializer to test. + */ + protected function assertYamlTags($serializer) { + /* @var \Drupal\Component\Serialization\TaggedSerializationInterface $serializer */ + + // Ensure the serializer supports the tagged interface. + $this->assertTrue(is_subclass_of($serializer, TaggedSerializationInterface::class)); + + // Ensure the component's default callbacks. + $this->assertEquals($serializer::getDefaultTagCallbacks(), $serializer::getTagCallbacks()); + + $yaml = 'value: !sum [1, 2, 3]'; + + // Ensure that without a callback, the tag is ignored. + $data = $serializer::decode($yaml); + $this->assertEquals($data['value'], [1, 2, 3]); + + // Now, add the custom tag callback. + $sum = function ($value, $tag) { + return array_sum($value); + }; + $serializer::addTagCallback('!sum', $sum); + + // Ensure that with a tag callback, the value is converted. + $data = $serializer::decode($yaml); + $this->assertEquals($data['value'], 6); + + // Remove the custom tag callback and ensure it returns original callback. + $callback = $serializer::removeTagCallback('!sum'); + $this->assertEquals($sum, $callback); + + // Ensure that without a callback, the tag is ignored. + $data = $serializer::decode($yaml); + $this->assertEquals($data['value'], [1, 2, 3]); + + // Reset tag callbacks. + $serializer::setTagCallbacks(); + $callbacks = new \ReflectionProperty($serializer, 'tagCallbacks'); + $callbacks->setAccessible(TRUE); + $this->assertEquals(NULL, $callbacks->getValue($serializer)); + + // Ensure the component's default callbacks are restored. + $this->assertEquals($serializer::getDefaultTagCallbacks(), $serializer::getTagCallbacks()); + } + /** * Some data that should be able to be serialized. */