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

BooleanNode::evaluateCompare()   D

Complexity

Conditions 18
Paths 33

Size

Total Lines 54

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 18
nc 33
nop 3
dl 0
loc 54
rs 4.8666
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
            case 'xor':
153
            case 'XOR':
154
                return (bool) ((int) $x | (int) $y);
155
156
            case '&&':
157
            case 'and':
158
            case 'AND':
159
            default:
160
                return $x && $y;
161
        }
162
    }
163
164
    protected function evaluateParts(array $parts, RenderingContextInterface $renderingContext): bool
165
    {
166
        $numberOfParts = count($parts);
167
        $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...
168
        if ($numberOfParts === 3) {
169
            // Reduce the verdict to one entry in $parts if we've collected enough to evaluate.
170
            // Future loops may re-
171
            $x = $parts[0] instanceof ComponentInterface ? $parts[0]->evaluate($renderingContext) : $parts[0];
172
            $y = $parts[2] instanceof ComponentInterface ? $parts[2]->evaluate($renderingContext) : $parts[2];
173
            $comparator = (string) ($parts[1] instanceof ComponentInterface ? $parts[1]->evaluate($renderingContext) : $parts[1]);
174
            return $this->evaluateCompare($x, $y, $comparator);
175
        } elseif ($numberOfParts === 1) {
176
            return $this->convertToBoolean(
177
                $parts[0] instanceof ComponentInterface ? $parts[0]->evaluate($renderingContext) : $parts[0]
178
            );
179
        }
180
        return !empty($parts);
181
    }
182
183
    /**
184
     * Compare two variables based on a specified comparator
185
     *
186
     * @param mixed $x
187
     * @param mixed $y
188
     * @param string $comparator
189
     * @return bool
190
     */
191
    protected function evaluateCompare($x, $y, string $comparator): bool
192
    {
193
        // enforce strong comparison for comparing two objects
194
        if ($comparator === '==' && is_object($x) && is_object($y)) {
195
            $comparator = '===';
196
        } elseif ($comparator === '!=' && is_object($x) && is_object($y)) {
197
            $comparator = '!==';
198
        }
199
200
        switch ($comparator) {
201
            case '==':
202
                $x = ($x == $y);
203
                break;
204
205
            case '===':
206
                $x = ($x === $y);
207
                break;
208
209
            case '!=':
210
                $x = ($x != $y);
211
                break;
212
213
            case '!==':
214
                $x = ($x !== $y);
215
                break;
216
217
            case '<=':
218
                $x = ($x <= $y);
219
                break;
220
221
            case '>=':
222
                $x = ($x >= $y);
223
                break;
224
225
            case '<':
226
                $x = ($x < $y);
227
                break;
228
229
            case '>':
230
                $x = ($x > $y);
231
                break;
232
233
            case '%':
234
                if (!is_numeric($x) || !is_numeric($y)) {
235
                    $x = 0;
236
                } else {
237
                    $x = (($x + 0) % ($y + 0));
238
                }
239
240
                break;
241
        }
242
243
        return (bool) $x;
244
    }
245
246
    /**
247
     * Convert argument strings to their equivalents. Needed to handle strings with a boolean meaning.
248
     *
249
     * Must be public and static as it is used from inside cached templates.
250
     *
251
     * @param mixed $value Value to be converted to boolean
252
     * @return boolean
253
     */
254
    protected function convertToBoolean($value): bool
255
    {
256
        if (is_string($value)) {
257
            return (strtolower($value) !== 'false' && !empty($value));
258
        } elseif (is_array($value)) {
259
            return !empty($value);
260
        } elseif ($value instanceof \Countable) {
261
            return count($value) > 0;
262
        }
263
        return (bool) $value;
264
    }
265
}
266