Completed
Pull Request — master (#470)
by Claus
01:32
created

BooleanNode::convertToBoolean()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
nc 5
nop 1
dl 0
loc 11
rs 9.6111
c 0
b 0
f 0
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;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this; (TYPO3Fluid\Fluid\Core\Pa...\SyntaxTree\BooleanNode) is incompatible with the return type declared by the interface TYPO3Fluid\Fluid\Compone...nentInterface::addChild of type self.

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:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
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());
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->convertToB...hildren[0]->getText()); (boolean) is incompatible with the return type declared by the interface TYPO3Fluid\Fluid\Compone...onentInterface::flatten of type TYPO3Fluid\Fluid\Compone...ing|integer|double|null.

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:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
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;
0 ignored issues
show
Unused Code introduced by
$x is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
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