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