1
|
|
|
<?php |
2
|
|
|
namespace TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\Expression; |
3
|
|
|
|
4
|
|
|
/* |
5
|
|
|
* This file belongs to the package "TYPO3 Fluid". |
6
|
|
|
* See LICENSE.txt that was shipped with this package. |
7
|
|
|
*/ |
8
|
|
|
|
9
|
|
|
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface; |
10
|
|
|
|
11
|
|
|
/** |
12
|
|
|
* Math Expression Syntax Node - is a container for numeric values. |
13
|
|
|
*/ |
14
|
|
|
class MathExpressionNode extends AbstractExpressionNode |
15
|
|
|
{ |
16
|
|
|
|
17
|
|
|
/** |
18
|
|
|
* Pattern which detects the mathematical expressions with either |
19
|
|
|
* object accessor expressions or numbers on left and right hand |
20
|
|
|
* side of a mathematical operator inside curly braces, e.g.: |
21
|
|
|
* |
22
|
|
|
* {variable * 10}, {100 / variable}, {variable + variable2} etc. |
23
|
|
|
*/ |
24
|
|
|
public static $detectionExpression = '/ |
25
|
|
|
( |
26
|
|
|
{ # Start of shorthand syntax |
27
|
|
|
(?: # Math expression is composed of... |
28
|
|
|
[a-zA-Z0-9\.]+(?:[\s]?[*+\^\/\%\-]{1}[\s]?[a-zA-Z0-9\.]+)+ # Various math expressions left and right sides with any spaces |
29
|
|
|
|(?R) # Other expressions inside |
30
|
|
|
)+ |
31
|
|
|
} # End of shorthand syntax |
32
|
|
|
)/x'; |
33
|
|
|
|
34
|
|
|
/** |
35
|
|
|
* @param RenderingContextInterface $renderingContext |
36
|
|
|
* @param string $expression |
37
|
|
|
* @param array $matches |
38
|
|
|
* @return integer|float |
39
|
|
|
*/ |
40
|
|
|
public static function evaluateExpression(RenderingContextInterface $renderingContext, $expression, array $matches) |
41
|
|
|
{ |
42
|
|
|
// Split the expression on all recognized operators |
43
|
|
|
$matches = []; |
44
|
|
|
preg_match_all('/([+\-*\^\/\%]|[a-zA-Z0-9\.]+)/s', $expression, $matches); |
45
|
|
|
$matches[0] = array_map('trim', $matches[0]); |
46
|
|
|
// Like the BooleanNode, we dumb down the processing logic to not apply |
47
|
|
|
// any special precedence on the priority of operators. We simply process |
48
|
|
|
// them in order. |
49
|
|
|
$result = array_shift($matches[0]); |
50
|
|
|
$result = static::getTemplateVariableOrValueItself($result, $renderingContext); |
51
|
|
|
$operator = null; |
52
|
|
|
$operators = ['*', '^', '-', '+', '/', '%']; |
53
|
|
|
foreach ($matches[0] as $part) { |
54
|
|
|
if (in_array($part, $operators)) { |
55
|
|
|
$operator = $part; |
56
|
|
|
} else { |
57
|
|
|
if (!is_numeric($part)) { |
58
|
|
|
$newPart = static::getTemplateVariableOrValueItself($part, $renderingContext); |
59
|
|
|
if ($newPart === $part) { |
60
|
|
|
// Pitfall: the expression part was not numeric and did not resolve to a variable. We null the |
61
|
|
|
// value - although this means the edge case of a variable's value being the same as its name, |
62
|
|
|
// results in the expression part being treated as zero. Which is different from how PHP would |
63
|
|
|
// coerce types in earlier versions, implying that a non-numeric string just counts as "1". |
64
|
|
|
// Here, it counts as zero with the intention of error prevention on undeclared variables. |
65
|
|
|
$part = null; |
66
|
|
|
} |
67
|
|
|
} |
68
|
|
|
$result = self::evaluateOperation($result, $operator, $part); |
69
|
|
|
} |
70
|
|
|
} |
71
|
|
|
return $result; |
72
|
|
|
} |
73
|
|
|
|
74
|
|
|
/** |
75
|
|
|
* @param integer|float $left |
76
|
|
|
* @param string $operator |
77
|
|
|
* @param integer|float $right |
78
|
|
|
* @return integer|float |
79
|
|
|
*/ |
80
|
|
|
protected static function evaluateOperation($left, $operator, $right) |
81
|
|
|
{ |
82
|
|
|
// Special case: the "+" operator can be used with two arrays which will combine the two arrays. But it is |
83
|
|
|
// only allowable if both sides are in fact arrays and only for this one operator. Please see PHP documentation |
84
|
|
|
// about "union" on https://secure.php.net/manual/en/language.operators.array.php for specific behavior! |
85
|
|
|
if ($operator === '+' && is_array($left) && is_array($right)) { |
86
|
|
|
return $left + $right; |
|
|
|
|
87
|
|
|
} |
88
|
|
|
|
89
|
|
|
// Guard: if left or right side are not numeric values, infer a value for the expression part based on how |
90
|
|
|
// PHP would coerce types in versions that are not strict typed. We do this to avoid fatal PHP errors about |
91
|
|
|
// encountering non-numeric values. |
92
|
|
|
$left = static::coerceNumericValue($left); |
93
|
|
|
$right = static::coerceNumericValue($right); |
94
|
|
|
|
95
|
|
|
if ($operator === '%') { |
96
|
|
|
return $left % $right; |
97
|
|
|
} elseif ($operator === '-') { |
98
|
|
|
return $left - $right; |
99
|
|
|
} elseif ($operator === '+') { |
100
|
|
|
return $left + $right; |
101
|
|
|
} elseif ($operator === '*') { |
102
|
|
|
return $left * $right; |
103
|
|
|
} elseif ($operator === '/') { |
104
|
|
|
return (integer) $right !== 0 ? $left / $right : 0; |
105
|
|
|
} elseif ($operator === '^') { |
106
|
|
|
return pow($left, $right); |
107
|
|
|
} |
108
|
|
|
return 0; |
109
|
|
|
} |
110
|
|
|
|
111
|
|
|
protected static function coerceNumericValue($value) |
112
|
|
|
{ |
113
|
|
|
if (is_object($value) && method_exists($object, '__toString')) { |
|
|
|
|
114
|
|
|
// Delegate to another coercion call after casting to string |
115
|
|
|
return static::coerceNumericValue((string) $value); |
116
|
|
|
} |
117
|
|
|
if (is_null($value)) { |
118
|
|
|
return 0; |
119
|
|
|
} |
120
|
|
|
if (is_bool($value)) { |
121
|
|
|
return $value ? 1 : 0; |
122
|
|
|
} |
123
|
|
|
if (is_numeric($value)) { |
124
|
|
|
return $value; |
125
|
|
|
} |
126
|
|
|
return 0; |
127
|
|
|
} |
128
|
|
|
} |
129
|
|
|
|
If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.
Let’s take a look at an example:
Our function
my_function
expects aPost
object, and outputs the author of the post. The base classPost
returns a simple string and outputting a simple string will work just fine. However, the child classBlogPost
which is a sub-type ofPost
instead decided to return anobject
, and is therefore violating the SOLID principles. If aBlogPost
were passed tomy_function
, PHP would not complain, but ultimately fail when executing thestrtoupper
call in its body.