From 7d6e003412b24be6f8e9601a6ab4a9ee1922ca98 Mon Sep 17 00:00:00 2001 From: Ted Bowman <ted+git@tedbow.com> Date: Tue, 18 Mar 2025 11:27:43 -0400 Subject: [PATCH 1/7] only require inputs for components that require them --- src/Plugin/DataType/ComponentInputs.php | 14 +++++++++++++- src/Plugin/DataType/ComponentTreeHydrated.php | 1 + src/Plugin/Field/FieldType/ComponentTreeItem.php | 5 ++++- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/Plugin/DataType/ComponentInputs.php b/src/Plugin/DataType/ComponentInputs.php index 87333e027b..6da21e118d 100644 --- a/src/Plugin/DataType/ComponentInputs.php +++ b/src/Plugin/DataType/ComponentInputs.php @@ -8,7 +8,9 @@ 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\ComponentSource\ComponentSourceInterface; use Drupal\experience_builder\MissingComponentInputsException; +use Drupal\experience_builder\Plugin\Field\FieldType\ComponentTreeItem; /** * @todo Implement ListInterface because it conceptually fits, but … what does it get us? @@ -125,8 +127,18 @@ class ComponentInputs extends TypedData implements \Stringable { * @throws \Drupal\experience_builder\MissingComponentInputsException */ public function getValues(string $component_instance_uuid): array { + $item = $this->parent; + assert($item instanceof ComponentTreeItem); + $tree = $item->get('tree'); + assert($tree instanceof ComponentTreeStructure); + $tree->getComponentId($component_instance_uuid); + $source = $tree->getComponentSource($component_instance_uuid); + \assert($source instanceof ComponentSourceInterface); if (!array_key_exists($component_instance_uuid, $this->inputs)) { - throw new MissingComponentInputsException($component_instance_uuid); + if ($source->requiresExplicitInput()) { + throw new MissingComponentInputsException($component_instance_uuid); + } + return []; } return $this->inputs[$component_instance_uuid]; diff --git a/src/Plugin/DataType/ComponentTreeHydrated.php b/src/Plugin/DataType/ComponentTreeHydrated.php index 56a3938c5e..647da8b03d 100644 --- a/src/Plugin/DataType/ComponentTreeHydrated.php +++ b/src/Plugin/DataType/ComponentTreeHydrated.php @@ -34,6 +34,7 @@ class ComponentTreeHydrated extends TypedData implements CacheableDependencyInte $item = $this->getParent(); assert($item instanceof ComponentTreeItem); $tree = $item->get('tree'); + assert($tree instanceof ComponentTreeStructure); $hydrated = []; diff --git a/src/Plugin/Field/FieldType/ComponentTreeItem.php b/src/Plugin/Field/FieldType/ComponentTreeItem.php index 1dcceffbe1..eff0430db4 100644 --- a/src/Plugin/Field/FieldType/ComponentTreeItem.php +++ b/src/Plugin/Field/FieldType/ComponentTreeItem.php @@ -16,6 +16,7 @@ use Drupal\Core\Render\RenderableInterface; use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\Core\TypedData\DataDefinition; use Drupal\experience_builder\Entity\Component; +use Drupal\experience_builder\Plugin\DataType\ComponentInputs; use Drupal\experience_builder\Plugin\DataType\ComponentTreeStructure; use Drupal\experience_builder\ShapeMatcher\FieldForComponentSuggester; use Symfony\Component\Validator\ConstraintViolation; @@ -261,6 +262,7 @@ class ComponentTreeItem extends FieldItemBase implements RenderableInterface { public function preSave(): void { $tree = $this->get('tree'); $inputs = $this->get('inputs'); + assert($inputs instanceof ComponentInputs); $component_instance_uuids = $tree->getComponentInstanceUuids(); $entity = $this->getRoot() === $this ? NULL : $this->getEntity(); @@ -305,7 +307,8 @@ class ComponentTreeItem extends FieldItemBase implements RenderableInterface { // This *internal-only* validation does not need to happen using validation // constraints because it does not validate user input: it only helps ensure // that the logic of this field type is correct. - if (array_intersect($component_instance_uuids, $inputs->getComponentInstanceUuids()) !== $component_instance_uuids) { + $input_required_uuids = array_filter($tree->getComponentInstanceUuids(), static fn(string $uuid) => $tree->getComponentSource($uuid)->requiresExplicitInput()); + if (array_intersect($input_required_uuids, $inputs->getComponentInstanceUuids()) !== $input_required_uuids) { throw new \LogicException(sprintf('The component UUIDs in the tree and inputs values do not match! Put a breakpoint here and figure out why.')); } -- GitLab From 2665ac67b006e4389efbcc74fc9846b087e2b7a1 Mon Sep 17 00:00:00 2001 From: Ted Bowman <ted+git@tedbow.com> Date: Tue, 18 Mar 2025 12:52:53 -0400 Subject: [PATCH 2/7] phpstan --- src/Plugin/Field/FieldType/ComponentTreeItem.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Plugin/Field/FieldType/ComponentTreeItem.php b/src/Plugin/Field/FieldType/ComponentTreeItem.php index eff0430db4..72e5856fd7 100644 --- a/src/Plugin/Field/FieldType/ComponentTreeItem.php +++ b/src/Plugin/Field/FieldType/ComponentTreeItem.php @@ -307,7 +307,7 @@ class ComponentTreeItem extends FieldItemBase implements RenderableInterface { // This *internal-only* validation does not need to happen using validation // constraints because it does not validate user input: it only helps ensure // that the logic of this field type is correct. - $input_required_uuids = array_filter($tree->getComponentInstanceUuids(), static fn(string $uuid) => $tree->getComponentSource($uuid)->requiresExplicitInput()); + $input_required_uuids = array_filter($tree->getComponentInstanceUuids(), static fn(string $uuid) => $tree->getComponentSource($uuid)?->requiresExplicitInput() === TRUE); if (array_intersect($input_required_uuids, $inputs->getComponentInstanceUuids()) !== $input_required_uuids) { throw new \LogicException(sprintf('The component UUIDs in the tree and inputs values do not match! Put a breakpoint here and figure out why.')); } -- GitLab From 498db236c12270fe5ea59bc24047ce9d98eca5a2 Mon Sep 17 00:00:00 2001 From: Ted Bowman <ted+git@tedbow.com> Date: Thu, 27 Mar 2025 09:58:32 -0400 Subject: [PATCH 3/7] start of unit test passing --- .../Plugin/DataType/ComponentInputsTest.php | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 tests/src/Unit/Plugin/DataType/ComponentInputsTest.php diff --git a/tests/src/Unit/Plugin/DataType/ComponentInputsTest.php b/tests/src/Unit/Plugin/DataType/ComponentInputsTest.php new file mode 100644 index 0000000000..0a0e556c4c --- /dev/null +++ b/tests/src/Unit/Plugin/DataType/ComponentInputsTest.php @@ -0,0 +1,96 @@ +<?php + +namespace Drupal\Tests\experience_builder\Unit\Plugin\DataType; + +use Drupal\Component\Serialization\Json; +use Drupal\Core\TypedData\DataDefinitionInterface; +use Drupal\experience_builder\ComponentSource\ComponentSourceInterface; +use Drupal\experience_builder\MissingComponentInputsException; +use Drupal\experience_builder\Plugin\DataType\ComponentInputs; +use Drupal\experience_builder\Plugin\DataType\ComponentTreeStructure; +use Drupal\experience_builder\Plugin\Field\FieldType\ComponentTreeItem; +use Drupal\Tests\UnitTestCase; +use Prophecy\PhpUnit\ProphecyTrait; + +/** + * Unit tests for the ComponentInputs class. + * + * @coversDefaultClass \Drupal\experience_builder\Plugin\DataType\ComponentInputs + * @group experience_builder + */ +class ComponentInputsTest extends UnitTestCase { + + use ProphecyTrait; + + /** + * Tests the getValues method. + * + * @covers ::getValues + */ + public function testGetValues() { + // UUID for testing. + $uuid = 'test-component-uuid'; + + // Create test data. + $test_inputs = [ + $uuid => [ + 'title' => [ + 'sourceType' => 'static:text', + 'value' => 'Test Title', + 'expression' => '', + ], + 'body' => [ + 'sourceType' => 'static:text', + 'value' => 'Test Body', + 'expression' => '', + ], + ], + ]; + + // Mock ComponentSource. + $component_source = $this->prophesize(ComponentSourceInterface::class); + + // Mock ComponentTreeStructure. + $tree = $this->prophesize(ComponentTreeStructure::class); + $tree->getComponentId($uuid)->willReturn('test-component-id'); + $tree->getComponentSource($uuid)->willReturn($component_source->reveal()); + + // Mock ComponentTreeItem. + $item = $this->prophesize(ComponentTreeItem::class); + $item->get('tree')->willReturn($tree->reveal()); + + // Setup ComponentInputs class with test data. + $component_inputs = new ComponentInputs( + $this->prophesize(DataDefinitionInterface::class)->reveal(), + ); + $component_inputs->setValue(Json::encode($test_inputs)); + + + + // Set parent property using reflection. + $reflection = new \ReflectionClass($component_inputs); + $parent_property = $reflection->getProperty('parent'); + $parent_property->setAccessible(TRUE); + $parent_property->setValue($component_inputs, $item->reveal()); + + // Test 1: Getting values for an existing UUID. + $values = $component_inputs->getValues($uuid); + $this->assertEquals($test_inputs[$uuid], $values); + + // Test 2: Getting values for a non-existing UUID that doesn't require explicit input. + $non_existing_uuid = 'non-existing-uuid'; + $tree->getComponentId($non_existing_uuid)->willReturn('some-id'); + $tree->getComponentSource($non_existing_uuid)->willReturn($component_source->reveal()); + $component_source->requiresExplicitInput()->willReturn(FALSE); + + $values = $component_inputs->getValues($non_existing_uuid); + $this->assertEquals([], $values); + + // Test 3: Getting values for a non-existing UUID that requires explicit input. + $component_source->requiresExplicitInput()->willReturn(TRUE); + + $this->expectException(MissingComponentInputsException::class); + $component_inputs->getValues($non_existing_uuid); + } + +} -- GitLab From 88fb88abfd2bd5dcd812994fc0d8ed88454c1c63 Mon Sep 17 00:00:00 2001 From: Ted Bowman <ted+git@tedbow.com> Date: Thu, 27 Mar 2025 10:31:21 -0400 Subject: [PATCH 4/7] cleanup test --- .../DataType/ComponentInputsTest.php | 49 ++++++------------- 1 file changed, 16 insertions(+), 33 deletions(-) rename tests/src/Unit/{Plugin => }/DataType/ComponentInputsTest.php (62%) diff --git a/tests/src/Unit/Plugin/DataType/ComponentInputsTest.php b/tests/src/Unit/DataType/ComponentInputsTest.php similarity index 62% rename from tests/src/Unit/Plugin/DataType/ComponentInputsTest.php rename to tests/src/Unit/DataType/ComponentInputsTest.php index 0a0e556c4c..dbc2a0d6ff 100644 --- a/tests/src/Unit/Plugin/DataType/ComponentInputsTest.php +++ b/tests/src/Unit/DataType/ComponentInputsTest.php @@ -1,5 +1,7 @@ <?php +declare(strict_types=1); + namespace Drupal\Tests\experience_builder\Unit\Plugin\DataType; use Drupal\Component\Serialization\Json; @@ -10,30 +12,21 @@ use Drupal\experience_builder\Plugin\DataType\ComponentInputs; use Drupal\experience_builder\Plugin\DataType\ComponentTreeStructure; use Drupal\experience_builder\Plugin\Field\FieldType\ComponentTreeItem; use Drupal\Tests\UnitTestCase; -use Prophecy\PhpUnit\ProphecyTrait; /** - * Unit tests for the ComponentInputs class. - * * @coversDefaultClass \Drupal\experience_builder\Plugin\DataType\ComponentInputs - * @group experience_builder */ class ComponentInputsTest extends UnitTestCase { - use ProphecyTrait; - /** - * Tests the getValues method. - * * @covers ::getValues */ public function testGetValues() { - // UUID for testing. - $uuid = 'test-component-uuid'; + $existing_component_uuid = 'test-component-uuid'; // Create test data. $test_inputs = [ - $uuid => [ + $existing_component_uuid => [ 'title' => [ 'sourceType' => 'static:text', 'value' => 'Test Title', @@ -46,49 +39,39 @@ class ComponentInputsTest extends UnitTestCase { ], ], ]; - - // Mock ComponentSource. $component_source = $this->prophesize(ComponentSourceInterface::class); - // Mock ComponentTreeStructure. $tree = $this->prophesize(ComponentTreeStructure::class); - $tree->getComponentId($uuid)->willReturn('test-component-id'); - $tree->getComponentSource($uuid)->willReturn($component_source->reveal()); + $tree->getComponentId($existing_component_uuid)->willReturn('test-component-id'); + $tree->getComponentSource($existing_component_uuid)->willReturn($component_source->reveal()); - // Mock ComponentTreeItem. $item = $this->prophesize(ComponentTreeItem::class); $item->get('tree')->willReturn($tree->reveal()); + $item->onChange(NULL)->shouldBeCalledTimes(1); - // Setup ComponentInputs class with test data. $component_inputs = new ComponentInputs( $this->prophesize(DataDefinitionInterface::class)->reveal(), + NULL, + $item->reveal() ); $component_inputs->setValue(Json::encode($test_inputs)); + // Test getting values for a existing UUID. + $this->assertEquals( + $test_inputs[$existing_component_uuid], + $component_inputs->getValues($existing_component_uuid) + ); - - // Set parent property using reflection. - $reflection = new \ReflectionClass($component_inputs); - $parent_property = $reflection->getProperty('parent'); - $parent_property->setAccessible(TRUE); - $parent_property->setValue($component_inputs, $item->reveal()); - - // Test 1: Getting values for an existing UUID. - $values = $component_inputs->getValues($uuid); - $this->assertEquals($test_inputs[$uuid], $values); - - // Test 2: Getting values for a non-existing UUID that doesn't require explicit input. + // Test getting values for a non-existing UUID that doesn't require explicit input. $non_existing_uuid = 'non-existing-uuid'; - $tree->getComponentId($non_existing_uuid)->willReturn('some-id'); $tree->getComponentSource($non_existing_uuid)->willReturn($component_source->reveal()); $component_source->requiresExplicitInput()->willReturn(FALSE); $values = $component_inputs->getValues($non_existing_uuid); $this->assertEquals([], $values); - // Test 3: Getting values for a non-existing UUID that requires explicit input. + // Test getting values for a non-existing UUID that requires explicit input. $component_source->requiresExplicitInput()->willReturn(TRUE); - $this->expectException(MissingComponentInputsException::class); $component_inputs->getValues($non_existing_uuid); } -- GitLab From 83be86f4a9ef02aa85a6b4b8d938ba6fa7d448c7 Mon Sep 17 00:00:00 2001 From: Ted Bowman <ted+git@tedbow.com> Date: Thu, 27 Mar 2025 10:31:53 -0400 Subject: [PATCH 5/7] remove unneeded code in non-test code --- src/Plugin/DataType/ComponentInputs.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Plugin/DataType/ComponentInputs.php b/src/Plugin/DataType/ComponentInputs.php index 6da21e118d..704600a1cb 100644 --- a/src/Plugin/DataType/ComponentInputs.php +++ b/src/Plugin/DataType/ComponentInputs.php @@ -131,7 +131,6 @@ class ComponentInputs extends TypedData implements \Stringable { assert($item instanceof ComponentTreeItem); $tree = $item->get('tree'); assert($tree instanceof ComponentTreeStructure); - $tree->getComponentId($component_instance_uuid); $source = $tree->getComponentSource($component_instance_uuid); \assert($source instanceof ComponentSourceInterface); if (!array_key_exists($component_instance_uuid, $this->inputs)) { -- GitLab From 652bf65d3fec39d342ab60a48e31b6111d338b96 Mon Sep 17 00:00:00 2001 From: Ted Bowman <ted+git@tedbow.com> Date: Thu, 27 Mar 2025 10:33:29 -0400 Subject: [PATCH 6/7] unneeded call --- src/Plugin/DataType/ComponentTreeHydrated.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Plugin/DataType/ComponentTreeHydrated.php b/src/Plugin/DataType/ComponentTreeHydrated.php index 647da8b03d..56a3938c5e 100644 --- a/src/Plugin/DataType/ComponentTreeHydrated.php +++ b/src/Plugin/DataType/ComponentTreeHydrated.php @@ -34,7 +34,6 @@ class ComponentTreeHydrated extends TypedData implements CacheableDependencyInte $item = $this->getParent(); assert($item instanceof ComponentTreeItem); $tree = $item->get('tree'); - assert($tree instanceof ComponentTreeStructure); $hydrated = []; -- GitLab From b2c9f819faff078d3b87a366c3f15c35a0dfcc65 Mon Sep 17 00:00:00 2001 From: Ted Bowman <ted+git@tedbow.com> Date: Thu, 27 Mar 2025 11:05:59 -0400 Subject: [PATCH 7/7] phpstan --- tests/src/Unit/DataType/ComponentInputsTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/src/Unit/DataType/ComponentInputsTest.php b/tests/src/Unit/DataType/ComponentInputsTest.php index dbc2a0d6ff..0e841d9296 100644 --- a/tests/src/Unit/DataType/ComponentInputsTest.php +++ b/tests/src/Unit/DataType/ComponentInputsTest.php @@ -21,7 +21,7 @@ class ComponentInputsTest extends UnitTestCase { /** * @covers ::getValues */ - public function testGetValues() { + public function testGetValues(): void { $existing_component_uuid = 'test-component-uuid'; // Create test data. -- GitLab