Completed
Pull Request — master (#396)
by Claus
03:37 queued 01:08
created

MathExpressionNode::evaluateOperation()   B

Complexity

Conditions 11
Paths 9

Size

Total Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 11
nc 9
nop 3
dl 0
loc 30
rs 7.3166
c 0
b 0
f 0

How to fix   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
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;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $left + $right; (array) is incompatible with the return type documented by TYPO3Fluid\Fluid\Core\Pa...Node::evaluateOperation of type integer|double.

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...
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')) {
0 ignored issues
show
Bug introduced by
The variable $object does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

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