Skip to content
Snippets Groups Projects
Commit 8c6d5ab5 authored by Wim Leers's avatar Wim Leers
Browse files

Issue #3453152 by Wim Leers, tedbow, larowlan: Centralize & standardize logic...

Issue #3453152 by Wim Leers, tedbow, larowlan: Centralize & standardize logic for constructing *PropSource objects + kernel test coverage
parent 5980589d
No related branches found
No related tags found
1 merge request!46#3453152: Centralize & standardize logic for constructing `*PropSource` objects
Pipeline #220710 passed
Showing
with 441 additions and 52 deletions
......@@ -18,8 +18,7 @@ required: true
translatable: false
default_value:
- tree: '[{"uuid":"dynamic-image-udf7d","type":"experience_builder:image"},{"uuid":"static-static-card1ab","type":"sdc_test:my-cta"},{"uuid":"dynamic-static-card2df","type":"sdc_test:my-cta"},{"uuid":"dynamic-dynamic-card3rr","type":"sdc_test:my-cta"},{"uuid":"dynamic-image-static-imageStyle-something7d","type":"experience_builder:image"}]'
# cspell:disable-next-line
props: '{"dynamic-static-card2df":{"text":{"sourceType":"dynamic","expression":"\u2139\ufe0e\u241centity:node:article\u241dtitle\u241e\u241fvalue"},"href":{"sourceType":"static:field_item:uri","value":"https:\/\/drupal.org","expression":"\u2139\ufe0euri\u241fvalue"}},"static-static-card1ab":{"text":{"sourceType":"static:field_item:string","value":"hello, world!","expression":"\u2139\ufe0estring\u241fvalue"},"href":{"sourceType":"static:field_item:uri","value":"https:\/\/drupal.org","expression":"\u2139\ufe0euri\u241fvalue"}},"dynamic-dynamic-card3rr":{"text":{"sourceType":"dynamic","expression":"\u2139\ufe0e\u241centity:node:article\u241dtitle\u241e\u241fvalue"},"href":{"sourceType":"dynamic","expression":"\u2139\ufe0e\u241centity:node:article\u241dfield_hero\u241e\u241fentity\u241c\u241centity:file\u241duri\u241e\u241fvalue"}},"dynamic-image-udf7d":{"image":{"sourceType":"dynamic","expression":"\u2139\ufe0e\u241centity:node:article\u241dfield_hero\u241e\u241f{src\u219dentity\u241c\u241centity:file\u241duri\u241e\u241fvalue,alt\u21a0alt,width\u21a0width,height\u21a0height}"}},"dynamic-image-static-imageStyle-something7d":{"image":{"sourceType":"adapter:image_apply_style","adapterInputs":{"image":{"sourceType":"dynamic","expression":"\u2139\ufe0e\u241centity:node:article\u241dfield_hero\u241e\u241f{src\u219dentity\u241c\u241centity:file\u241duri\u241e0\u241fvalue,alt\u21a0alt,width\u21a0width,height\u21a0height}"},"imageStyle":{"sourceType":"static:field_item:string","value":"thumbnail","expression":"\u2139\ufe0estring\u241fvalue"}}}}}'
props: '{"dynamic-static-card2df":{"text":{"sourceType":"dynamic","expression":"ℹ︎␜entity:node:article␝title␞␟value"},"href":{"sourceType":"static:field_item:uri","value":"https:\/\/drupal.org","expression":"ℹ︎uri␟value"}},"static-static-card1ab":{"text":{"sourceType":"static:field_item:string","value":"hello, world!","expression":"ℹ︎string␟value"},"href":{"sourceType":"static:field_item:uri","value":"https:\/\/drupal.org","expression":"ℹ︎uri␟value"}},"dynamic-dynamic-card3rr":{"text":{"sourceType":"dynamic","expression":"ℹ︎␜entity:node:article␝title␞␟value"},"href":{"sourceType":"dynamic","expression":"ℹ︎␜entity:node:article␝field_hero␞␟entity␜␜entity:file␝uri␞␟value"}},"dynamic-image-udf7d":{"image":{"sourceType":"dynamic","expression":"ℹ︎␜entity:node:article␝field_hero␞␟{src↝entity␜␜entity:file␝uri␞␟value,alt↠alt,width↠width,height↠height}"}},"dynamic-image-static-imageStyle-something7d":{"image":{"sourceType":"adapter:image_apply_style","adapterInputs":{"image":{"sourceType":"dynamic","expression":"ℹ︎␜entity:node:article␝field_hero␞␟{src↝entity␜␜entity:file␝uri␞0␟value,alt↠alt,width↠width,height↠height}"},"imageStyle":{"sourceType":"static:field_item:string","value":"thumbnail","expression":"ℹ︎string␟value"}}}}}'
default_value_callback: ''
settings:
translation: symmetric|asymmetric
......
<?php
declare(strict_types=1);
namespace Drupal\experience_builder\Plugin\Adapter;
use Drupal\Core\StringTranslation\TranslatableMarkup;
#[Adapter(
id: 'day_count',
label: new TranslatableMarkup('Count days'),
inputs: [
'oldest' => ['type' => 'string', 'format' => 'date'],
'newest' => ['type' => 'string', 'format' => 'date'],
],
requiredInputs: ['oldest'],
output: ['type' => 'integer'],
)]
final class DayCountAdapter extends AdapterBase {
protected string $oldest;
protected ?string $newest = NULL;
public function adapt(): mixed {
$utc = new \DateTimeZone("UTC");
$oldest = \DateTime::createFromFormat('Y-m-d', $this->oldest, $utc);
$newest = $this->newest
? \DateTime::createFromFormat('Y-m-d', $this->newest, $utc)
: new \DateTimeImmutable("now", $utc);
// Note: $oldest and $newest are already guaranteed to be valid, so this
// assertion exists only to satisfy PHPStan.
assert($oldest !== FALSE && $newest !== FALSE);
return $newest->diff($oldest)->days;
}
}
<?php
declare(strict_types=1);
namespace Drupal\experience_builder\Plugin\Adapter;
use Drupal\Core\StringTranslation\TranslatableMarkup;
#[Adapter(
id: 'unix_to_date',
label: new TranslatableMarkup('UNIX timestamp to date'),
inputs: [
'unix' => ['type' => 'integer'],
],
requiredInputs: ['unix'],
output: ['type' => 'string', 'format' => 'date'],
)]
final class UnixTimestampToDateAdapter extends AdapterBase {
protected string $unix;
public function adapt(): mixed {
// @todo Ensure that the `unix` input is constrained to the appropriate range.
$datetime = \DateTime::createFromFormat('U', $this->unix);
assert($datetime !== FALSE);
return $datetime->format('Y-m-d');
}
}
......@@ -8,8 +8,8 @@ use Drupal\Component\Serialization\Json;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\TypedData\Attribute\DataType;
use Drupal\Core\TypedData\TypedData;
use Drupal\experience_builder\PropSource\AdaptedPropSource;
use Drupal\experience_builder\PropSource\DynamicPropSource;
use Drupal\experience_builder\PropSource\PropSource;
use Drupal\experience_builder\PropSource\PropSourceBase;
use Drupal\experience_builder\PropSource\StaticPropSource;
/**
......@@ -90,7 +90,7 @@ class ComponentPropsValues extends TypedData implements \Stringable {
* @param string $component_instance_uuid
* The UUID of a placed component instance.
*
* @return array<string, \Drupal\experience_builder\PropSource\PropSource>
* @return array<string, \Drupal\experience_builder\PropSource\PropSourceBase>
*/
public function getComponentPropsSources(string $component_instance_uuid): array {
if (!array_key_exists($component_instance_uuid, $this->propsValues)) {
......@@ -98,12 +98,7 @@ class ComponentPropsValues extends TypedData implements \Stringable {
}
return array_map(
fn (array $sdc_prop_source) => match (TRUE) {
$sdc_prop_source['sourceType'] === 'dynamic' => DynamicPropSource::parse($sdc_prop_source),
str_starts_with($sdc_prop_source['sourceType'], 'static:') => StaticPropSource::parse($sdc_prop_source),
str_starts_with($sdc_prop_source['sourceType'], 'adapter:') => AdaptedPropSource::parse($sdc_prop_source),
default => throw new \OutOfRangeException(),
},
fn (array $prop_source): PropSourceBase => PropSource::parse($prop_source),
$this->propsValues[$component_instance_uuid]
);
}
......
......@@ -22,7 +22,7 @@ use Drupal\experience_builder\FieldForComponentSuggester;
use Drupal\experience_builder\Plugin\DataType\ComponentPropsValues;
use Drupal\experience_builder\Plugin\DataType\ComponentTreeHydrated;
use Drupal\experience_builder\Plugin\DataType\ComponentTreeStructure;
use Drupal\experience_builder\PropSource\PropSource;
use Drupal\experience_builder\PropSource\PropSourceBase;
/**
* Plugin implementation of the 'component_tree' field type.
......@@ -208,7 +208,7 @@ class ComponentTreeItem extends FieldItemBase implements RenderableInterface {
$entity = $this->getEntity();
return array_map(
fn (PropSource $s): mixed => $s->evaluate($entity),
fn (PropSourceBase $s): mixed => $s->evaluate($entity),
$props->getComponentPropsSources($component_instance_uuid)
);
}
......
......@@ -8,7 +8,10 @@ use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\experience_builder\Plugin\AdapterManager;
use Drupal\experience_builder\Plugin\Adapter\AdapterInterface;
final class AdaptedPropSource extends PropSource {
/**
* @phpstan-import-type AdaptedPropSourceArray from PropSource
*/
final class AdaptedPropSource extends PropSourceBase {
/**
* @param \Drupal\experience_builder\Plugin\Adapter\AdapterInterface $adapter_instance
......@@ -19,30 +22,47 @@ final class AdaptedPropSource extends PropSource {
private readonly array $adapter_inputs,
) {}
/**
* {@inheritdoc}
*/
public static function getSourceTypePrefix(): string {
return 'adapter';
}
/**
* {@inheritdoc}
*/
public function getSourceType(): string {
return $this->getSourceTypePrefix() . self::SOURCE_TYPE_PREFIX_SEPARATOR . $this->adapter_instance->getPluginId();
}
/**
* {@inheritdoc}
*/
public function __toString(): string {
// @phpstan-ignore-next-line
return json_encode([
'sourceType' => 'adapter:' . $this->adapter_instance->getPluginId(),
'adapterInputs' => array_map(
fn (PropSource $source): array => json_decode((string) $source, TRUE),
'sourceType' => $this->getSourceType(),
'adapterInputs' => array_combine(
array_keys($this->adapter_inputs),
array_map(
fn (string $input_name): PropSource => $this->getInputPropSource($input_name),
array_keys($this->adapter_inputs)
)
fn (PropSourceBase $source): array => json_decode((string) $source, TRUE),
array_map(
fn (string $input_name): PropSourceBase => $this->getInputPropSource($input_name),
array_keys($this->adapter_inputs)
)
),
),
], JSON_UNESCAPED_UNICODE);
}
/**
* @param array{sourceType: string, expression: string, value?: array<string, mixed>, adapterInputs?: array<string, mixed>} $sdc_prop_source
* @param AdaptedPropSourceArray $sdc_prop_source
*/
public static function parse(array $sdc_prop_source): static {
$adapter_manager = \Drupal::service(AdapterManager::class);
assert($adapter_manager instanceof AdapterManager);
$adapter_instance = $adapter_manager->createInstance(explode(':', $sdc_prop_source['sourceType'])[1]);
$adapter_instance = $adapter_manager->createInstance(explode(self::SOURCE_TYPE_PREFIX_SEPARATOR, $sdc_prop_source['sourceType'])[1]);
assert($adapter_instance instanceof AdapterInterface);
// `sourceType = adapter:*` requires adapterInputs to be specified.
......@@ -72,15 +92,8 @@ final class AdaptedPropSource extends PropSource {
return $this->adapter_instance->getPluginId();
}
public function getInputPropSource(string $input_name) : StaticPropSource|DynamicPropSource {
$input = $this->adapter_inputs[$input_name];
return match(TRUE) {
$input['sourceType'] === 'dynamic' => DynamicPropSource::parse($input),
// @todo Determine whether nested adapted inputs should be supported.
// str_starts_with($input['sourceType'], 'adapter:') => AdaptedPropSource::parse($input),
str_starts_with($input['sourceType'], 'static:') => StaticPropSource::parse($input),
default => throw new \OutOfRangeException(),
};
public function getInputPropSource(string $input_name) : PropSourceBase {
return PropSource::parse($this->adapter_inputs[$input_name]);
}
}
......@@ -9,19 +9,33 @@ use Drupal\experience_builder\PropExpressions\StructuredData\Evaluator;
use Drupal\experience_builder\PropExpressions\StructuredData\StructuredDataPropExpression;
use Drupal\experience_builder\PropExpressions\StructuredData\StructuredDataPropExpressionInterface;
final class DynamicPropSource extends PropSource {
final class DynamicPropSource extends PropSourceBase {
public function __construct(
private readonly StructuredDataPropExpressionInterface $expression,
) {}
/**
* {@inheritdoc}
*/
public static function getSourceTypePrefix(): string {
return 'dynamic';
}
/**
* {@inheritdoc}
*/
public function getSourceType(): string {
return self::getSourceTypePrefix();
}
/**
* {@inheritdoc}
*/
public function __toString(): string {
// @phpstan-ignore-next-line
return json_encode([
'sourceType' => 'dynamic',
'sourceType' => $this->getSourceType(),
'expression' => (string) $this->expression,
], JSON_UNESCAPED_UNICODE);
}
......@@ -35,6 +49,7 @@ final class DynamicPropSource extends PropSource {
if (!empty($missing)) {
throw new \LogicException(sprintf('Missing the keys %s.', implode(',', $missing)));
}
assert(array_key_exists('expression', $sdc_prop_source));
return new DynamicPropSource(StructuredDataPropExpression::fromString($sdc_prop_source['expression']));
}
......
......@@ -4,17 +4,39 @@ declare(strict_types=1);
namespace Drupal\experience_builder\PropSource;
use Drupal\Core\Entity\FieldableEntityInterface;
abstract class PropSource implements \Stringable {
/**
* @phpstan-type PropSourceArray array{sourceType: string, expression: string, value?: mixed|array<string, mixed>}
* TRICKY: adapters can be chained/nested, PHPStan does not allow expressing that.
* @phpstan-type AdaptedPropSourceArray array{sourceType: string, adapterInputs: array<string, mixed>}
*/
final class PropSource {
/**
* @param array{sourceType: string, expression: string, value: array<string, mixed>} $sdc_prop_source
* @param PropSourceArray|AdaptedPropSourceArray $prop_source
*/
abstract public static function parse(array $sdc_prop_source): static;
abstract public function evaluate(FieldableEntityInterface $host_entity): mixed;
abstract public function asChoice(): string;
public static function parse(array $prop_source): PropSourceBase {
$source_type_prefix = strstr($prop_source['sourceType'], PropSourceBase::SOURCE_TYPE_PREFIX_SEPARATOR, TRUE);
// If the prefix separator is not present, then use the full source type.
// For example: `dynamic` does not need a more detailed source type.
// @see \Drupal\experience_builder\PropSource\DynamicPropSource::__toString()
if ($source_type_prefix === FALSE) {
$source_type_prefix = $prop_source['sourceType'];
}
// The AdaptedPropSource is the exception: it composes multiple other prop
// sources, and those are listed under `adapterInputs`.
if ($source_type_prefix === AdaptedPropSource::getSourceTypePrefix()) {
assert(array_key_exists('adapterInputs', $prop_source));
return AdaptedPropSource::parse($prop_source);
}
// All others PropSources are the norm: they each have an expression.
assert(array_key_exists('expression', $prop_source));
return match ($source_type_prefix) {
StaticPropSource::getSourceTypePrefix() => StaticPropSource::parse($prop_source),
DynamicPropSource::getSourceTypePrefix() => DynamicPropSource::parse($prop_source),
default => throw new \LogicException('Unknown source type.'),
};
}
}
<?php
declare(strict_types=1);
namespace Drupal\experience_builder\PropSource;
use Drupal\Core\Entity\FieldableEntityInterface;
/**
* @phpstan-import-type PropSourceArray from PropSource
* @phpstan-import-type AdaptedPropSourceArray from PropSource
*/
abstract class PropSourceBase implements \Stringable {
const SOURCE_TYPE_PREFIX_SEPARATOR = ':';
/**
* @param PropSourceArray|AdaptedPropSourceArray $sdc_prop_source
*/
abstract public static function parse(array $sdc_prop_source): static;
abstract public function evaluate(FieldableEntityInterface $host_entity): mixed;
abstract public function asChoice(): string;
abstract public static function getSourceTypePrefix(): string;
abstract public function getSourceType(): string;
}
......@@ -25,13 +25,20 @@ use Drupal\experience_builder\PropExpressions\StructuredData\StructuredDataPropE
/**
* @todo Finalize name. "Fixed" might be better. "Local" might be even better?
*/
final class StaticPropSource extends PropSource {
final class StaticPropSource extends PropSourceBase {
public function __construct(
private readonly FieldItemInterface $fieldItem,
private readonly StructuredDataPropExpressionInterface $expression,
) {}
/**
* {@inheritdoc}
*/
public static function getSourceTypePrefix(): string {
return 'static';
}
/**
* {@inheritdoc}
*/
......@@ -79,6 +86,7 @@ final class StaticPropSource extends PropSource {
if (!empty($missing)) {
throw new \LogicException(sprintf('Missing the keys %s.', implode(',', $missing)));
}
assert(array_key_exists('value', $sdc_prop_source));
// First: construct an expression object from the expression string.
$expression = StructuredDataPropExpression::fromString($sdc_prop_source['expression']);
......@@ -144,7 +152,7 @@ final class StaticPropSource extends PropSource {
}
public function getSourceType(): string {
return sprintf("static:%s", $this->fieldItem->getDataDefinition()->getDataType());
return self::getSourceTypePrefix() . self::SOURCE_TYPE_PREFIX_SEPARATOR . $this->fieldItem->getDataDefinition()->getDataType();
}
public function getValue(): mixed {
......
......@@ -17,7 +17,7 @@ use Drupal\experience_builder\Plugin\Validation\Constraint\StringSemanticsConstr
* @todo Question: Does React also use JSON schema for restricting/defining its props? I.e.: identical set of primitives or not?
* @todo expand test coverage for testing each known type as being REQUIRED too
* @todo enums are widely used — auto-generating e.g. FieldConfig using @FieldType=list_string + settings would solve the 90% use case
* @todo adapters for transforming @FieldType=timestamp -> `type:string,format=time`, @FieldType=datetime -> `type:string,format=time`, a StringSemanticsConstraint::MARKUP string could be adapted to StringSemanticsConstraint::PROSE
* @todo adapters for transforming @FieldType=timestamp -> `type:string,format=time`, @FieldType=datetime -> `type:string,format=time`, a StringSemanticsConstraint::MARKUP string could be adapted to StringSemanticsConstraint::PROSE, UnixTimestampToDateAdapter was a test-only start
* @todo the `array` type — in particular arrays of tuples/objects, for example an array of "(image uri, alt)" pairs for an image gallery component, see https://stackoverflow.com/questions/40750340/how-to-define-json-schema-for-mapstring-integer
* @todo `exclusiveMinimum` and `exclusiveMaximum` work differently in JSON schema draft 4 (which SDC uses) than other versions. This is a future BC nightmare.
* @todo for `string` + `format=duration`, Drupal core has \Drupal\Core\TypedData\Plugin\DataType\DurationIso8601, but nothing uses it!
......
......@@ -13,7 +13,7 @@ use Drupal\experience_builder\Plugin\DataType\ComponentTreeHydrated;
use Drupal\experience_builder\Plugin\DataType\ComponentTreeStructure;
use Drupal\experience_builder\PropSource\AdaptedPropSource;
use Drupal\experience_builder\PropSource\DynamicPropSource;
use Drupal\experience_builder\PropSource\PropSource;
use Drupal\experience_builder\PropSource\PropSourceBase;
use Drupal\experience_builder\PropSource\StaticPropSource;
use Drupal\file\Entity\File;
use Drupal\image\Entity\ImageStyle;
......@@ -207,7 +207,9 @@ class EndToEndDemoIntegrationTest extends BrowserTestBase {
],
], json_decode($props->getValue(), TRUE));
// Second, assert the interpreted results.
$make_source_assertable = fn (PropSource $source) : array => [
// More detailed/theoretical tests also exist.
// @see \Drupal\Tests\experience_builder\Kernel\PropSourceTest
$make_source_assertable = fn (PropSourceBase $source) : array => [
'source class' => get_class($source),
'JSONified' => (string) $source,
'evaluated' => $source->evaluate($node),
......@@ -272,7 +274,7 @@ class EndToEndDemoIntegrationTest extends BrowserTestBase {
$this->assertEquals([
'image' => [
'source class' => AdaptedPropSource::class,
'JSONified' => '{"sourceType":"adapter:image_apply_style","adapterInputs":[{"sourceType":"dynamic","expression":"ℹ︎␜entity:node:article␝field_hero␞␟{src↝entity␜␜entity:file␝uri␞0␟value,alt↠alt,width↠width,height↠height}"},{"sourceType":"static:field_item:string","value":"thumbnail","expression":"ℹ︎string␟value"}]}',
'JSONified' => '{"sourceType":"adapter:image_apply_style","adapterInputs":{"image":{"sourceType":"dynamic","expression":"ℹ︎␜entity:node:article␝field_hero␞␟{src↝entity␜␜entity:file␝uri␞0␟value,alt↠alt,width↠width,height↠height}"},"imageStyle":{"sourceType":"static:field_item:string","value":"thumbnail","expression":"ℹ︎string␟value"}}}',
'evaluated' => [
'src' => ImageStyle::load('thumbnail')->buildUrl(File::load(1)->getFileUri()),
'alt' => 'A random image for testing purposes.',
......
......@@ -53,8 +53,7 @@ final class FieldTypeUninstallValidatorTest extends KernelTestBase {
'type' => 'article',
'field_xb_test' => [
'tree' => '[{"uuid":"dynamic-static-card2df","type":"sdc_test:my-cta"}]',
// cspell:ignore centity dtitle elink furi fvalue
'props' => '{"dynamic-static-card2df":{"text":{"sourceType":"dynamic","expression":"\u2139\ufe0e\u241centity:node:article\u241dtitle\u241e\u241fvalue"},"href":{"sourceType":"static:field_item:link","value":{"uri":"https:\/\/drupal.org","title":null,"options":[]},"expression":"ℹ︎link␟uri"}}}',
'props' => '{"dynamic-static-card2df":{"text":{"sourceType":"dynamic","expression":"ℹ︎␜entity:node:article␝title␞␟value"},"href":{"sourceType":"static:field_item:link","value":{"uri":"https:\/\/drupal.org","title":null,"options":[]},"expression":"ℹ︎link␟uri"}}}',
],
]);
$this->expectException(ModuleUninstallValidatorException::class);
......
......@@ -21,8 +21,7 @@ class ComponentTreeItemTest extends KernelTestBase {
const DEFAULT_VALUE = [
'tree' => '[{"uuid":"dynamic-image-udf7d","type":"experience_builder:image"},{"uuid":"static-static-card1ab","type":"sdc_test:my-cta"},{"uuid":"dynamic-static-card2df","type":"sdc_test:my-cta"},{"uuid":"dynamic-dynamic-card3rr","type":"sdc_test:my-cta"},{"uuid":"dynamic-image-static-imageStyle-something7d","type":"experience_builder:image"}]',
// cspell:disable-next-line
'props' => '{"dynamic-static-card2df":{"text":{"sourceType":"dynamic","expression":"\u2139\ufe0e\u241centity:node:article\u241dtitle\u241e\u241fvalue"},"href":{"sourceType":"static:field_item:uri","value":"https:\/\/drupal.org","expression":"\u2139\ufe0euri\u241fvalue"}},"static-static-card1ab":{"text":{"sourceType":"static:field_item:string","value":"hello, world!","expression":"\u2139\ufe0estring\u241fvalue"},"href":{"sourceType":"static:field_item:uri","value":"https:\/\/drupal.org","expression":"\u2139\ufe0euri\u241fvalue"}},"dynamic-dynamic-card3rr":{"text":{"sourceType":"dynamic","expression":"\u2139\ufe0e\u241centity:node:article\u241dtitle\u241e\u241fvalue"},"href":{"sourceType":"dynamic","expression":"\u2139\ufe0e\u241centity:node:article\u241dfield_hero\u241e\u241fentity\u241c\u241centity:file\u241duri\u241e\u241fvalue"}},"dynamic-image-udf7d":{"image":{"sourceType":"dynamic","expression":"\u2139\ufe0e\u241centity:node:article\u241dfield_hero\u241e\u241f{src\u219dentity\u241c\u241centity:file\u241duri\u241e\u241fvalue,alt\u21a0alt,width\u21a0width,height\u21a0height}"}},"dynamic-image-static-imageStyle-something7d":{"image":{"sourceType":"adapter:image_apply_style","adapterInputs":{"image":{"sourceType":"dynamic","expression":"\u2139\ufe0e\u241centity:node:article\u241dfield_hero\u241e\u241f{src\u219dentity\u241c\u241centity:file\u241duri\u241e0\u241fvalue,alt\u21a0alt,width\u21a0width,height\u21a0height}"},"imageStyle":{"sourceType":"static:field_item:string","value":"thumbnail","expression":"\u2139\ufe0estring\u241fvalue"}}}}}',
'props' => '{"dynamic-static-card2df":{"text":{"sourceType":"dynamic","expression":"ℹ︎␜entity:node:article␝title␞␟value"},"href":{"sourceType":"static:field_item:uri","value":"https:\/\/drupal.org","expression":"ℹ︎uri␟value"}},"static-static-card1ab":{"text":{"sourceType":"static:field_item:string","value":"hello, world!","expression":"ℹ︎string␟value"},"href":{"sourceType":"static:field_item:uri","value":"https:\/\/drupal.org","expression":"ℹ︎uri␟value"}},"dynamic-dynamic-card3rr":{"text":{"sourceType":"dynamic","expression":"ℹ︎␜entity:node:article␝title␞␟value"},"href":{"sourceType":"dynamic","expression":"ℹ︎␜entity:node:article␝field_hero␞␟entity␜␜entity:file␝uri␞␟value"}},"dynamic-image-udf7d":{"image":{"sourceType":"dynamic","expression":"ℹ︎␜entity:node:article␝field_hero␞␟{src↝entity␜␜entity:file␝uri␞␟value,alt↠alt,width↠width,height↠height}"}},"dynamic-image-static-imageStyle-something7d":{"image":{"sourceType":"adapter:image_apply_style","adapterInputs":{"image":{"sourceType":"dynamic","expression":"ℹ︎␜entity:node:article␝field_hero␞␟{src↝entity␜␜entity:file␝uri␞0␟value,alt↠alt,width↠width,height↠height}"},"imageStyle":{"sourceType":"static:field_item:string","value":"thumbnail","expression":"ℹ︎string␟value"}}}}}',
];
const EXPECTED_DEPENDENCIES = [
'config' => ['experience_builder.component.experience_builder+image', 'experience_builder.component.sdc_test+my-cta'],
......
<?php
declare(strict_types=1);
namespace Drupal\Tests\experience_builder\Kernel;
use Drupal\Core\Field\Plugin\Field\FieldWidget\StringTextfieldWidget;
use Drupal\datetime_range\Plugin\Field\FieldWidget\DateRangeDatelistWidget;
use Drupal\datetime_range\Plugin\Field\FieldWidget\DateRangeDefaultWidget;
use Drupal\experience_builder\PropExpressions\StructuredData\FieldPropExpression;
use Drupal\experience_builder\PropExpressions\StructuredData\FieldTypeObjectPropsExpression;
use Drupal\experience_builder\PropExpressions\StructuredData\FieldTypePropExpression;
use Drupal\experience_builder\PropExpressions\StructuredData\StructuredDataPropExpression;
use Drupal\experience_builder\PropSource\AdaptedPropSource;
use Drupal\experience_builder\PropSource\DynamicPropSource;
use Drupal\experience_builder\PropSource\PropSource;
use Drupal\experience_builder\PropSource\StaticPropSource;
use Drupal\KernelTests\KernelTestBase;
use Drupal\Tests\experience_builder\Traits\ContribStrictConfigSchemaTestTrait;
use Drupal\user\Entity\User;
/**
* @coversDefaultClass \Drupal\experience_builder\PropSource\PropSource
* @group experience_builder
*/
class PropSourceTest extends KernelTestBase {
use ContribStrictConfigSchemaTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'experience_builder',
'user',
'datetime',
'datetime_range',
];
/**
* @coversClass \Drupal\experience_builder\PropSource\StaticPropSource
*/
public function testStaticPropSource(): void {
// A simple example.
$simple_example = StaticPropSource::parse([
'sourceType' => 'static:field_item:string',
'value' => 'Hello, world!',
'expression' => 'ℹ︎string␟value',
]);
// First, get the string representation and parse it back, to prove
// serialization and deserialization works.
$json_representation = (string) $simple_example;
$this->assertSame('{"sourceType":"static:field_item:string","value":"Hello, world!","expression":"ℹ︎string␟value"}', $json_representation);
$simple_example = PropSource::parse(json_decode($json_representation, TRUE));
$this->assertInstanceOf(StaticPropSource::class, $simple_example);
// The contained information read back out.
$this->assertSame('static:field_item:string', $simple_example->getSourceType());
$this->assertInstanceOf(FieldTypePropExpression::class, StructuredDataPropExpression::fromString($simple_example->asChoice()));
$this->assertSame('Hello, world!', $simple_example->getValue());
// Test the functionality of a StaticPropSource:
// - evaluate it to populate an SDC prop
$this->assertSame('Hello, world!', $simple_example->evaluate(User::create([])));
// - generate a widget to edit the stored value — using the default widget
// or a specified widget.
// @see \Drupal\experience_builder\Entity\Component::$defaults
$this->assertInstanceOf(StringTextfieldWidget::class, $simple_example->getWidget('irrelevant-for-test'));
$this->assertInstanceOf(StringTextfieldWidget::class, $simple_example->getWidget('irrelevant-for-test', 'string_textfield'));
// The widget plugin manager ignores any request for another widget type and
// falls back to the default widget if
// @see \Drupal\Core\Field\WidgetPluginManager::getInstance()
$this->assertInstanceOf(StringTextfieldWidget::class, $simple_example->getWidget('irrelevant-for-test', 'string_textarea'));
// A complex example.
$complex_example = StaticPropSource::parse([
'sourceType' => 'static:field_item:daterange',
'value' => [
'value' => '2020-04-16T00:00',
'end_value' => '2024-07-10T10:24',
],
'expression' => 'ℹ︎daterange␟{start↠value,stop↠end_value}',
]);
// First, get the string representation and parse it back, to prove
// serialization and deserialization works.
$json_representation = (string) $complex_example;
$this->assertSame('{"sourceType":"static:field_item:daterange","value":{"value":"2020-04-16T00:00","end_value":"2024-07-10T10:24"},"expression":"ℹ︎daterange␟{start↠value,stop↠end_value}"}', $json_representation);
$complex_example = PropSource::parse(json_decode($json_representation, TRUE));
$this->assertInstanceOf(StaticPropSource::class, $complex_example);
// The contained information read back out.
$this->assertSame('static:field_item:daterange', $complex_example->getSourceType());
$this->assertInstanceOf(FieldTypeObjectPropsExpression::class, StructuredDataPropExpression::fromString($complex_example->asChoice()));
$this->assertSame([
'value' => '2020-04-16T00:00',
'end_value' => '2024-07-10T10:24',
], $complex_example->getValue());
// Test the functionality of a StaticPropSource:
// - evaluate it to populate an SDC prop
$this->assertSame([
'start' => '2020-04-16T00:00',
'stop' => '2024-07-10T10:24',
], $complex_example->evaluate(User::create([])));
// - generate a widget to edit the stored value — using the default widget
// or a specified widget.
// @see \Drupal\experience_builder\Entity\Component::$defaults
$this->assertInstanceOf(DateRangeDefaultWidget::class, $complex_example->getWidget('irrelevant-for-test'));
$this->assertInstanceOf(DateRangeDefaultWidget::class, $complex_example->getWidget('irrelevant-for-test', 'daterange_default'));
$this->assertInstanceOf(DateRangeDatelistWidget::class, $complex_example->getWidget('irrelevant-for-test', 'daterange_datelist'));
}
/**
* @coversClass \Drupal\experience_builder\PropSource\DynamicPropSource
*/
public function testDynamicPropSource(): void {
// A simple example.
$simple_example = DynamicPropSource::parse([
'sourceType' => 'dynamic',
'expression' => 'ℹ︎␜entity:user␝name␞␟value',
]);
// First, get the string representation and parse it back, to prove
// serialization and deserialization works.
$json_representation = (string) $simple_example;
$this->assertSame('{"sourceType":"dynamic","expression":"ℹ︎␜entity:user␝name␞␟value"}', $json_representation);
$simple_example = PropSource::parse(json_decode($json_representation, TRUE));
$this->assertInstanceOf(DynamicPropSource::class, $simple_example);
// The contained information read back out.
$this->assertSame('dynamic', $simple_example->getSourceType());
$this->assertInstanceOf(FieldPropExpression::class, StructuredDataPropExpression::fromString($simple_example->asChoice()));
// Test the functionality of a DynamicPropSource:
// - evaluate it to populate an SDC prop
$this->assertSame('John Doe', $simple_example->evaluate(User::create(['name' => 'John Doe'])));
}
/**
* @coversClass \Drupal\experience_builder\PropSource\AdaptedPropSource
*/
public function testAdaptedPropSource(): void {
// 2. user created access
// 1. daterange
// A simple static example.
$simple_static_example = AdaptedPropSource::parse([
'sourceType' => 'adapter:day_count',
'adapterInputs' => [
'oldest' => [
'sourceType' => 'static:field_item:daterange',
'value' => [
'value' => '2020-04-16',
'end_value' => '2024-11-04',
],
'expression' => 'ℹ︎daterange␟value',
],
'newest' => [
'sourceType' => 'static:field_item:daterange',
'value' => [
'value' => '2020-04-16',
'end_value' => '2024-11-04',
],
'expression' => 'ℹ︎daterange␟end_value',
],
],
]);
// First, get the string representation and parse it back, to prove
// serialization and deserialization works.
$json_representation = (string) $simple_static_example;
$this->assertSame('{"sourceType":"adapter:day_count","adapterInputs":{"oldest":{"sourceType":"static:field_item:daterange","value":{"value":"2020-04-16","end_value":"2024-11-04"},"expression":"ℹ︎daterange␟value"},"newest":{"sourceType":"static:field_item:daterange","value":{"value":"2020-04-16","end_value":"2024-11-04"},"expression":"ℹ︎daterange␟end_value"}}}', $json_representation);
$simple_static_example = PropSource::parse(json_decode($json_representation, TRUE));
$this->assertInstanceOf(AdaptedPropSource::class, $simple_static_example);
// The contained information read back out.
$this->assertSame('adapter:day_count', $simple_static_example->getSourceType());
// Test the functionality of a DynamicPropSource:
// - evaluate it to populate an SDC prop
$this->assertSame(1663, $simple_static_example->evaluate(User::create(['name' => 'John Doe', 'created' => 694695600, 'access' => 1720602713])));
// A simple dynamic example.
$simple_dynamic_example = AdaptedPropSource::parse([
'sourceType' => 'adapter:day_count',
'adapterInputs' => [
'oldest' => [
'sourceType' => 'adapter:unix_to_date',
'adapterInputs' => [
'unix' => [
'sourceType' => 'dynamic',
'expression' => 'ℹ︎␜entity:user␝created␞␟value',
],
],
],
'newest' => [
'sourceType' => 'adapter:unix_to_date',
'adapterInputs' => [
'unix' => [
'sourceType' => 'dynamic',
'expression' => 'ℹ︎␜entity:user␝access␞␟value',
],
],
],
],
]);
// First, get the string representation and parse it back, to prove
// serialization and deserialization works.
$json_representation = (string) $simple_dynamic_example;
$this->assertSame('{"sourceType":"adapter:day_count","adapterInputs":{"oldest":{"sourceType":"adapter:unix_to_date","adapterInputs":{"unix":{"sourceType":"dynamic","expression":"ℹ︎␜entity:user␝created␞␟value"}}},"newest":{"sourceType":"adapter:unix_to_date","adapterInputs":{"unix":{"sourceType":"dynamic","expression":"ℹ︎␜entity:user␝access␞␟value"}}}}}', $json_representation);
$simple_dynamic_example = PropSource::parse(json_decode($json_representation, TRUE));
$this->assertInstanceOf(AdaptedPropSource::class, $simple_dynamic_example);
// The contained information read back out.
$this->assertSame('adapter:day_count', $simple_dynamic_example->getSourceType());
// Test the functionality of a DynamicPropSource:
// - evaluate it to populate an SDC prop
$this->assertSame(11874, $simple_dynamic_example->evaluate(User::create(['name' => 'John Doe', 'created' => 694695600, 'access' => 1720602713])));
// A complex example.
$complex_example = AdaptedPropSource::parse([
'sourceType' => 'adapter:day_count',
'adapterInputs' => [
'oldest' => [
'sourceType' => 'static:field_item:datetime',
'value' => '2020-04-16',
'expression' => 'ℹ︎datetime␟value',
],
'newest' => [
'sourceType' => 'adapter:unix_to_date',
'adapterInputs' => [
'unix' => [
'sourceType' => 'dynamic',
'expression' => 'ℹ︎␜entity:user␝access␞␟value',
],
],
],
],
]);
// First, get the string representation and parse it back, to prove
// serialization and deserialization works.
$json_representation = (string) $complex_example;
$this->assertSame('{"sourceType":"adapter:day_count","adapterInputs":{"oldest":{"sourceType":"static:field_item:datetime","value":{"value":"2020-04-16"},"expression":"ℹ︎datetime␟value"},"newest":{"sourceType":"adapter:unix_to_date","adapterInputs":{"unix":{"sourceType":"dynamic","expression":"ℹ︎␜entity:user␝access␞␟value"}}}}}', $json_representation);
$complex_example = PropSource::parse(json_decode($json_representation, TRUE));
$this->assertInstanceOf(AdaptedPropSource::class, $complex_example);
// The contained information read back out.
$this->assertSame('adapter:day_count', $complex_example->getSourceType());
// Test the functionality of a DynamicPropSource:
// - evaluate it to populate an SDC prop
$this->assertSame(1546, $complex_example->evaluate(User::create(['name' => 'John Doe', 'created' => 694695600, 'access' => 1720602713])));
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment