Skip to content

Commit

Permalink
Custom operators and filters in conditions (#19)
Browse files Browse the repository at this point in the history
  • Loading branch information
MartinMystikJonas authored Oct 14, 2021
1 parent 45cec14 commit 95e51a2
Show file tree
Hide file tree
Showing 7 changed files with 157 additions and 42 deletions.
37 changes: 33 additions & 4 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,39 @@ Complex logical expressions can be made using && (and), || (or) and brackets.
{/if}
```

You can use filters on values used in comparsions.

```
{if someArray|count > otherArray|count}
someArray has more items than otherArray
{/if}
```

### Adding Operators

You can add custom operators for use in conditions.

Adding a new Operator:

```php
$tt->addOperator("contains",
function ($operand1, $operand2) {
return strpos($operand1, $operand2) !== false;
}
);
```

### Predefined Operators

| Operator | Description |
|----------------|--------------------------------------------|
| == | Equal |
| != | Not equal |
| \> | Greater than |
| < | Less than |
| >= | Greater than or equal |
| <= | Less than or equal |

## Conditions (else)
```
{if someVarName == "SomeValue"}
Expand Down Expand Up @@ -386,10 +419,6 @@ $textTemplate->setOpenCloseTagChars("{{", "}}");

The above example will listen to `{{tag}}{{/tag}}`.

## Limitations

The logic-Expression-Parser won't handle logic connections (OR / AND) in conditions.

## Benchmark

Although the parser is build of pure regular-expressions, I tried to avoid too expensive constructions like
Expand Down
99 changes: 64 additions & 35 deletions src/TextTemplate.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
// Require only when Class is first loaded by classloader
require_once __DIR__."/buildInFilters.inc.php";
require_once __DIR__."/buildInFunctions.inc.php";
require_once __DIR__."/buildInOperators.inc.php";


class TextTemplate {
Expand All @@ -45,11 +46,13 @@ class TextTemplate {

public static $__DEFAULT_FILTER = [];
public static $__DEFAULT_FUNCTION = [];
public static $__DEFAULT_OPERATOR = [];
public static $__DEFAULT_SECTIONS = [];

private $mTemplateText;
private $mFilter = [];
private $mFunctions = [];
private $mOperators = [];

private $OC = "{";
private $OCE = "\{";
Expand All @@ -67,6 +70,7 @@ public function __construct ($text="") {
$this->mTemplateText = $text;
$this->mFilter = self::$__DEFAULT_FILTER;
$this->mFunctions = self::$__DEFAULT_FUNCTION;
$this->mOperators = self::$__DEFAULT_OPERATOR;
$this->sections = self::$__DEFAULT_SECTIONS;
}

Expand Down Expand Up @@ -137,6 +141,22 @@ public function addFunction ($functionName, callable $callback) {
return $this;
}


/**
* Register a operator you can use in conditions
*
*
* @param $operator
* @param callable $callback
*
* @return $this
*/
public function addOperator ($operator, callable $callback) {
$this->mOperators[$operator] = $callback;
return $this;
}


public function addPlugin (TextTemplatePlugin $plugin)
{
$plugin->registerPlugin($this);
Expand Down Expand Up @@ -343,30 +363,45 @@ private function _applyFilter ($filterNameAndParams, $value) {
}


private function _parseValueOfTags ($context, $match, $softFail) {
$chain = explode("|", $match);
private function _parseItemChain ($item) {
$chain = explode("|", $item);
for ($i=0; $i<count ($chain); $i++)
$chain[$i] = trim ($chain[$i]);
return $chain;
}

if ( ! in_array("raw", $chain))
$chain[] = "_DEFAULT_";

private function _getItemValueWithFilters ($context, $item, $softFail) {
$chain = $this->_parseItemChain($item);
$varName = trim (array_shift($chain));

if ($varName === "__CONTEXT__") {
$value = "\n----- __CONTEXT__ -----\n" . var_export($context, true) . "\n----- / __CONTEXT__ -----\n";
} else {
$value = $this->_getValueByName($context, $varName, $softFail);
$value = $this->_getItemValue($context, $varName, $softFail);
}

foreach ($chain as $curName) {
echo json_encode($value) ."\n";
$value = $this->_applyFilter($curName, $value);
}
echo json_encode($value) ."\n";

return $value;
}


private function _parseValueOfTags ($context, $item, $softFail) {
$value = $this->_getItemValueWithFilters($context, $item, $softFail);

$chain = $this->_parseItemChain($item);
if ( ! in_array("raw", $chain)) {
$value = $this->_applyFilter("_DEFAULT_", $value);
}

return $value;
}


private function _runFor (&$context, $content, $cmdParam, $softFail) {
if ( ! preg_match ('/([a-z0-9\.\_]+) in ([a-z0-9\.\_]+)/i', $cmdParam, $matches)) {
Expand Down Expand Up @@ -405,7 +440,7 @@ private function _runFor (&$context, $content, $cmdParam, $softFail) {
}


private function _getItemValue ($compName, $context, $softFail) {
private function _getItemValue ($context, $compName, $softFail) {
if (preg_match ('/^("|\')(.*?)\1$/i', $compName, $matches))
return $matches[2]; // string Value
if (is_numeric($compName)) {
Expand All @@ -423,18 +458,10 @@ private function _getItemValue ($compName, $context, $softFail) {

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'");
if(!isset($this->mOperators[$operator])) {
throw new TemplateParsingException("Unknown operator: '$operator'");
}
return $this->mOperators[$operator]($operand1, $operand2);
}


Expand All @@ -445,26 +472,28 @@ private function _compareValues($operand1, $operand2, $operator)
* @param $softFail
* @return bool|string
*/
private function _evaluateCondition($expression, $context, $softFail)
private function _evaluateCondition($context, $expression, $softFail)
{
if(!preg_match('/(([\"\']?.*?[\"\']?)\s*(==|<|>|!=)\s*([\"\']?.*[\"\']?)|((!?)\s*(.*)))/i', $expression, $matches)) {
$operatorsRegExp = implode("|", array_filter(array_keys($this->mOperators), "preg_quote"));

if(!preg_match('/(([\"\']?.*?[\"\']?)\s*(' . $operatorsRegExp .')\s*([\"\']?.*[\"\']?)|((!?)\s*(.*)))/i', $expression, $matches)) {
throw new TemplateParsingException("Invalid expression: '$expression'");
}
if(count($matches) == 8) {
$comp1 = $this->_getItemValue(trim($matches[7]), $context, $softFail);
$comp1 = $this->_getItemValueWithFilters($context, trim($matches[7]), $softFail);
$operator = '==';
$comp2 = $matches[6] ? false : true; // ! prefix
} elseif(count($matches) == 5) {
$comp1 = $this->_getItemValue(trim($matches[2]), $context, $softFail);
$comp1 = $this->_getItemValueWithFilters($context, trim($matches[2]), $softFail);
$operator = trim($matches[3]);
$comp2 = $this->_getItemValue(trim($matches[4]), $context, $softFail);
$comp2 = $this->_getItemValueWithFilters($context, trim($matches[4]), $softFail);
} else {
throw new TemplateParsingException("Invalid expression: '$expression'");
}
return $this->_compareValues($comp1, $comp2, $operator);
}

private function _interpretExpressionValue(&$expressionComponents, &$index, $context, $softFail, $depth = 0) {
private function _interpretExpressionValue($context, &$expressionComponents, &$index, $softFail, $depth = 0) {
if($index >= count($expressionComponents)) {
throw new TemplateParsingException("Unexpected end of expression.");
}
Expand All @@ -476,16 +505,16 @@ private function _interpretExpressionValue(&$expressionComponents, &$index, $con
throw new TemplateParsingException("Unexpected '$component' instead of value.");
case "(":
$index++;
return $this->_interpretExpression($expressionComponents, $index, $context, $softFail, $depth + 1);
return $this->_interpretExpression($context, $expressionComponents, $index, $softFail, $depth + 1);
case "!(":
$index++;
return !$this->_interpretExpression($expressionComponents, $index, $context, $softFail, $depth + 1);
return !$this->_interpretExpression($context, $expressionComponents, $index, $softFail, $depth + 1);
default:
return $this->_evaluateCondition($component, $context, $softFail);
return $this->_evaluateCondition($context, $component, $softFail);
}
}

private function _interpretExpression(&$expressionComponents, &$index, $context, $softFail, $depth = 0) {
private function _interpretExpression($context, &$expressionComponents, &$index, $softFail, $depth = 0) {
$value = null;
while($index < count($expressionComponents)) {
$component = $expressionComponents[$index];
Expand All @@ -495,14 +524,14 @@ private function _interpretExpression(&$expressionComponents, &$index, $context,
throw new TemplateParsingException("Unexpected '$component'.");
}
$index++;
$value = $this->_interpretExpressionValue($expressionComponents, $index, $context, $softFail, $depth) && $value;
$value = $this->_interpretExpressionValue($context, $expressionComponents, $index, $softFail, $depth) && $value;
break;
case "||":
if($value === null) {
throw new TemplateParsingException("Unexpected '$component'.");
}
$index++;
$value = $this->_interpretExpressionValue($expressionComponents, $index, $context, $softFail, $depth) || $value;
$value = $this->_interpretExpressionValue($context, $expressionComponents, $index, $softFail, $depth) || $value;
break;
case ")":
if($depth == 0) {
Expand All @@ -513,7 +542,7 @@ private function _interpretExpression(&$expressionComponents, &$index, $context,
if($value !== null) {
throw new TemplateParsingException("Unexpected '$component'.");
}
$value = $this->_interpretExpressionValue($expressionComponents, $index, $context, $softFail, $depth);
$value = $this->_interpretExpressionValue($context, $expressionComponents, $index, $softFail, $depth);
}
$index++;
}
Expand All @@ -531,7 +560,7 @@ private function _interpretExpression(&$expressionComponents, &$index, $context,
* @param $softFail
* @return bool|string
*/
private function _evaluateConditionExpression($expression, $context, $softFail)
private function _evaluateConditionExpression($context, $expression, $softFail)
{
$expression = preg_replace("/\s+/", " ", $expression);
$expression = preg_replace("/!\s+\(/", "!(", $expression);
Expand All @@ -544,7 +573,7 @@ private function _evaluateConditionExpression($expression, $context, $softFail)
);
$index = 0;
try {
return $this->_interpretExpression($expressionComponents, $index, $context, $softFail);
return $this->_interpretExpression($context,$expressionComponents, $index, $softFail);
} catch(TemplateParsingException $e) {
throw new TemplateParsingException("Error parsing expression '$expression': " . $e->getMessage(), 0, $e);
}
Expand Down Expand Up @@ -574,7 +603,7 @@ private function _runIf (&$context, $content, $cmdParam, $softFail, &$ifConditio
$ifConditionDidMatch = false;
}

if ( ! $this->_evaluateConditionExpression($cmdParam, $context, $softFail)) {
if ( ! $this->_evaluateConditionExpression($context, $cmdParam, $softFail)) {
return "";
}

Expand Down Expand Up @@ -631,7 +660,7 @@ private function _parseFunctionParameters ($cmdParam, &$context, $softFail)
{
$paramArr = [];
$cmdParamRest = preg_replace_callback('/(?<name>[a-z0-9_]+)\s*=\s*(?<sval>((\"|\')(.*?)\4)|[a-z0-9\.\_]+)/i', function ($matches) use(&$paramArr, &$context, $softFail) {
$paramArr[$matches["name"]] = $this->_getItemValue($matches["sval"], $context, $softFail);
$paramArr[$matches["name"]] = $this->_getItemValue($context, $matches["sval"], $softFail);
}, $cmdParam);

$retAs = null;
Expand Down Expand Up @@ -660,7 +689,7 @@ private function _parseBlock (&$context, $block, $softFail) {
$result = preg_replace_callback("/({$this->OCE}(?!=)((?<bcommand>if|for|{$bCommands})(?<bnestingLevel>[0-9]+))(?<bcmdParam>.*?){$this->CCE}(?<bcontent>.*?)\\n?{$this->OCE}\/\\2{$this->CCE}|{$this->OCE}(?!=)(?<command>[a-z]+)\s*(?<cmdParam>.*?){$this->CCE}|{$this->OCE}\=(?<value>.+?){$this->CCE})/ism",
function ($matches) use (&$context, $softFail) {
if (isset ($matches["value"]) && $matches["value"] != null) {
return $this->_parseValueOfTags($context, $matches["value"], $softFail);
return $this-> _parseValueOfTags($context, $matches["value"], $softFail);
} else if (isset ($matches["bcommand"]) && $matches["bcommand"] != null) {

// Block-Commands
Expand Down
35 changes: 35 additions & 0 deletions src/buildInOperators.inc.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php
/**
* Created by PhpStorm.
* User: matthes
* Date: 11.10.17
* Time: 08:16
*/


namespace Leuffen\TextTemplate;


TextTemplate::$__DEFAULT_OPERATOR["=="] = function ($operand1, $operand2) {
return $operand1 == $operand2;
};

TextTemplate::$__DEFAULT_OPERATOR["!="] = function ($operand1, $operand2) {
return $operand1 != $operand2;
};

TextTemplate::$__DEFAULT_OPERATOR[">"] = function ($operand1, $operand2) {
return $operand1 > $operand2;
};

TextTemplate::$__DEFAULT_OPERATOR["<"] = function ($operand1, $operand2) {
return $operand1 < $operand2;
};

TextTemplate::$__DEFAULT_OPERATOR[">="] = function ($operand1, $operand2) {
return $operand1 >= $operand2;
};

TextTemplate::$__DEFAULT_OPERATOR["<="] = function ($operand1, $operand2) {
return $operand1 <= $operand2;
};
17 changes: 17 additions & 0 deletions test/operator.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php
namespace Leuffen\TextTemplate;

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


use Tester\Assert;



\Tester\Environment::setup();


$p = new TextTemplate('{if text contains "Hello"}Hello{/if}{if text contains "Bye"}Bye{/if}');
$p->addOperator("contains", function ($operand1, $operand2) { return strpos($operand1, $operand2) !== false; });
Assert::equal("Hello", $p->apply(["text" => "Hello world"]));

3 changes: 2 additions & 1 deletion test/unit/tpls/02_basic_if/_in.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
"falseVal" => false,
"value1" => "Some Value",
"floatVal" => 1.234,
"emptyVal" => null
"emptyVal" => null,
"arrayVal" => ["A", "B", "C"]
];
5 changes: 4 additions & 1 deletion test/unit/tpls/02_basic_if/_in.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,7 @@
{if emptyVal}
8: Some empty string
{/if}
9: Last text
{if arrayVal|count > 2}
9: Array has more than 2 items
{/if}
10: Last text
3 changes: 2 additions & 1 deletion test/unit/tpls/02_basic_if/out.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@
6: falseVal is false
7: Yet another text
8: Some not empty string
9: Last text
9: Array has more than 2 items
10: Last text

0 comments on commit 95e51a2

Please sign in to comment.