N°8772 - dynamic form

This commit is contained in:
Benjamin Dalsass
2025-11-12 08:20:17 +01:00
parent a448f668bc
commit 7fd27913f4
9 changed files with 261 additions and 39 deletions

View File

@@ -491,6 +491,7 @@ return array(
'Combodo\\iTop\\Forms\\Block\\DataModel\\AttributeChoiceFormBlock' => $baseDir . '/sources/Forms/Block/DataModel/AttributeChoiceFormBlock.php',
'Combodo\\iTop\\Forms\\Block\\DataModel\\AttributeValueChoiceFormBlock' => $baseDir . '/sources/Forms/Block/DataModel/AttributeValueChoiceFormBlock.php',
'Combodo\\iTop\\Forms\\Block\\DataModel\\OqlFormBlock' => $baseDir . '/sources/Forms/Block/DataModel/OqlFormBlock.php',
'Combodo\\iTop\\Forms\\Block\\Expression\\ExpressionFormBlock' => $baseDir . '/sources/Forms/Block/Expression/ExpressionFormBlock.php',
'Combodo\\iTop\\Forms\\Block\\FormBlockException' => $baseDir . '/sources/Forms/Block/FormBlockException.php',
'Combodo\\iTop\\Forms\\Block\\FormBlockIOException' => $baseDir . '/sources/Forms/Block/IO/FormBlockIOException.php',
'Combodo\\iTop\\Forms\\Block\\FormType\\ChoiceFormType' => $baseDir . '/sources/Forms/Block/FormType/ChoiceFormType.php',
@@ -523,6 +524,7 @@ return array(
'Combodo\\iTop\\Forms\\FormsException' => $baseDir . '/sources/Forms/FormsException.php',
'Combodo\\iTop\\Forms\\IFormBlock' => $baseDir . '/sources/Forms/IFormBlock.php',
'Combodo\\iTop\\Forms\\Twig\\Extension\\FormCompatibilityExtension' => $baseDir . '/sources/Forms/Twig/Extension/FormCompatibilityExtension.php',
'Combodo\\iTop\\ItopSdkFormDemonstrator\\Form\\Block\\ExpressionFormBlock' => $baseDir . '/sources/Forms/Block/Base/ExpressionFormBlock.php',
'Combodo\\iTop\\PhpParser\\Evaluation\\PhpExpressionEvaluator' => $baseDir . '/sources/PhpParser/Evaluation/PhpExpressionEvaluator.php',
'Combodo\\iTop\\Renderer\\BlockRenderer' => $baseDir . '/sources/Renderer/BlockRenderer.php',
'Combodo\\iTop\\Renderer\\Bootstrap\\BsFieldRendererMappings' => $baseDir . '/sources/Renderer/Bootstrap/BsFieldRendererMappings.php',

View File

@@ -872,6 +872,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'Combodo\\iTop\\Forms\\Block\\DataModel\\AttributeChoiceFormBlock' => __DIR__ . '/../..' . '/sources/Forms/Block/DataModel/AttributeChoiceFormBlock.php',
'Combodo\\iTop\\Forms\\Block\\DataModel\\AttributeValueChoiceFormBlock' => __DIR__ . '/../..' . '/sources/Forms/Block/DataModel/AttributeValueChoiceFormBlock.php',
'Combodo\\iTop\\Forms\\Block\\DataModel\\OqlFormBlock' => __DIR__ . '/../..' . '/sources/Forms/Block/DataModel/OqlFormBlock.php',
'Combodo\\iTop\\Forms\\Block\\Expression\\ExpressionFormBlock' => __DIR__ . '/../..' . '/sources/Forms/Block/Expression/ExpressionFormBlock.php',
'Combodo\\iTop\\Forms\\Block\\FormBlockException' => __DIR__ . '/../..' . '/sources/Forms/Block/FormBlockException.php',
'Combodo\\iTop\\Forms\\Block\\FormBlockIOException' => __DIR__ . '/../..' . '/sources/Forms/Block/IO/FormBlockIOException.php',
'Combodo\\iTop\\Forms\\Block\\FormType\\ChoiceFormType' => __DIR__ . '/../..' . '/sources/Forms/Block/FormType/ChoiceFormType.php',
@@ -904,6 +905,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'Combodo\\iTop\\Forms\\FormsException' => __DIR__ . '/../..' . '/sources/Forms/FormsException.php',
'Combodo\\iTop\\Forms\\IFormBlock' => __DIR__ . '/../..' . '/sources/Forms/IFormBlock.php',
'Combodo\\iTop\\Forms\\Twig\\Extension\\FormCompatibilityExtension' => __DIR__ . '/../..' . '/sources/Forms/Twig/Extension/FormCompatibilityExtension.php',
'Combodo\\iTop\\ItopSdkFormDemonstrator\\Form\\Block\\ExpressionFormBlock' => __DIR__ . '/../..' . '/sources/Forms/Block/Base/ExpressionFormBlock.php',
'Combodo\\iTop\\PhpParser\\Evaluation\\PhpExpressionEvaluator' => __DIR__ . '/../..' . '/sources/PhpParser/Evaluation/PhpExpressionEvaluator.php',
'Combodo\\iTop\\Renderer\\BlockRenderer' => __DIR__ . '/../..' . '/sources/Renderer/BlockRenderer.php',
'Combodo\\iTop\\Renderer\\Bootstrap\\BsFieldRendererMappings' => __DIR__ . '/../..' . '/sources/Renderer/Bootstrap/BsFieldRendererMappings.php',

View File

@@ -26,6 +26,7 @@ class CheckboxFormBlock extends AbstractTypeFormBlock
return CheckboxType::class;
}
/** @inheritdoc */
function InitBlockOptions(array &$aUserOptions): void
{
parent::InitBlockOptions($aUserOptions);

View File

@@ -0,0 +1,96 @@
<?php
/*
* @copyright Copyright (C) 2010-2025 Combodo SAS
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\ItopSdkFormDemonstrator\Form\Block;
use Combodo\iTop\Forms\Block\AbstractFormBlock;
use Combodo\iTop\Forms\Block\IO\Format\BooleanIOFormat;
use Combodo\iTop\Forms\Block\IO\Format\RawFormat;
use Combodo\iTop\Forms\FormsException;
use IssueLog;
use Symfony\Component\Form\FormEvents;
class ExpressionFormBlock extends AbstractFormBlock
{
const OUTPUT_RESULT = "result";
public function InitBlockOptions(array &$aUserOptions): void
{
parent::InitBlockOptions($aUserOptions);
}
public function InitOutputs(): void
{
parent::InitOutputs();
$this->AddOutput(self::OUTPUT_RESULT, BooleanIOFormat::class);
// $this->AddOutput(self::OUTPUT_RAW, RawFormat::class);
}
// public function InputHasChanged()
// {
// if (!$this->IsInputsDataReady()) {
// return;
// }
// $sExpression = $this->GetOptions()['expression'];
// $sValue = preg_replace_callback(
// "/\[\[(?<input>[^\]]+)]]/",
// function(array $aMatches): string {
// return $this->GetInput($aMatches['input'])->GetValue();
// },
// $sExpression);
//
// foreach ($this->GetInputs() as $oFormInput) {
// IssueLog::Info($oFormInput->GetName().' = '.$oFormInput->GetValue());
// }
// IssueLog::Info("Result of [$sExpression] is [$sValue]");
//
// $result = '';
// eval('$result = '.$sValue.';');
// IssueLog::Info("Result of [$sExpression] is eval to [$result]");
//
// $this->GetOutput(self::OUTPUT_RESULT)->SetValue(FormEvents::POST_SUBMIT, new BooleanIOFormat($result));
// $this->GetOutput(self::OUTPUT_VALUE)->SetValue(FormEvents::POST_SUBMIT, new RawFormat($result));
// }
public function InputHasChanged()
{
if (!$this->IsInputsDataReady()) {
return;
}
$sExpression = $this->GetOptions()['expression'];
$this->Compute($sExpression, FormEvents::POST_SET_DATA);
$this->Compute($sExpression, FormEvents::POST_SUBMIT);
}
public function Compute(string $sExpression, string $sEventType): void
{
try{
$sValue = preg_replace_callback(
"/\[\[(?<input>[^\]]+)]]/",
function(array $aMatches) use ($sEventType): ?string {
$oInput = $this->GetInput($aMatches['input']);
if(!$oInput->HasEventValue($sEventType)){
throw new FormsException('Unable to compute expression: input '.$aMatches['input'].' has no value for event '.$sEventType.'.');
}
return $oInput->GetValue($sEventType);
},
$sExpression);
$result = '';
eval('$result = '.$sValue.';');
$this->GetOutput(self::OUTPUT_RESULT)->SetValue($sEventType, new BooleanIOFormat($result));
$this->GetOutput(self::OUTPUT_VALUE)->SetValue($sEventType, new RawFormat($result));
}
catch(\Exception $e){
IssueLog::Exception('Compute expression block issue', $e);
}
}
}

View File

@@ -0,0 +1,96 @@
<?php
/*
* @copyright Copyright (C) 2010-2025 Combodo SAS
* @license http://opensource.org/licenses/AGPL-3.0
*/
namespace Combodo\iTop\Forms\Block\Expression;
use Combodo\iTop\Forms\Block\AbstractFormBlock;
use Combodo\iTop\Forms\Block\IO\Format\BooleanIOFormat;
use Combodo\iTop\Forms\Block\IO\Format\RawFormat;
use Combodo\iTop\Forms\FormsException;
use IssueLog;
use Symfony\Component\Form\FormEvents;
class ExpressionFormBlock extends AbstractFormBlock
{
const OUTPUT_RESULT = "result";
public function InitBlockOptions(array &$aUserOptions): void
{
parent::InitBlockOptions($aUserOptions);
}
public function InitOutputs(): void
{
parent::InitOutputs();
$this->AddOutput(self::OUTPUT_RESULT, BooleanIOFormat::class);
// $this->AddOutput(self::OUTPUT_RAW, RawFormat::class);
}
// public function InputHasChanged()
// {
// if (!$this->IsInputsDataReady()) {
// return;
// }
// $sExpression = $this->GetOptions()['expression'];
// $sValue = preg_replace_callback(
// "/\[\[(?<input>[^\]]+)]]/",
// function(array $aMatches): string {
// return $this->GetInput($aMatches['input'])->GetValue();
// },
// $sExpression);
//
// foreach ($this->GetInputs() as $oFormInput) {
// IssueLog::Info($oFormInput->GetName().' = '.$oFormInput->GetValue());
// }
// IssueLog::Info("Result of [$sExpression] is [$sValue]");
//
// $result = '';
// eval('$result = '.$sValue.';');
// IssueLog::Info("Result of [$sExpression] is eval to [$result]");
//
// $this->GetOutput(self::OUTPUT_RESULT)->SetValue(FormEvents::POST_SUBMIT, new BooleanIOFormat($result));
// $this->GetOutput(self::OUTPUT_VALUE)->SetValue(FormEvents::POST_SUBMIT, new RawFormat($result));
// }
public function InputHasChanged()
{
if (!$this->IsInputsDataReady()) {
return;
}
$sExpression = $this->GetOptions()['expression'];
$this->Compute($sExpression, FormEvents::POST_SET_DATA);
$this->Compute($sExpression, FormEvents::POST_SUBMIT);
}
public function Compute(string $sExpression, string $sEventType): void
{
try{
$sValue = preg_replace_callback(
"/\[\[(?<input>[^\]]+)]]/",
function(array $aMatches) use ($sEventType): ?string {
$oInput = $this->GetInput($aMatches['input']);
if(!$oInput->HasEventValue($sEventType)){
throw new FormsException('Unable to compute expression: input '.$aMatches['input'].' has no value for event '.$sEventType.'.');
}
return $oInput->GetValue($sEventType);
},
$sExpression);
$result = '';
eval('$result = '.$sValue.';');
$this->GetOutput(self::OUTPUT_RESULT)->SetValue($sEventType, new BooleanIOFormat($result));
$this->GetOutput(self::OUTPUT_VALUE)->SetValue($sEventType, new RawFormat($result));
}
catch(\Exception $e){
IssueLog::Exception('Compute expression block issue', $e);
}
}
}

View File

@@ -245,5 +245,14 @@ class AbstractFormIO
return $this->HasValue();
}
public function HasBindingOut(): bool
{
return count($this->aBindingsToInputs) > 0;
}
public function GetBindingsToInputs(): array
{
return $this->aBindingsToInputs;
}
}

View File

@@ -26,8 +26,8 @@ class DependencyHandler
/** @var DependencyMap dependencies map */
private DependencyMap $oDependenciesMap;
/** @var array Debug data */
private array $aDebugData = [];
/** @var array events */
private array $aEvents = [];
private readonly string $sName;
private readonly AbstractFormBlock $oFormBlock;
private readonly FormBuilder $oFormBuilder;
@@ -94,12 +94,8 @@ class DependencyHandler
continue;
}
$this->aDebugData[] = [
'builder' => $this->oFormBuilder->getName(),
'event' => 'form.listen',
'form' => $sOutputBlockName,
'value' => 'NA',
];
// Add event
$this->AddEvent('form.listen', $sOutputBlockName);
// Listen the output block POST_SET_DATA & POST_SUBMIT
$this->oFormBuilder->get($sOutputBlockName)->addEventListener(FormEvents::POST_SET_DATA, $this->GetEventListeningCallback());
@@ -120,12 +116,8 @@ class DependencyHandler
// Get the event type
$sEventType = FormHelper::GetEventType($oEvent);
$this->aDebugData[] = [
'builder' => $this->oFormBuilder->getName(),
'event' => $sEventType,
'form' => $oEvent->getForm()->getName(),
'value' => $oEvent->getData(),
];
// Add event
$this->AddEvent($sEventType, $oEvent->getForm()->getName(), $oEvent->getData());
// Get the form
$oForm = $oEvent->getForm();
@@ -176,9 +168,13 @@ class DependencyHandler
if ($oDependentBlock->IsVisible($sEventType) && $oDependentBlock->IsInputsDataReady($sEventType)) {
// Get the dependent field options
$aBefore = $oDependentBlock->GetOptionsMergedWithDynamic();
$oDependentBlock->UpdateDynamicOptions($sEventType);
$aOptions = $oDependentBlock->GetOptionsMergedWithDynamic($sEventType);
// Options changed flag
$bOptionsChanged = FormHelper::CompareArrayValues($aBefore, $aOptions);
// Add the listener callback to the dependent field if it is also a dependency for another field
if ($this->oDependenciesMap->IsTheBlockInDependencies($oDependentBlock->getName())) {
@@ -188,22 +184,12 @@ class DependencyHandler
]);
}
if ($oDependentBlock->AllowAdd($sEventType)) {
$this->aDebugData[] = [
'builder' => $this->oFormBuilder->getName(),
'event' => 'form.add',
'form' => $oDependentBlock->getName(),
'value' => 'NA',
];
if ( (!$oDependentBlock->IsAdded() || $bOptionsChanged) && $oDependentBlock->AllowAdd($sEventType)) {
// Add events
$this->AddEvent('form.add', $oDependentBlock->getName());
if (array_key_exists('builder_listener', $aOptions)) {
$this->aDebugData[] = [
'builder' => $this->oFormBuilder->getName(),
'event' => 'form.listen.after',
'form' => $oDependentBlock->getName(),
'value' => 'NA',
];
$this->AddEvent('form.listen.after', $oDependentBlock->getName());
}
// Mark the dependency as added
@@ -220,12 +206,8 @@ class DependencyHandler
$oForm->remove($oDependentBlock->GetName());
$oDependentBlock->SetAdded(false);
$this->aDebugData[] = [
'builder' => $this->oFormBuilder->getName(),
'event' => 'form.remove',
'form' => $oDependentBlock->getName(),
'value' => 'NA'
];
// Add event
$this->AddEvent('form.remove', $oDependentBlock->getName());
}
}
@@ -239,7 +221,7 @@ class DependencyHandler
*/
public function GetDebugData(): array
{
return $this->aDebugData;
return $this->aEvents;
}
public function GetMap(): DependencyMap
@@ -247,13 +229,20 @@ class DependencyHandler
return $this->oDependenciesMap;
}
public function GetSubBlocks(): array
{
return $this->aSubBlocks;
}
public function GetName(): string
{
return $this->sName;
}
private function AddEvent(string $sEvent, string $sForm, mixed $oValue = 'NA'): void
{
$this->aEvents[] = [
'builder' => $this->oFormBuilder->getName(),
'event' => $sEvent,
'form' => $sForm,
'value' => $oValue,
];
}
}

View File

@@ -23,6 +23,9 @@ class DependencyMap
/** @var array array of blocks with dependencies group by dependence */
private array $aBlocksWithDependenciesGroupByDependence = [];
/** @var array array of binding */
private array $aBindings = [];
/** @var array array of binding (OUT > OUT) grouped by block and output name */
private array $aBindingsOutputToInput = [];
@@ -148,6 +151,7 @@ class DependencyMap
// add to map
$map[$sBlockName][$sIOName][] = $oBinding;
$this->aBindings[] = $oBinding;
}
/**
@@ -240,4 +244,9 @@ class DependencyMap
{
return $this->aBindingsOutputToOutputs;
}
public function GetBindings()
{
return $this->aBindings;
}
}

View File

@@ -30,4 +30,22 @@ class FormHelper
throw new FormBuilderException(sprintf('Unknown event type %s', get_class($event)));
}
public static function CompareArrayValues($mValue1, $mValue2): int
{
if (is_array($mValue1) && is_array($mValue2)) {
if (count($mValue1) !== count($mValue2)) {
return 1;
}
$aDiff = array_udiff_assoc($mValue1, $mValue2, [FormHelper::class, 'CompareArrayValues']);
return count($aDiff);
}
if ($mValue1 === $mValue2) {
return 0;
}
return 1;
}
}