From fdc958a73407a44972b740a987fd739d6ea314d8 Mon Sep 17 00:00:00 2001 From: Eric Espie Date: Thu, 11 Dec 2025 15:57:53 +0100 Subject: [PATCH] =?UTF-8?q?N=C2=B08772=20-=20Compiler:=20Expressions=20in?= =?UTF-8?q?=20input?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/composer/autoload_classmap.php | 1 + lib/composer/autoload_static.php | 1 + .../Expression/NumberExpressionFormBlock.php | 2 +- .../Expression/StringExpressionFormBlock.php | 43 +++++++ sources/PropertyTree/AbstractProperty.php | 6 + sources/PropertyTree/Property.php | 79 ++++++++----- .../ValueType/AbstractValueType.php | 15 ++- .../Forms/Compiler/TestFormsCompiler.php | 109 +++++++++++++++--- 8 files changed, 208 insertions(+), 48 deletions(-) create mode 100644 sources/Forms/Block/Expression/StringExpressionFormBlock.php diff --git a/lib/composer/autoload_classmap.php b/lib/composer/autoload_classmap.php index 8c03bedbf..85baff5e5 100644 --- a/lib/composer/autoload_classmap.php +++ b/lib/composer/autoload_classmap.php @@ -502,6 +502,7 @@ return array( 'Combodo\\iTop\\Forms\\Block\\Expression\\AbstractExpressionFormBlock' => $baseDir . '/sources/Forms/Block/Expression/AbstractExpressionFormBlock.php', 'Combodo\\iTop\\Forms\\Block\\Expression\\BooleanExpressionFormBlock' => $baseDir . '/sources/Forms/Block/Expression/BooleanExpressionFormBlock.php', 'Combodo\\iTop\\Forms\\Block\\Expression\\NumberExpressionFormBlock' => $baseDir . '/sources/Forms/Block/Expression/NumberExpressionFormBlock.php', + 'Combodo\\iTop\\Forms\\Block\\Expression\\StringExpressionFormBlock' => $baseDir . '/sources/Forms/Block/Expression/StringExpressionFormBlock.php', 'Combodo\\iTop\\Forms\\Block\\FormBlockException' => $baseDir . '/sources/Forms/Block/FormBlockException.php', 'Combodo\\iTop\\Forms\\Block\\FormBlockHelper' => $baseDir . '/sources/Forms/Block/FormBlockHelper.php', 'Combodo\\iTop\\Forms\\Block\\FormBlockService' => $baseDir . '/sources/Forms/Block/FormBlockService.php', diff --git a/lib/composer/autoload_static.php b/lib/composer/autoload_static.php index b07ec9feb..e8bb0de1b 100644 --- a/lib/composer/autoload_static.php +++ b/lib/composer/autoload_static.php @@ -888,6 +888,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f 'Combodo\\iTop\\Forms\\Block\\Expression\\AbstractExpressionFormBlock' => __DIR__ . '/../..' . '/sources/Forms/Block/Expression/AbstractExpressionFormBlock.php', 'Combodo\\iTop\\Forms\\Block\\Expression\\BooleanExpressionFormBlock' => __DIR__ . '/../..' . '/sources/Forms/Block/Expression/BooleanExpressionFormBlock.php', 'Combodo\\iTop\\Forms\\Block\\Expression\\NumberExpressionFormBlock' => __DIR__ . '/../..' . '/sources/Forms/Block/Expression/NumberExpressionFormBlock.php', + 'Combodo\\iTop\\Forms\\Block\\Expression\\StringExpressionFormBlock' => __DIR__ . '/../..' . '/sources/Forms/Block/Expression/StringExpressionFormBlock.php', 'Combodo\\iTop\\Forms\\Block\\FormBlockException' => __DIR__ . '/../..' . '/sources/Forms/Block/FormBlockException.php', 'Combodo\\iTop\\Forms\\Block\\FormBlockHelper' => __DIR__ . '/../..' . '/sources/Forms/Block/FormBlockHelper.php', 'Combodo\\iTop\\Forms\\Block\\FormBlockService' => __DIR__ . '/../..' . '/sources/Forms/Block/FormBlockService.php', diff --git a/sources/Forms/Block/Expression/NumberExpressionFormBlock.php b/sources/Forms/Block/Expression/NumberExpressionFormBlock.php index 8df1b0b2a..920567485 100644 --- a/sources/Forms/Block/Expression/NumberExpressionFormBlock.php +++ b/sources/Forms/Block/Expression/NumberExpressionFormBlock.php @@ -12,7 +12,7 @@ use Combodo\iTop\Forms\IO\Format\NumberIOFormat; use Combodo\iTop\Forms\Register\IORegister; /** - * An abstract block to manage an expression. + * A block to manage an number expression. * This block expose a number output: the result of the expression. * * @package Combodo\iTop\Forms\Block\Expression diff --git a/sources/Forms/Block/Expression/StringExpressionFormBlock.php b/sources/Forms/Block/Expression/StringExpressionFormBlock.php new file mode 100644 index 000000000..13aed6593 --- /dev/null +++ b/sources/Forms/Block/Expression/StringExpressionFormBlock.php @@ -0,0 +1,43 @@ +AddOutput(self::OUTPUT_RESULT, StringIOFormat::class); + } + + /** + * Compute the expression and set the output values. + * + * @param string $sEventType + * + * @return mixed + * @throws \Combodo\iTop\Forms\Block\FormBlockException + * @throws \Combodo\iTop\Forms\Register\RegisterException + */ + public function ComputeExpression(string $sEventType): mixed + { + $oResult = parent::ComputeExpression($sEventType); + + // Update output + $this->GetOutput(self::OUTPUT_RESULT)->SetValue($sEventType, new StringIOFormat($oResult)); + + return $oResult; + } +} diff --git a/sources/PropertyTree/AbstractProperty.php b/sources/PropertyTree/AbstractProperty.php index a03cfbddf..a0af621a7 100644 --- a/sources/PropertyTree/AbstractProperty.php +++ b/sources/PropertyTree/AbstractProperty.php @@ -78,4 +78,10 @@ abstract class AbstractProperty return null; } + + public function QuoteForPHP(string $sValue): string + { + $sEscaped = str_replace(['\\', "'"], ['\\\\', "\\'"], $sValue); + return "'$sEscaped'"; + } } diff --git a/sources/PropertyTree/Property.php b/sources/PropertyTree/Property.php index d74e5b982..2ea67dc92 100644 --- a/sources/PropertyTree/Property.php +++ b/sources/PropertyTree/Property.php @@ -8,6 +8,13 @@ namespace Combodo\iTop\PropertyTree; use Combodo\iTop\DesignElement; +use Combodo\iTop\Forms\Block\Expression\BooleanExpressionFormBlock; +use Combodo\iTop\Forms\Block\Expression\NumberExpressionFormBlock; +use Combodo\iTop\Forms\Block\Expression\StringExpressionFormBlock; +use Combodo\iTop\Forms\IO\Format\BooleanIOFormat; +use Combodo\iTop\Forms\IO\Format\ClassIOFormat; +use Combodo\iTop\Forms\IO\Format\NumberIOFormat; +use Combodo\iTop\Forms\IO\Format\StringIOFormat; use Combodo\iTop\PropertyTree\ValueType\ValueTypeFactory; use Exception; use Expression; @@ -48,13 +55,34 @@ class Property extends AbstractProperty $sFormBlockClass = $this->oValueType->GetFormBlockClass(); $sInputs = ''; - $sRelevanceCondition = ''; - $sBinding = null; + $sPrerequisiteExpressions = ''; if (!is_null($this->sRelevanceCondition)) { + $this->GenerateInputs('visible', $this->sRelevanceCondition, $sPrerequisiteExpressions, $sInputs); + } + + foreach ($this->oValueType->GetInputValues() as $sInputName => $sValue) { + $this->GenerateInputs($sInputName, $sValue, $sPrerequisiteExpressions, $sInputs); + } + $sLabel = $this->QuoteForPHP($this->sLabel); + return <<Add('$this->sId', '$sFormBlockClass', [ + 'label' => $sLabel, + ]){$sInputs}; + +PHP; + } + + private function GenerateInputs(string $sInputName, string $sValue, string &$sPrerequisiteExpressions, string &$sInputs): void + { + if (preg_match('/^{{(?\w+)\.(?\w+)}}$/', $sValue, $aMatches) === 1) { + $sInputs .= "\n ->InputDependsOn('$sInputName', '{$aMatches['node']}', '{$aMatches['output']}')"; + } elseif (preg_match('/^{{(?.*)}}$/', $sValue, $aMatches) === 1) { + $sExpression = $aMatches['expression']; + $sBindings = ''; try { - $oExpression = Expression::FromOQL($this->sRelevanceCondition); + $oExpression = Expression::FromOQL($sExpression); } catch (Exception $e) { - throw new PropertyTreeException("Node: {$this->sId}, invalid syntax in relevance condition: ".$e->getMessage()); + throw new PropertyTreeException("Node: {$this->sId}, invalid syntax in condition: ".$e->getMessage()); } $aFieldsToResolve = array_unique($oExpression->ListRequiredFields()); foreach ($aFieldsToResolve as $sFieldToResolve) { @@ -62,42 +90,37 @@ class Property extends AbstractProperty $sNode = $aMatches['node']; $oSibling = $this->GetSibling($sNode); if (is_null($oSibling)) { - throw new PropertyTreeException("Node: {$this->sId}, invalid source in relevance condition: $sNode"); + throw new PropertyTreeException("Node: {$this->sId}, invalid source in condition: $sNode"); } $sOutput = $aMatches['output']; if (!in_array($sOutput, $oSibling->oValueType->GetOutputs())) { - throw new PropertyTreeException("Node: {$this->sId}, invalid output in relevance condition: $sFieldToResolve"); + throw new PropertyTreeException("Node: {$this->sId}, invalid output in condition: $sFieldToResolve"); } - $sBinding .= "\n ->AddInputDependsOn('{$sNode}.$sOutput', '$sNode', '$sOutput')"; + $sBindings .= "\n ->AddInputDependsOn('{$sNode}.$sOutput', '$sNode', '$sOutput')"; } else { - throw new PropertyTreeException("Node: {$this->sId}, missing output or source in relevance condition: $sFieldToResolve"); + throw new PropertyTreeException("Node: {$this->sId}, missing output or source in condition: $sFieldToResolve"); } } - $sRelevanceCondition = <<Add('{$this->sId}_relevance_condition', 'Combodo\iTop\Forms\Block\Expression\BooleanExpressionFormBlock', [ - 'expression' => "{$this->sRelevanceCondition}", - ]){$sBinding}; + $sExpressionClass = match ($this->oValueType->GetInputType($sInputName)) { + BooleanIOFormat::class => BooleanExpressionFormBlock::class, + StringIOFormat::class, ClassIOFormat::class => StringExpressionFormBlock::class, + NumberIOFormat::class => NumberExpressionFormBlock::class, + default => throw new PropertyTreeException("Node: {$this->sId}, unsupported expression for input type: $sInputName"), + }; + + $sExpression = $this->QuoteForPHP($sExpression); + $sPrerequisiteExpressions = <<Add('{$this->sId}_{$sInputName}_expression', '$sExpressionClass', [ + 'expression' => $sExpression, + ]){$sBindings}; PHP; - $sInputs .= "\n ->InputDependsOn('visible', '{$this->sId}_relevance_condition', 'result')"; + $sInputs .= "\n ->InputDependsOn('$sInputName', '{$this->sId}_{$sInputName}_expression', 'result')"; + } else { + $sInputs .= "\n ->SetInputValue('$sInputName', ".$this->QuoteForPHP($sValue).")"; } - - foreach ($this->oValueType->GetInputs() as $sInput => $sValue) { - if (preg_match("/^{{(?\w+)\.(?\w+)}}$/", $sValue, $aMatches) === 1) { - $sInputs .= "\n ->InputDependsOn('$sInput', '{$aMatches['node']}', '{$aMatches['output']}')"; - } else { - $sInputs .= "\n ->SetInputValue('$sInput', '$sValue')"; - } - } - - return <<Add('$this->sId', '$sFormBlockClass', [ - 'label' => '$this->sLabel', - ]){$sInputs}; - -PHP; } } diff --git a/sources/PropertyTree/ValueType/AbstractValueType.php b/sources/PropertyTree/ValueType/AbstractValueType.php index 0fbe61019..246204edb 100644 --- a/sources/PropertyTree/ValueType/AbstractValueType.php +++ b/sources/PropertyTree/ValueType/AbstractValueType.php @@ -8,6 +8,7 @@ namespace Combodo\iTop\PropertyTree\ValueType; use Combodo\iTop\DesignElement; +use Combodo\iTop\Forms\IO\FormInput; use utils; /** @@ -17,8 +18,10 @@ abstract class AbstractValueType { abstract public function GetFormBlockClass(): string; + /** @var FormInput[] */ protected array $aInputs = []; protected array $aOutputs = []; + protected array $aInputValues = []; public function InitFromDomNode(DesignElement $oDomNode): void { @@ -26,9 +29,10 @@ abstract class AbstractValueType $oBlockNode = new $sBlockNodeClass('foo'); foreach ($oBlockNode->GetInputs() as $oInput) { $sInputName = $oInput->GetName(); + $this->aInputs[$sInputName] = $oInput; $sInputValue = $oDomNode->GetChildText($sInputName); if (utils::IsNotNullOrEmptyString($sInputValue)) { - $this->aInputs[$sInputName] = $sInputValue; + $this->aInputValues[$sInputName] = $sInputValue; } } foreach ($oBlockNode->GetOutputs() as $oOutput) { @@ -36,9 +40,14 @@ abstract class AbstractValueType } } - public function GetInputs(): array + public function GetInputValues(): array { - return $this->aInputs; + return $this->aInputValues; + } + + public function GetInputType(string $sInputName): string + { + return $this->aInputs[$sInputName]->GetDataType(); } public function GetOutputs(): array diff --git a/tests/php-unit-tests/unitary-tests/sources/Forms/Compiler/TestFormsCompiler.php b/tests/php-unit-tests/unitary-tests/sources/Forms/Compiler/TestFormsCompiler.php index c37c84287..7e26c2bc4 100644 --- a/tests/php-unit-tests/unitary-tests/sources/Forms/Compiler/TestFormsCompiler.php +++ b/tests/php-unit-tests/unitary-tests/sources/Forms/Compiler/TestFormsCompiler.php @@ -292,6 +292,41 @@ class FormFor__input_static_test extends Combodo\iTop\Forms\Block\Base\FormBlock PHP, ], + 'Quotes should be handled gracefully' => [ + 'sXMLContent' => << + + + + + + {{CONCAT("'", '"')}} + 'Class' and "Attribute" + Test + + + + +XML, + 'sExpectedPHP' => <<Add('class_attribute_property_class_expression', 'Combodo\iTop\Forms\Block\Expression\StringExpressionFormBlock', [ + 'expression' => 'CONCAT("\'", \'"\')', + ]); + + \$this->Add('class_attribute_property', 'Combodo\iTop\Forms\Block\DataModel\AttributeChoiceFormBlock', [ + 'label' => '\'Class\' and "Attribute"', + ]) + ->InputDependsOn('class', 'class_attribute_property_class_expression', 'result') + ->SetInputValue('category', '\'Class\' and "Attribute"'); + } +} +PHP, + ], + 'Dynamic input should be bound' => [ 'sXMLContent' => << @@ -329,6 +364,48 @@ class FormFor__input_binding_test extends Combodo\iTop\Forms\Block\Base\FormBloc PHP, ], + 'Dynamic input can be an expression' => [ + 'sXMLContent' => << + + + + + + + + + + + {{IF(class_property.text = '', 'Person', class_property.text)}} + + + + +XML, + 'sExpectedPHP' => <<Add('class_property', 'Combodo\iTop\Forms\Block\Base\TextFormBlock', [ + 'label' => 'UI:Class', + ]); + + \$this->Add('class_attribute_property_class_expression', 'Combodo\iTop\Forms\Block\Expression\StringExpressionFormBlock', [ + 'expression' => 'IF(class_property.text = \'\', \'Person\', class_property.text)', + ]) + ->AddInputDependsOn('class_property.text', 'class_property', 'text'); + + \$this->Add('class_attribute_property', 'Combodo\iTop\Forms\Block\DataModel\AttributeChoiceFormBlock', [ + 'label' => 'UI:ClassAttribute', + ]) + ->InputDependsOn('class', 'class_attribute_property_class_expression', 'result'); + } +} +PHP, + ], + 'Relevance condition should generate a boolean block expression' => [ 'sXMLContent' => << @@ -341,7 +418,7 @@ PHP, - source_property.text != 'count' + {{source_property.text != 'count'}} @@ -357,15 +434,15 @@ class FormFor__RelevanceCondition extends Combodo\iTop\Forms\Block\Base\FormBloc 'label' => 'UI:Source', ]); - \$this->Add('dependant_property_relevance_condition', 'Combodo\iTop\Forms\Block\Expression\BooleanExpressionFormBlock', [ - 'expression' => "source_property.text != 'count'", + \$this->Add('dependant_property_visible_expression', 'Combodo\iTop\Forms\Block\Expression\BooleanExpressionFormBlock', [ + 'expression' => 'source_property.text != \'count\'', ]) ->AddInputDependsOn('source_property.text', 'source_property', 'text'); \$this->Add('dependant_property', 'Combodo\iTop\Forms\Block\Base\TextFormBlock', [ 'label' => 'UI:Dependant', ]) - ->InputDependsOn('visible', 'dependant_property_relevance_condition', 'result'); + ->InputDependsOn('visible', 'dependant_property_visible_expression', 'result'); } } PHP, @@ -388,7 +465,7 @@ PHP, - IF(source_a_property.text != '', source_a_property.text, source_b_property.text) + {{IF(source_a_property.text != '', source_a_property.text, source_b_property.text)}} @@ -408,8 +485,8 @@ class FormFor__ComplexRelevanceCondition extends Combodo\iTop\Forms\Block\Base\F 'label' => 'UI:Source', ]); - \$this->Add('dependant_property_relevance_condition', 'Combodo\iTop\Forms\Block\Expression\BooleanExpressionFormBlock', [ - 'expression' => "IF(source_a_property.text != '', source_a_property.text, source_b_property.text)", + \$this->Add('dependant_property_visible_expression', 'Combodo\iTop\Forms\Block\Expression\BooleanExpressionFormBlock', [ + 'expression' => 'IF(source_a_property.text != \'\', source_a_property.text, source_b_property.text)', ]) ->AddInputDependsOn('source_a_property.text', 'source_a_property', 'text') ->AddInputDependsOn('source_b_property.text', 'source_b_property', 'text'); @@ -417,7 +494,7 @@ class FormFor__ComplexRelevanceCondition extends Combodo\iTop\Forms\Block\Base\F \$this->Add('dependant_property', 'Combodo\iTop\Forms\Block\Base\TextFormBlock', [ 'label' => 'UI:Dependant', ]) - ->InputDependsOn('visible', 'dependant_property_relevance_condition', 'result'); + ->InputDependsOn('visible', 'dependant_property_visible_expression', 'result'); } } PHP, @@ -469,7 +546,7 @@ PHP, - source_property.text == 'count' + {{source_property.text == 'count'}} @@ -477,7 +554,7 @@ PHP, XML, 'sExpectedClass' => 'Combodo\iTop\PropertyTree\PropertyTreeException', - 'sExpectedMessage' => 'Node: dependant_property, invalid syntax in relevance condition: Unexpected token EQ - found \'=\' at 22 in \'source_property.text == \'count\'\'', + 'sExpectedMessage' => 'Node: dependant_property, invalid syntax in condition: Unexpected token EQ - found \'=\' at 22 in \'source_property.text == \'count\'\'', ], 'Unknown source in relevance condition' => [ @@ -487,7 +564,7 @@ XML, - source_property.text = 'count' + {{source_property.text = 'count'}} @@ -495,7 +572,7 @@ XML, XML, 'sExpectedClass' => 'Combodo\iTop\PropertyTree\PropertyTreeException', - 'sExpectedMessage' => 'Node: dependant_property, invalid source in relevance condition: source_property', + 'sExpectedMessage' => 'Node: dependant_property, invalid source in condition: source_property', ], 'Unknown output in relevance condition' => [ @@ -510,7 +587,7 @@ XML, - source_property.text_output != 'count' + {{source_property.text_output != 'count'}} @@ -518,7 +595,7 @@ XML, XML, 'sExpectedClass' => 'Combodo\iTop\PropertyTree\PropertyTreeException', - 'sExpectedMessage' => 'Node: dependant_property, invalid output in relevance condition: source_property.text_output', + 'sExpectedMessage' => 'Node: dependant_property, invalid output in condition: source_property.text_output', ], 'Missing output or source in relevance condition' => [ @@ -533,7 +610,7 @@ XML, - source_property != 'count' + {{source_property != 'count'}} @@ -541,7 +618,7 @@ XML, XML, 'sExpectedClass' => 'Combodo\iTop\PropertyTree\PropertyTreeException', - 'sExpectedMessage' => 'Node: dependant_property, missing output or source in relevance condition: source_property', + 'sExpectedMessage' => 'Node: dependant_property, missing output or source in condition: source_property', ], 'Missing value-type in node specification' => [