From 45cec1489b9fd55ae467e4ba024a4d8ca4972259 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Mystik=20Jon=C3=A1=C5=A1?= Date: Wed, 13 Oct 2021 17:56:01 +0200 Subject: [PATCH] Support for complex logic (#18) --- docs/README.md | 17 ++- src/TextTemplate.php | 168 +++++++++++++++++++----- test/complex-logic.phpt | 111 ++++++++++++++++ test/unit/tpls/16_complex_logic/_in.php | 6 + test/unit/tpls/16_complex_logic/_in.txt | 106 +++++++++++++++ test/unit/tpls/16_complex_logic/out.txt | 35 +++++ 6 files changed, 406 insertions(+), 37 deletions(-) create mode 100644 test/complex-logic.phpt create mode 100644 test/unit/tpls/16_complex_logic/_in.php create mode 100644 test/unit/tpls/16_complex_logic/_in.txt create mode 100644 test/unit/tpls/16_complex_logic/out.txt diff --git a/docs/README.md b/docs/README.md index abb5fab..91a70c0 100644 --- a/docs/README.md +++ b/docs/README.md @@ -134,7 +134,22 @@ Shortcut: Test if a variable is null: {/if} ``` -Limitation: Logical connections like OR / AND are not possible at the moment. Maybe in the future. +Complex logical expressions can be made using && (and), || (or) and brackets. + +``` +{if someVarName && otherVarName} + someVarName and otherVarName are set! +{/if} +{if someVarName || otherVarName} + someVarName or otherVarName are set! +{/if} +{if someVarName || (otherVarName && anotherVarName)} + Condition is true! +{/if} +{if someVarName && !(otherVarName && anotherVarName)} + Condition is true! +{/if} +``` ## Conditions (else) ``` diff --git a/src/TextTemplate.php b/src/TextTemplate.php index be93d0a..356a1ea 100644 --- a/src/TextTemplate.php +++ b/src/TextTemplate.php @@ -421,10 +421,138 @@ private function _getItemValue ($compName, $context, $softFail) { } - private function _runIf (&$context, $content, $cmdParam, $softFail, &$ifConditionDidMatch) { - //echo $cmdParam; - $doIf = false; + private function _compareValues($operand1, $operand2, $operator) + { + switch($operator) { + case "==": + return ($operand1 == $operand2); + case "!=": + return ($operand1 != $operand2); + case "<": + return ($operand1 < $operand2); + case ">": + return ($operand1 > $operand2); + default: + throw new TemplateParsingException("Unknown operator: '$operator'"); + } + } + + + /** + * @param $cmdParam + * @param $matches + * @param $context + * @param $softFail + * @return bool|string + */ + private function _evaluateCondition($expression, $context, $softFail) + { + if(!preg_match('/(([\"\']?.*?[\"\']?)\s*(==|<|>|!=)\s*([\"\']?.*[\"\']?)|((!?)\s*(.*)))/i', $expression, $matches)) { + throw new TemplateParsingException("Invalid expression: '$expression'"); + } + if(count($matches) == 8) { + $comp1 = $this->_getItemValue(trim($matches[7]), $context, $softFail); + $operator = '=='; + $comp2 = $matches[6] ? false : true; // ! prefix + } elseif(count($matches) == 5) { + $comp1 = $this->_getItemValue(trim($matches[2]), $context, $softFail); + $operator = trim($matches[3]); + $comp2 = $this->_getItemValue(trim($matches[4]), $context, $softFail); + } else { + throw new TemplateParsingException("Invalid expression: '$expression'"); + } + return $this->_compareValues($comp1, $comp2, $operator); + } + + private function _interpretExpressionValue(&$expressionComponents, &$index, $context, $softFail, $depth = 0) { + if($index >= count($expressionComponents)) { + throw new TemplateParsingException("Unexpected end of expression."); + } + $component = $expressionComponents[$index]; + switch($component) { + case "&&": + case "||": + case ")": + throw new TemplateParsingException("Unexpected '$component' instead of value."); + case "(": + $index++; + return $this->_interpretExpression($expressionComponents, $index, $context, $softFail, $depth + 1); + case "!(": + $index++; + return !$this->_interpretExpression($expressionComponents, $index, $context, $softFail, $depth + 1); + default: + return $this->_evaluateCondition($component, $context, $softFail); + } + } + + private function _interpretExpression(&$expressionComponents, &$index, $context, $softFail, $depth = 0) { + $value = null; + while($index < count($expressionComponents)) { + $component = $expressionComponents[$index]; + switch($component) { + case "&&": + if($value === null) { + throw new TemplateParsingException("Unexpected '$component'."); + } + $index++; + $value = $this->_interpretExpressionValue($expressionComponents, $index, $context, $softFail, $depth) && $value; + break; + case "||": + if($value === null) { + throw new TemplateParsingException("Unexpected '$component'."); + } + $index++; + $value = $this->_interpretExpressionValue($expressionComponents, $index, $context, $softFail, $depth) || $value; + break; + case ")": + if($depth == 0) { + throw new TemplateParsingException("Unexpected '$component'. No matching opening '('."); + } + return $value; + default: + if($value !== null) { + throw new TemplateParsingException("Unexpected '$component'."); + } + $value = $this->_interpretExpressionValue($expressionComponents, $index, $context, $softFail, $depth); + } + $index++; + } + if($depth != 0) { + throw new TemplateParsingException("Unmatched '('."); + } + return $value; + } + + /** + * @param $cmdParam + * @param $matches + * @param $context + * @param $softFail + * @return bool|string + */ + private function _evaluateConditionExpression($expression, $context, $softFail) + { + $expression = preg_replace("/\s+/", " ", $expression); + $expression = preg_replace("/!\s+\(/", "!(", $expression); + // separators: &&, ||, !(, (, ) + $expressionComponents = preg_split( + "/\s*(&&|\|\||!\(|\(|\))\s*/", + $expression, + 0, + PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE + ); + $index = 0; + try { + return $this->_interpretExpression($expressionComponents, $index, $context, $softFail); + } catch(TemplateParsingException $e) { + throw new TemplateParsingException("Error parsing expression '$expression': " . $e->getMessage(), 0, $e); + } + } + + + private function _runIf (&$context, $content, $cmdParam, $softFail, &$ifConditionDidMatch) + { $cmdParam = trim ($cmdParam); //echo "\n+ $cmdParam " . strpos($cmdParam, "::NL_ELSE_FALSE"); // Handle {else}{elseif} constructions @@ -446,38 +574,7 @@ private function _runIf (&$context, $content, $cmdParam, $softFail, &$ifConditio $ifConditionDidMatch = false; } - if ( ! preg_match('/(([\"\']?.*?[\"\']?)\s*(==|<|>|!=)\s*([\"\']?.*[\"\']?)|((!?)\s*(.*)))/i', $cmdParam, $matches)) { - return "!! Invalid command sequence: '$cmdParam' !!"; - } - if(count($matches) == 8) { - $comp1 = $this->_getItemValue(trim($matches[7]), $context, $softFail); - $operator = '=='; - $comp2 = $matches[6] ? FALSE : TRUE; // ! prefix - } elseif(count($matches) == 5){ - $comp1 = $this->_getItemValue(trim($matches[2]), $context, $softFail); - $operator = trim($matches[3]); - $comp2 = $this->_getItemValue(trim($matches[4]), $context, $softFail); - } else { - return "!! Invalid command sequence: '$cmdParam' !!"; - } - - switch ($operator) { - case "==": - $doIf = ($comp1 == $comp2); - break; - case "!=": - $doIf = ($comp1 != $comp2); - break; - case "<": - $doIf = ($comp1 < $comp2); - break; - case ">": - $doIf = ($comp1 > $comp2); - break; - - } - - if ( ! $doIf) { + if ( ! $this->_evaluateConditionExpression($cmdParam, $context, $softFail)) { return ""; } @@ -680,5 +777,4 @@ public function apply ($params, $softFail=TRUE, &$context=[]) { return $result; } - } \ No newline at end of file diff --git a/test/complex-logic.phpt b/test/complex-logic.phpt new file mode 100644 index 0000000..180bc5f --- /dev/null +++ b/test/complex-logic.phpt @@ -0,0 +1,111 @@ + true]; + +Assert::throws(function() use ($vars) { + $in = "{if && val}{/if}"; + $tt = new TextTemplate($in); + $tt->apply($vars, false); +}, TemplateParsingException::class, "Error parsing expression '&& val': Unexpected '&&'."); + +Assert::throws(function() use ($vars) { + $in = "{if || val}{/if}"; + $tt = new TextTemplate($in); + $tt->apply($vars, false); +}, TemplateParsingException::class, "Error parsing expression '|| val': Unexpected '||'."); + +Assert::throws(function() use ($vars) { + $in = "{if val &&}{/if}"; + $tt = new TextTemplate($in); + $tt->apply($vars, false); +}, TemplateParsingException::class, "Error parsing expression 'val &&': Unexpected end of expression."); + +Assert::throws(function() use ($vars) { + $in = "{if val ||}{/if}"; + $tt = new TextTemplate($in); + $tt->apply($vars, false); +}, TemplateParsingException::class, "Error parsing expression 'val ||': Unexpected end of expression."); + +Assert::throws(function() use ($vars) { + $in = "{if (val}{/if}"; + $tt = new TextTemplate($in); + $tt->apply($vars, false); +}, TemplateParsingException::class, "Error parsing expression '(val': Unmatched '('."); + +Assert::throws(function() use ($vars) { + $in = "{if val)}{/if}"; + $tt = new TextTemplate($in); + $tt->apply($vars, false); +}, TemplateParsingException::class, "Error parsing expression 'val)': Unexpected ')'. No matching opening '('."); + +Assert::throws(function() use ($vars) { + $in = "{if !(val}{/if}"; + $tt = new TextTemplate($in); + $tt->apply($vars, false); +}, TemplateParsingException::class, "Error parsing expression '!(val': Unmatched '('."); + +Assert::throws(function() use ($vars) { + $in = "{if (val))}{/if}"; + $tt = new TextTemplate($in); + $tt->apply($vars, false); +}, TemplateParsingException::class, "Error parsing expression '(val))': Unexpected ')'. No matching opening '('."); + +Assert::throws(function() use ($vars) { + $in = "{if ((val)}{/if}"; + $tt = new TextTemplate($in); + $tt->apply($vars, false); +}, TemplateParsingException::class, "Error parsing expression '((val)': Unmatched '('."); + +Assert::throws(function() use ($vars) { + $in = "{if (!(val)}{/if}"; + $tt = new TextTemplate($in); + $tt->apply($vars, false); +}, TemplateParsingException::class, "Error parsing expression '(!(val)': Unmatched '('."); + + +Assert::throws(function() use ($vars) { + $in = "{if (val) val}{/if}"; + $tt = new TextTemplate($in); + $tt->apply($vars, false); +}, TemplateParsingException::class, "Error parsing expression '(val) val': Unexpected 'val'."); + +Assert::throws(function() use ($vars) { + $in = "{if val (val)}{/if}"; + $tt = new TextTemplate($in); + $tt->apply($vars, false); +}, TemplateParsingException::class, "Error parsing expression 'val (val)': Unexpected '('."); + +Assert::throws(function() use ($vars) { + $in = "{if (val) (val)}{/if}"; + $tt = new TextTemplate($in); + $tt->apply($vars, false); +}, TemplateParsingException::class, "Error parsing expression '(val) (val)': Unexpected '('."); + +Assert::throws(function() use ($vars) { + $in = "{if val && &&}{/if}"; + $tt = new TextTemplate($in); + $tt->apply($vars, false); +}, TemplateParsingException::class, "Error parsing expression 'val && &&': Unexpected '&&' instead of value."); + +Assert::throws(function() use ($vars) { + $in = "{if val && ||}{/if}"; + $tt = new TextTemplate($in); + $tt->apply($vars, false); +}, TemplateParsingException::class, "Error parsing expression 'val && ||': Unexpected '||' instead of value."); + +Assert::throws(function() use ($vars) { + $in = "{if val && )}{/if}"; + $tt = new TextTemplate($in); + $tt->apply($vars, false); +}, TemplateParsingException::class, "Error parsing expression 'val && )': Unexpected ')' instead of value."); diff --git a/test/unit/tpls/16_complex_logic/_in.php b/test/unit/tpls/16_complex_logic/_in.php new file mode 100644 index 0000000..a2d6d5f --- /dev/null +++ b/test/unit/tpls/16_complex_logic/_in.php @@ -0,0 +1,6 @@ + true, + "no" => false +]; \ No newline at end of file diff --git a/test/unit/tpls/16_complex_logic/_in.txt b/test/unit/tpls/16_complex_logic/_in.txt new file mode 100644 index 0000000..0e4907f --- /dev/null +++ b/test/unit/tpls/16_complex_logic/_in.txt @@ -0,0 +1,106 @@ +{if (yes)} +(1)=1 +{/if} +{if (no)} +(0)=0 +{/if} +{if !(yes)} +!(1)=0 +{/if} +{if !(no)} +!(0)=1 +{/if} + + +{if yes && yes} +1&1=1 +{/if} +{if yes && no} +1&0=0 +{/if} +{if no && yes} +0&1=0 +{/if} +{if no && no} +0&0=0 +{/if} + + +{if yes || yes} +1|1=1 +{/if} +{if yes || no} +1|0=1 +{/if} +{if no || yes} +0|1=1 +{/if} +{if no || no} +0|0=0 +{/if} + + +{if yes && no || yes} +1&0|1=1 +{/if} +{if yes && no || no} +1&0|0=0 +{/if} +{if no && yes || no} +0&1|0=0 +{/if} +{if no && yes || yes} +0&1|1=1 +{/if} + + +{if yes && (no || yes)} +1&(0|1)=1 +{/if} +{if yes && (no || no)} +1&(0|0)=0 +{/if} + + +{if no || (yes && yes)} +0|(1&1)=1 +{/if} +{if no || (no && yes)} +0|(0&1)=0 +{/if} + + +{if yes && !(no || yes)} +1&!(0|1)=0 +{/if} +{if yes && !(no || no)} +1&!(0|0)=1 +{/if} + + +{if no || !(yes && yes)} +0|!(1&1)=0 +{/if} +{if no || !(no && yes)} +0|!(0&1)=1 +{/if} + + +{if (yes)} +A +{/if} +{if (yes)} +B +{/if} +{if (yes && yes)} +C +{/if} +{if ((yes && yes))} +D +{/if} +{if (((yes) && (yes)))} +E +{/if} +{if (((no) && (yes)) || ((yes) && (yes)))} +F +{/if} diff --git a/test/unit/tpls/16_complex_logic/out.txt b/test/unit/tpls/16_complex_logic/out.txt new file mode 100644 index 0000000..41fd5d2 --- /dev/null +++ b/test/unit/tpls/16_complex_logic/out.txt @@ -0,0 +1,35 @@ + +(1)=1 +!(0)=1 + + +1&1=1 + + +1|1=1 +1|0=1 +0|1=1 + + +1&0|1=1 +0&1|1=1 + + +1&(0|1)=1 + + +0|(1&1)=1 + + +1&!(0|0)=1 + + +0|!(0&1)=1 + + +A +B +C +D +E +F