Skip to content

Commit

Permalink
Support for complex logic (#18)
Browse files Browse the repository at this point in the history
  • Loading branch information
MartinMystikJonas authored Oct 13, 2021
1 parent fd77c2a commit 45cec14
Show file tree
Hide file tree
Showing 6 changed files with 406 additions and 37 deletions.
17 changes: 16 additions & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
```
Expand Down
168 changes: 132 additions & 36 deletions src/TextTemplate.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 "";
}

Expand Down Expand Up @@ -680,5 +777,4 @@ public function apply ($params, $softFail=TRUE, &$context=[]) {
return $result;
}


}
111 changes: 111 additions & 0 deletions test/complex-logic.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<?php
namespace Leuffen\TextTemplate;

require __DIR__ . "/../vendor/autoload.php";


use Tester\Assert;



\Tester\Environment::setup();


$vars = ["val" => 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.");
6 changes: 6 additions & 0 deletions test/unit/tpls/16_complex_logic/_in.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?php

return [
"yes" => true,
"no" => false
];
Loading

0 comments on commit 45cec14

Please sign in to comment.