Completed
Pull Request — master (#396)
by Claus
02:22
created

MathExpressionNode   A

Complexity

Total Complexity 25

Size/Duplication

Total Lines 118
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 1

Importance

Changes 0
Metric Value
dl 0
loc 118
rs 10
c 0
b 0
f 0
wmc 25
lcom 1
cbo 1

3 Methods

Rating   Name   Duplication   Size   Complexity  
B evaluateExpression() 0 36 7
B evaluateOperation() 0 30 11
B coerceNumericValue() 0 17 7
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
        $firstPart = static::getTemplateVariableOrValueItself($result, $renderingContext);
51
        if ($firstPart === $result && !is_numeric($firstPart)) {
52
            // Pitfall: the expression part was not numeric and did not resolve to a variable. We null the
53
            // value - although this means the edge case of a variable's value being the same as its name,
54
            // results in the expression part being treated as zero. Which is different from how PHP would
55
            // coerce types in earlier versions, implying that a non-numeric string just counts as "1".
56
            // Here, it counts as zero with the intention of error prevention on undeclared variables.
57
            // Note that the same happens in the loop below.
58
            $firstPart = null;
59
        }
60
        $result = $firstPart;
61
        $operator = null;
62
        $operators = ['*', '^', '-', '+', '/', '%'];
63
        foreach ($matches[0] as $part) {
64
            if (in_array($part, $operators)) {
65
                $operator = $part;
66
            } else {
67
                $newPart = static::getTemplateVariableOrValueItself($part, $renderingContext);
68
                if ($newPart === $part && !is_numeric($part)) {
69
                    $newPart = null;
70
                }
71
                $result = self::evaluateOperation($result, $operator, $newPart);
72
            }
73
        }
74
        return $result;
75
    }
76
77
    /**
78
     * @param integer|float $left
79
     * @param string $operator
80
     * @param integer|float $right
81
     * @return integer|float
82
     */
83
    protected static function evaluateOperation($left, $operator, $right)
84
    {
85
        // Special case: the "+" operator can be used with two arrays which will combine the two arrays. But it is
86
        // only allowable if both sides are in fact arrays and only for this one operator. Please see PHP documentation
87
        // about "union" on https://secure.php.net/manual/en/language.operators.array.php for specific behavior!
88
        if ($operator === '+' && is_array($left) && is_array($right)) {
89
            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...
90
        }
91
92
        // Guard: if left or right side are not numeric values, infer a value for the expression part based on how
93
        // PHP would coerce types in versions that are not strict typed. We do this to avoid fatal PHP errors about
94
        // encountering non-numeric values.
95
        $left = static::coerceNumericValue($left);
96
        $right = static::coerceNumericValue($right);
97
98
        if ($operator === '%') {
99
            return $left % $right;
100
        } elseif ($operator === '-') {
101
            return $left - $right;
102
        } elseif ($operator === '+') {
103
            return $left + $right;
104
        } elseif ($operator === '*') {
105
            return $left * $right;
106
        } elseif ($operator === '/') {
107
            return (integer) $right !== 0 ? $left / $right : 0;
108
        } elseif ($operator === '^') {
109
            return pow($left, $right);
110
        }
111
        return 0;
112
    }
113
114
    protected static function coerceNumericValue($value)
115
    {
116
        if (is_object($value) && method_exists($value, '__toString')) {
117
            // Delegate to another coercion call after casting to string
118
            return static::coerceNumericValue((string) $value);
119
        }
120
        if (is_null($value)) {
121
            return 0;
122
        }
123
        if (is_bool($value)) {
124
            return $value ? 1 : 0;
125
        }
126
        if (is_numeric($value)) {
127
            return $value;
128
        }
129
        return 0;
130
    }
131
}
132