From df3782b2a09ec1f397dd599bcd54e55b309dad97 Mon Sep 17 00:00:00 2001 From: Pablo Largo Mohedano Date: Fri, 29 Aug 2025 14:06:50 +0200 Subject: [PATCH 1/4] vive-coded --- composer.json | 3 +- src/DynamicFormBuilder.php | 89 ++++++++++++++++++++--- tests/FunctionalTest.php | 39 ++++++++++ tests/fixtures/DynamicFormsTestKernel.php | 19 +++++ tests/fixtures/server.php | 18 +++++ tests/fixtures/templates/form.html.twig | 2 +- 6 files changed, 157 insertions(+), 13 deletions(-) create mode 100644 tests/fixtures/server.php diff --git a/composer.json b/composer.json index c59cfe9..76b19a2 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,8 @@ "symfony/twig-bundle": "^5.4|^6.3|^7.0", "twig/twig": "^2.15|^3.0", "symfony/options-resolver": "^5.4|^6.3|^7.0", - "phpunit/phpunit": "^9.6" + "phpunit/phpunit": "^9.6", + "symfony/panther": "^2.2" }, "minimum-stability": "dev", "autoload": { diff --git a/src/DynamicFormBuilder.php b/src/DynamicFormBuilder.php index adf2a73..b22369c 100644 --- a/src/DynamicFormBuilder.php +++ b/src/DynamicFormBuilder.php @@ -130,24 +130,91 @@ public function clearDataOnTransformationError(FormEvent $event): void } private function executeReadyCallbacks(array $availableDependencyData, string $eventName): void + { + $hasChanges = true; + $maxIterations = 10; + $iteration = 0; + + while ($hasChanges && $iteration < $maxIterations) { + $hasChanges = false; + $iteration++; + + // First pass: handle removals and reset dependent callbacks + foreach ($this->dependentFieldConfigs as $dependentFieldConfig) { + if ($dependentFieldConfig->isReady($availableDependencyData, $eventName)) { + $dynamicField = $dependentFieldConfig->execute($availableDependencyData, $eventName); + $name = $dependentFieldConfig->name; + $fieldExisted = $this->form->has($name); + + if (!$dynamicField->shouldBeAdded()) { + if ($fieldExisted) { + $this->form->remove($name); + $hasChanges = true; + + // Reset callbacks for fields that depend on this removed field + $this->resetDependentCallbacks($name, $eventName); + } + continue; + } + + if (!$fieldExisted) { + $this->builder->add($name, $dynamicField->getType(), $dynamicField->getOptions()); + $this->initializeListeners([$name]); + // auto initialize mimics FormBuilder::getForm() behavior + $field = $this->builder->get($name)->setAutoInitialize(false)->getForm(); + $this->form->add($field); + $hasChanges = true; + } + } + } + + // If we had changes, we need to re-evaluate all dependencies that might now be invalid + if ($hasChanges) { + $this->validateAndRemoveOrphanedFields($eventName); + } + } + } + + /** + * Remove fields that should no longer exist because their dependencies are missing + */ + private function validateAndRemoveOrphanedFields(string $eventName): void { foreach ($this->dependentFieldConfigs as $dependentFieldConfig) { - if ($dependentFieldConfig->isReady($availableDependencyData, $eventName)) { - $dynamicField = $dependentFieldConfig->execute($availableDependencyData, $eventName); - $name = $dependentFieldConfig->name; + $name = $dependentFieldConfig->name; + + // If the field exists in the form, check if it should still exist + if ($this->form->has($name)) { + $hasAllDependencies = true; + + foreach ($dependentFieldConfig->dependencies as $dependency) { + // Check if the dependency field exists and has appropriate data + if (!$this->form->has($dependency)) { + $hasAllDependencies = false; + break; + } + } - if (!$dynamicField->shouldBeAdded()) { + // If dependencies are missing, remove the field and reset its callback + if (!$hasAllDependencies) { $this->form->remove($name); + $dependentFieldConfig->callbackExecuted[$eventName] = false; - continue; + // Reset callbacks for fields that depend on this removed field + $this->resetDependentCallbacks($name, $eventName); } + } + } + } - $this->builder->add($name, $dynamicField->getType(), $dynamicField->getOptions()); - - $this->initializeListeners([$name]); - // auto initialize mimics FormBuilder::getForm() behavior - $field = $this->builder->get($name)->setAutoInitialize(false)->getForm(); - $this->form->add($field); + /** + * Reset callback execution status for fields that depend on a removed field + */ + private function resetDependentCallbacks(string $removedFieldName, string $eventName): void + { + foreach ($this->dependentFieldConfigs as $dependentFieldConfig) { + if (in_array($removedFieldName, $dependentFieldConfig->dependencies)) { + $dependentFieldConfig->callbackExecuted[$eventName] = false; } } } diff --git a/tests/FunctionalTest.php b/tests/FunctionalTest.php index 6f065ee..ed8f3ba 100644 --- a/tests/FunctionalTest.php +++ b/tests/FunctionalTest.php @@ -82,6 +82,45 @@ public function testDynamicFields() ; } + public function testRecursiveDynamicFields() + { + $browser = $this->pantherBrowser(); + $browser->visit('/form-pizza-selected') + // check for the hidden field + ->assertSeeElement('#test_dynamic_form___dynamic_error') + ->assertSee('Is Form Valid: no') + ->assertSee('Pizza 🍕') + ->assertNotContains('