Skip to content
Snippets Groups Projects
Commit 94f5d3d1 authored by Pierre Dureau's avatar Pierre Dureau Committed by Mikael Meulle
Browse files

Issue #3474822 by pdureau, sea2709, smustgrave, just_like_good_vibes: Normalize attributes values

parent 69d0e750
No related branches found
No related tags found
1 merge request!250Issue #3474822 by pdureau, sea2709, smustgrave, just_like_good_vibes: Normalize attributes values
Pipeline #324477 passed
......@@ -35,9 +35,12 @@ class ComponentElementAlter implements TrustedCallbackInterface {
public function alter(array $element): array {
$element = $this->normalizeSlots($element);
$component = $this->componentPluginManager->find($element['#component']);
$element = $this->processAttributesProp($element, $component);
$element = $this->processAttributesRenderProperty($element);
$element = $this->normalizeProps($element, $component);
// Attributes prop must never be empty, to avoid the processing of SDC's
// ComponentsTwigExtension::mergeAdditionalRenderContext() which is adding
// an Attribute PHP object before running the validator.
$element["#props"]["attributes"]['data-component-id'] = $component->getPluginId();
$element = $this->processAttributesRenderProperty($element);
return $element;
}
......@@ -87,24 +90,6 @@ class ComponentElementAlter implements TrustedCallbackInterface {
return $element;
}
/**
* Process attributes prop.
*/
public function processAttributesProp(array $element, Component $component): array {
$element["#props"]["attributes"] = $element["#props"]["attributes"] ?? [];
// Attribute PHP objects are rendered as strings by SDC ComponentValidator,
// this is raising an error: "InvalidComponentException: String value
// found, but an object is required".
if (is_a($element["#props"]["attributes"], '\Drupal\Core\Template\Attribute')) {
$element["#props"]["attributes"] = $element["#props"]["attributes"]->toArray();
}
// Attributes prop must never be empty, to avoid the processing of SDC's
// ComponentsTwigExtension::mergeAdditionalRenderContext() which is adding
// an Attribute PHP object.
$element["#props"]["attributes"]['data-component-id'] = $component->getPluginId();
return $element;
}
/**
* Process #attributes render property.
*
......
......@@ -4,8 +4,12 @@ declare(strict_types=1);
namespace Drupal\ui_patterns\Plugin\UiPatterns\PropType;
use Drupal\Component\Render\MarkupInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\RenderableInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Template\Attribute;
use Drupal\Core\Url;
use Drupal\ui_patterns\Attribute\PropType;
use Drupal\ui_patterns\PropTypePluginBase;
......@@ -49,12 +53,119 @@ class AttributesPropType extends PropTypePluginBase {
plugins are expected to return a mapping to not break SDC prop validation
against the prop type schema.
*/
if (is_a($value, '\Drupal\Core\Template\Attribute')) {
return $value->toArray();
if (is_array($value)) {
// Attribute::createAttributeValue() is already normalizing some stuff:
// - 'class' attribute must be a list
// - MarkupInterface values must be resolved.
$value = (new Attribute($value))->toArray();
}
elseif (is_a($value, '\Drupal\Core\Template\Attribute')) {
// Attribute PHP objects are rendered as strings by SDC ComponentValidator
// this is raising an error: "InvalidComponentException: String value
// found, but an object is required".
$value = $value->toArray();
}
else {
return [];
}
foreach ($value as $attr => $attr_value) {
$value[$attr] = self::normalizeAttrValue($attr_value);
}
return $value;
}
/**
* Normalize attribute value.
*/
protected static function normalizeAttrValue(mixed $value): mixed {
if (is_object($value)) {
return self::normalizeObject($value);
}
if (is_array($value) && array_is_list($value)) {
return self::normalizeList($value);
}
if (is_array($value) && !array_is_list($value)) {
return self::normalizeMapping($value);
}
// We don't allow markup in attribute value.
return strip_tags((string) $value);
}
/**
* Normalize list item.
*/
protected static function normalizeListItem(mixed $value): mixed {
if (is_object($value)) {
return self::normalizeObject($value);
}
if (is_array($value) && array_is_list($value)) {
// We encode to JSON because we don't know how deep is the nesting.
return json_encode($value, 0, 3) ?: "";
}
if (is_array($value) && !array_is_list($value)) {
return self::normalizeRenderArray($value);
}
// Integer and number are always allowed values.
if (is_int($value) || is_float($value)) {
return $value;
}
// We don't allow markup in attribute value.
return strip_tags((string) $value);
}
/**
* Normalize object attribute value.
*/
protected static function normalizeObject(object $value): array|string {
if ($value instanceof Url) {
return $value->toString();
}
if ($value instanceof RenderableInterface) {
return static::normalizeRenderArray($value->toRenderable());
}
if ($value instanceof MarkupInterface) {
return (string) $value;
}
if ($value instanceof \Stringable) {
return (string) $value;
}
// Instead of keeping an unexpected object, we return PHP namespace.
// It will be valid and can inform the component user about its mistake.
return get_class($value);
}
/**
* Normalize list attribute value.
*/
protected static function normalizeList(array $value): array {
foreach ($value as $index => $item) {
$value[$index] = self::normalizeListItem($item);
}
return $value;
}
/**
* Normalize mapping attribute value.
*/
protected static function normalizeMapping(array $value): array|string {
if (!empty(Element::properties($value))) {
return static::normalizeRenderArray($value);
}
return static::normalizeList(array_values($value));
}
/**
* Normalize render array.
*/
protected static function normalizeRenderArray(array $value): string {
if (!empty(Element::properties($value))) {
$markup = (string) \Drupal::service('renderer')->render($value);
return strip_tags($markup);
}
// We encode to JSON because we don't know how deep is the nesting.
return json_encode($value, 0, 3) ?: "";
}
/**
* {@inheritdoc}
*/
......
......@@ -156,12 +156,7 @@ class LinksPropType extends PropTypePluginBase implements ContainerFactoryPlugin
unset($item[$property]);
return $item;
}
// Convert URL objects to strings.
foreach ($item[$property] as $attr => $value) {
if ($value instanceof Url) {
$item[$property][$attr] = $value->toString();
}
}
$item[$property] = AttributesPropType::normalize($item[$property]);
return $item;
}
......
<?php
declare(strict_types=1);
namespace Drupal\Tests\ui_patterns\Unit;
use Drupal\Tests\UnitTestCase;
use Drupal\ui_patterns\Plugin\UiPatterns\PropType\AttributesPropType;
/**
* @coversDefaultClass \Drupal\ui_patterns\Plugin\UiPatterns\PropType\LinksPropType
*
* @group ui_patterns
*/
final class AttributesPropTypeNormalizationTest extends UnitTestCase {
/**
* @covers ::normalize
*
* @dataProvider provideNormalizationData
*/
public function testNormalize(array $value, array $expected): void {
$normalized = AttributesPropType::normalize($value);
self::assertEquals($normalized, $expected);
}
/**
* Provide data for testNormalize.
*/
public static function provideNormalizationData(): \Generator {
$data = [
"Empty value" => [
"value" => [],
"expected" => [],
],
"Standardized primitives, so already OK" => self::standardizedPrimitives(),
"Type transformations" => self::typeTransformation(),
"List array" => self::listArray(),
];
foreach ($data as $label => $test) {
yield $label => [
$test['value'],
$test['expected'],
];
};
}
/**
* Standardized primitives, so already OK.
*/
protected static function standardizedPrimitives() {
$value = [
"foo" => "bar",
"string" => "Lorem ipsum",
"array" => [
"One",
"Two",
3,
],
"integer" => 4,
"float" => 1.4,
];
$expected = $value;
return [
"value" => $value,
"expected" => $expected,
];
}
/**
* Type transformations.
*/
protected static function typeTransformation() {
$value = [
"true_boolean" => TRUE,
"false_boolean" => FALSE,
"null_boolean" => NULL,
"markup" => "Hello <b>World</b>",
"associative_array" => [
"Un" => "One",
"Deux" => "Two",
"Trois" => 3,
],
"nested_array" => [
"One",
[
"Two",
"Three",
],
[
"deep" => [
"very deep" => ["foo", "bar"],
],
],
],
];
$expected = [
"true_boolean" => "1",
"false_boolean" => "",
"null_boolean" => "",
"markup" => "Hello World",
"associative_array" => [
"One",
"Two",
3,
],
"nested_array" => [
"One",
// JSON encoding because we don't know how deep is the nesting.
'["Two","Three"]',
'{"deep":{"very deep":["foo","bar"]}}',
],
];
return [
"value" => $value,
"expected" => $expected,
];
}
/**
* List array.
*/
protected static function listArray() {
$value = [
"One",
"Two",
3,
];
// This doesn't look like a valid HTML attribute structure, but we rely on
// Drupal Attribute object normalization here.
$expected = [
"0" => "One",
"1" => "Two",
"2" => 3,
];
return [
"value" => $value,
"expected" => $expected,
];
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment