1
|
|
|
<?php |
2
|
|
|
declare(strict_types=1); |
3
|
|
|
namespace TYPO3Fluid\Fluid\Core\Parser\SyntaxTree; |
4
|
|
|
|
5
|
|
|
/* |
6
|
|
|
* This file belongs to the package "TYPO3 Fluid". |
7
|
|
|
* See LICENSE.txt that was shipped with this package. |
8
|
|
|
*/ |
9
|
|
|
|
10
|
|
|
use TYPO3Fluid\Fluid\Component\AbstractComponent; |
11
|
|
|
use TYPO3Fluid\Fluid\Component\ComponentInterface; |
12
|
|
|
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface; |
13
|
|
|
|
14
|
|
|
/** |
15
|
|
|
* A node which is used inside boolean arguments |
16
|
|
|
*/ |
17
|
|
|
class BooleanNode extends AbstractComponent |
18
|
|
|
{ |
19
|
|
|
protected $escapeOutput = false; |
20
|
|
|
|
21
|
|
|
protected $combiners = ['&&', '||', 'AND', 'OR', 'and', 'or', '&', '|', 'xor', 'XOR']; |
22
|
|
|
|
23
|
|
|
/** |
24
|
|
|
* @var mixed |
25
|
|
|
*/ |
26
|
|
|
protected $value; |
27
|
|
|
|
28
|
|
|
/** |
29
|
|
|
* @param mixed $input NodeInterface, array (of nodes or expression parts) or a simple type that can be evaluated to boolean |
30
|
|
|
*/ |
31
|
|
|
public function __construct($input = null) |
32
|
|
|
{ |
33
|
|
|
// First, evaluate everything that is not an ObjectAccessorNode, ArrayNode |
34
|
|
|
// or ViewHelper so we get all text, numbers, comparators and |
35
|
|
|
// groupers from the text parts of the expression. All other nodes |
36
|
|
|
// we leave intact for later processing |
37
|
|
|
if ($input !== null) { |
38
|
|
|
$this->value = is_string($input) ? trim($input) : $input; |
39
|
|
|
} |
40
|
|
|
} |
41
|
|
|
|
42
|
|
|
public function addChild(ComponentInterface $component): ComponentInterface |
43
|
|
|
{ |
44
|
|
|
if ($component instanceof TextNode || $component instanceof RootNode && $component->isQuoted()) { |
45
|
|
|
$this->children[] = $component; |
46
|
|
|
} else { |
47
|
|
|
parent::addChild($component); |
48
|
|
|
} |
49
|
|
|
return $this; |
|
|
|
|
50
|
|
|
} |
51
|
|
|
|
52
|
|
|
public function flatten(bool $extractNode = false) |
53
|
|
|
{ |
54
|
|
|
if ($extractNode && $this->children[0] instanceof TextNode && count($this->children) === 1) { |
55
|
|
|
return $this->convertToBoolean($this->children[0]->getText()); |
|
|
|
|
56
|
|
|
} |
57
|
|
|
return $this; |
58
|
|
|
} |
59
|
|
|
|
60
|
|
|
public function evaluate(RenderingContextInterface $renderingContext): bool |
61
|
|
|
{ |
62
|
|
|
$combiner = null; |
63
|
|
|
$x = null; |
64
|
|
|
$parts = []; |
65
|
|
|
$negated = false; |
66
|
|
|
|
67
|
|
|
foreach ($this->getChildren() as $part) { |
68
|
|
|
$quoted = false; |
69
|
|
|
if ($part instanceof RootNode) { |
70
|
|
|
$quoted = $part->isQuoted(); |
71
|
|
|
$part = $part->flatten(true); |
72
|
|
|
} |
73
|
|
|
|
74
|
|
|
if ($part instanceof TextNode) { |
75
|
|
|
$part = $part->getText(); |
76
|
|
|
} |
77
|
|
|
|
78
|
|
|
if ($part === '!') { |
79
|
|
|
$negated = true; |
80
|
|
|
continue; |
81
|
|
|
} |
82
|
|
|
|
83
|
|
|
|
84
|
|
|
if ($quoted) { |
85
|
|
|
// Quoted expression parts must always be cast to string |
86
|
|
|
$part = (string) $part; |
87
|
|
|
} elseif (is_string($part)) { |
88
|
|
|
// If not quoted the value may be numeric or a hardcoded true/false (not quoted string) |
89
|
|
|
$lowered = strtolower($part); |
90
|
|
|
if ($lowered === 'true') { |
91
|
|
|
$part = true; |
92
|
|
|
} elseif ($lowered === 'false') { |
93
|
|
|
$part = false; |
94
|
|
|
} elseif (is_numeric($lowered)) { |
95
|
|
|
$part = $part + 0; |
96
|
|
|
} elseif (in_array($part, $this->combiners, true)) { |
97
|
|
|
// And/or encountered. Evaluate parts so far and assign left value. |
98
|
|
|
|
99
|
|
|
$evaluatedParts = $this->evaluateParts($parts, $renderingContext); |
100
|
|
|
$parts = []; |
101
|
|
|
if ($combiner !== null && $x !== null) { |
102
|
|
|
// We must evaluate any parts collected so var |
103
|
|
|
$x = $this->evaluateAndOr($x, $evaluatedParts, $combiner); |
104
|
|
|
$combiner = null; |
105
|
|
|
} else { |
106
|
|
|
$x = $evaluatedParts; |
107
|
|
|
$combiner = $part; |
108
|
|
|
} |
109
|
|
|
|
110
|
|
|
if ($negated) { |
111
|
|
|
$x = !$x; |
112
|
|
|
$negated = false; |
113
|
|
|
} |
114
|
|
|
|
115
|
|
|
if (($x === false && ($part === '&&' || $part === 'AND' || $part === 'and')) || ($x === true && ($part === '||' || $part === 'OR' || $part === 'or'))) { |
116
|
|
|
// If $x is false and condition is AND, or $x is true and condition is OR, then no more |
117
|
|
|
// evaluation is required and we can return $x now. |
118
|
|
|
return $x; |
119
|
|
|
} |
120
|
|
|
continue; |
121
|
|
|
} |
122
|
|
|
} |
123
|
|
|
|
124
|
|
|
$parts[] = $part; |
125
|
|
|
} |
126
|
|
|
|
127
|
|
|
if (!empty($parts)) { |
128
|
|
|
$evaluatedParts = $this->evaluateParts($parts, $renderingContext); |
129
|
|
|
if ($combiner !== null) { |
130
|
|
|
return $this->evaluateAndOr($x, $evaluatedParts, $combiner); |
131
|
|
|
} |
132
|
|
|
return $negated ? !$evaluatedParts : $evaluatedParts; |
133
|
|
|
} |
134
|
|
|
|
135
|
|
|
$value = $this->value instanceof ComponentInterface ? $this->value->evaluate($renderingContext) : $this->value; |
136
|
|
|
return $this->convertToBoolean($value); |
137
|
|
|
} |
138
|
|
|
|
139
|
|
|
protected function evaluateAndOr($x, $y, string $combiner): bool |
140
|
|
|
{ |
141
|
|
|
switch ($combiner) { |
142
|
|
|
|
143
|
|
|
case '||'; |
144
|
|
|
case 'or': |
145
|
|
|
case 'OR': |
146
|
|
|
return $x || $y; |
147
|
|
|
|
148
|
|
|
case '&': |
149
|
|
|
return (bool) ((int) $x & (int) $y); |
150
|
|
|
|
151
|
|
|
case '|': |
152
|
|
|
return (bool) ((int) $x | (int) $y); |
153
|
|
|
|
154
|
|
|
case 'xor': |
155
|
|
|
case 'XOR': |
156
|
|
|
return (bool) ((int) $x XOR (int) $y); |
157
|
|
|
|
158
|
|
|
case '&&': |
159
|
|
|
case 'and': |
160
|
|
|
case 'AND': |
161
|
|
|
default: |
162
|
|
|
return $x && $y; |
163
|
|
|
} |
164
|
|
|
} |
165
|
|
|
|
166
|
|
|
protected function evaluateParts(array $parts, RenderingContextInterface $renderingContext): bool |
167
|
|
|
{ |
168
|
|
|
$numberOfParts = count($parts); |
169
|
|
|
$x = null; |
|
|
|
|
170
|
|
|
if ($numberOfParts === 3) { |
171
|
|
|
// Reduce the verdict to one entry in $parts if we've collected enough to evaluate. |
172
|
|
|
// Future loops may re- |
173
|
|
|
$x = $parts[0] instanceof ComponentInterface ? $parts[0]->evaluate($renderingContext) : $parts[0]; |
174
|
|
|
$y = $parts[2] instanceof ComponentInterface ? $parts[2]->evaluate($renderingContext) : $parts[2]; |
175
|
|
|
$comparator = (string) ($parts[1] instanceof ComponentInterface ? $parts[1]->evaluate($renderingContext) : $parts[1]); |
176
|
|
|
return $this->evaluateCompare($x, $y, $comparator); |
177
|
|
|
} elseif ($numberOfParts === 1) { |
178
|
|
|
return $this->convertToBoolean( |
179
|
|
|
$parts[0] instanceof ComponentInterface ? $parts[0]->evaluate($renderingContext) : $parts[0] |
180
|
|
|
); |
181
|
|
|
} |
182
|
|
|
return !empty($parts); |
183
|
|
|
} |
184
|
|
|
|
185
|
|
|
/** |
186
|
|
|
* Compare two variables based on a specified comparator |
187
|
|
|
* |
188
|
|
|
* @param mixed $x |
189
|
|
|
* @param mixed $y |
190
|
|
|
* @param string $comparator |
191
|
|
|
* @return bool |
192
|
|
|
*/ |
193
|
|
|
protected function evaluateCompare($x, $y, string $comparator): bool |
194
|
|
|
{ |
195
|
|
|
// enforce strong comparison for comparing two objects |
196
|
|
|
if ($comparator === '==' && is_object($x) && is_object($y)) { |
197
|
|
|
$comparator = '==='; |
198
|
|
|
} elseif ($comparator === '!=' && is_object($x) && is_object($y)) { |
199
|
|
|
$comparator = '!=='; |
200
|
|
|
} |
201
|
|
|
|
202
|
|
|
switch ($comparator) { |
203
|
|
|
case '==': |
204
|
|
|
$x = ($x == $y); |
205
|
|
|
break; |
206
|
|
|
|
207
|
|
|
case '===': |
208
|
|
|
$x = ($x === $y); |
209
|
|
|
break; |
210
|
|
|
|
211
|
|
|
case '!=': |
212
|
|
|
$x = ($x != $y); |
213
|
|
|
break; |
214
|
|
|
|
215
|
|
|
case '!==': |
216
|
|
|
$x = ($x !== $y); |
217
|
|
|
break; |
218
|
|
|
|
219
|
|
|
case '<=': |
220
|
|
|
$x = ($x <= $y); |
221
|
|
|
break; |
222
|
|
|
|
223
|
|
|
case '>=': |
224
|
|
|
$x = ($x >= $y); |
225
|
|
|
break; |
226
|
|
|
|
227
|
|
|
case '<': |
228
|
|
|
$x = ($x < $y); |
229
|
|
|
break; |
230
|
|
|
|
231
|
|
|
case '>': |
232
|
|
|
$x = ($x > $y); |
233
|
|
|
break; |
234
|
|
|
|
235
|
|
|
case '%': |
236
|
|
|
if (!is_numeric($x) || !is_numeric($y)) { |
237
|
|
|
$x = 0; |
238
|
|
|
} else { |
239
|
|
|
$x = (($x + 0) % ($y + 0)); |
240
|
|
|
} |
241
|
|
|
|
242
|
|
|
break; |
243
|
|
|
} |
244
|
|
|
|
245
|
|
|
return (bool) $x; |
246
|
|
|
} |
247
|
|
|
|
248
|
|
|
/** |
249
|
|
|
* Convert argument strings to their equivalents. Needed to handle strings with a boolean meaning. |
250
|
|
|
* |
251
|
|
|
* Must be public and static as it is used from inside cached templates. |
252
|
|
|
* |
253
|
|
|
* @param mixed $value Value to be converted to boolean |
254
|
|
|
* @return boolean |
255
|
|
|
*/ |
256
|
|
|
protected function convertToBoolean($value): bool |
257
|
|
|
{ |
258
|
|
|
if (is_string($value)) { |
259
|
|
|
return (strtolower($value) !== 'false' && !empty($value)); |
260
|
|
|
} elseif (is_array($value)) { |
261
|
|
|
return !empty($value); |
262
|
|
|
} elseif ($value instanceof \Countable) { |
263
|
|
|
return count($value) > 0; |
264
|
|
|
} |
265
|
|
|
return (bool) $value; |
266
|
|
|
} |
267
|
|
|
} |
268
|
|
|
|
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.