Completed
Pull Request — master (#457)
by Claus
02:40
created

AbstractConditionViewHelper::initializeArguments()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 0
dl 0
loc 5
rs 10
c 0
b 0
f 0
1
<?php
2
namespace TYPO3Fluid\Fluid\Core\ViewHelper;
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\Compiler\TemplateCompiler;
10
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\NodeInterface;
11
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\ViewHelperNode;
12
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
13
use TYPO3Fluid\Fluid\ViewHelpers\ElseViewHelper;
14
use TYPO3Fluid\Fluid\ViewHelpers\ThenViewHelper;
15
16
/**
17
 * This view helper is an abstract ViewHelper which implements an if/else condition.
18
 *
19
 * = Usage =
20
 *
21
 * To create a custom Condition ViewHelper, you need to subclass this class, and
22
 * implement your own render() method. Inside there, you should call $this->renderThenChild()
23
 * if the condition evaluated to TRUE, and $this->renderElseChild() if the condition evaluated
24
 * to FALSE.
25
 *
26
 * Every Condition ViewHelper has a "then" and "else" argument, so it can be used like:
27
 * <[aConditionViewHelperName] .... then="condition true" else="condition false" />,
28
 * or as well use the "then" and "else" child nodes.
29
 *
30
 * @see TYPO3Fluid\Fluid\ViewHelpers\IfViewHelper for a more detailed explanation and a simple usage example.
31
 * Make sure to NOT OVERRIDE the constructor.
32
 *
33
 * @api
34
 */
35
abstract class AbstractConditionViewHelper extends AbstractViewHelper
36
{
37
38
    /**
39
     * @var boolean
40
     */
41
    protected $escapeOutput = false;
42
43
    /**
44
     * Initializes the "then" and "else" arguments
45
     */
46
    public function initializeArguments()
47
    {
48
        $this->registerArgument('then', 'mixed', 'Value to be returned if the condition if met.', false);
49
        $this->registerArgument('else', 'mixed', 'Value to be returned if the condition if not met.', false);
50
    }
51
52
    /**
53
     * Renders <f:then> child if $condition is true, otherwise renders <f:else> child.
54
     * Method which only gets called if the template is not compiled. For static calling,
55
     * the then/else nodes are converted to closures and condition evaluation closures.
56
     *
57
     * @return string the rendered string
58
     * @api
59
     */
60
    public function render()
61
    {
62
        if (static::verdict($this->arguments, $this->renderingContext)) {
63
            return $this->renderThenChild();
64
        }
65
        return $this->renderElseChild();
66
    }
67
68
    /**
69
     * @param array $arguments
70
     * @param \Closure $renderChildrenClosure
71
     * @param RenderingContextInterface $renderingContext
72
     * @return mixed
73
     */
74
    public static function renderStatic(array $arguments, \Closure $renderChildrenClosure, RenderingContextInterface $renderingContext)
75
    {
76
        if (static::verdict($arguments, $renderingContext)) {
77
            if (isset($arguments['then'])) {
78
                return $arguments['then'];
79
            }
80
            if (isset($arguments['__thenClosure'])) {
81
                return $arguments['__thenClosure']();
82
            }
83
        } elseif (!empty($arguments['__elseClosures'])) {
84
            $elseIfClosures = isset($arguments['__elseifClosures']) ? $arguments['__elseifClosures'] : [];
85
            return static::evaluateElseClosures($arguments['__elseClosures'], $elseIfClosures, $renderingContext);
0 ignored issues
show
Bug introduced by
Since evaluateElseClosures() is declared private, calling it with static will lead to errors in possible sub-classes. You can either use self, or increase the visibility of evaluateElseClosures() to at least protected.

Let’s assume you have a class which uses late-static binding:

class YourClass
{
    private static function getTemperature() {
        return "3422 °C";
}

public static function getSomeVariable()
{
    return static::getTemperature();
}

}

The code above will run fine in your PHP runtime. However, if you now create a sub-class and call the getSomeVariable() on that sub-class, you will receive a runtime error:

class YourSubClass extends YourClass {
      private static function getTemperature() {
        return "-182 °C";
    }
}

print YourSubClass::getSomeVariable(); // Will cause an access error.

In the case above, it makes sense to update SomeClass to use self instead:

class YourClass
{
    private static function getTemperature() {
        return "3422 °C";
    }

    public static function getSomeVariable()
    {
        return self::getTemperature();
    }
}
Loading history...
86
        } elseif (array_key_exists('else', $arguments)) {
87
            return $arguments['else'];
88
        }
89
        return '';
90
    }
91
92
    /**
93
     * Static method which can be overridden by subclasses. If a subclass
94
     * requires a different (or faster) decision then this method is the one
95
     * to override and implement.
96
     *
97
     * @param array $arguments
98
     * @param RenderingContextInterface $renderingContext
99
     * @return bool
100
     */
101
    public static function verdict(array $arguments, RenderingContextInterface $renderingContext)
0 ignored issues
show
Unused Code introduced by
The parameter $renderingContext is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
102
    {
103
        return static::evaluateCondition($arguments);
0 ignored issues
show
Deprecated Code introduced by
The method TYPO3Fluid\Fluid\Core\Vi...er::evaluateCondition() has been deprecated with message: Deprecated in favor of ClassName::verdict($arguments, renderingContext), will no longer be called in 3.0

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
104
    }
105
106
    /**
107
     * Static method which can be overridden by subclasses. If a subclass
108
     * requires a different (or faster) decision then this method is the one
109
     * to override and implement.
110
     *
111
     * Note: method signature does not type-hint that an array is desired,
112
     * and as such, *appears* to accept any input type. There is no type hint
113
     * here for legacy reasons - the signature is kept compatible with third
114
     * party packages which depending on PHP version would error out if this
115
     * signature was not compatible with that of existing and in-production
116
     * subclasses that will be using this base class in the future. Let this
117
     * be a warning if someone considers changing this method signature!
118
     *
119
     * @deprecated Deprecated in favor of ClassName::verdict($arguments, renderingContext), will no longer be called in 3.0
120
     * @param array|NULL $arguments
121
     * @return boolean
122
     * @api
123
     */
124
    protected static function evaluateCondition($arguments = null)
125
    {
126
        return isset($arguments['condition']) && (bool)($arguments['condition']);
127
    }
128
129
    /**
130
     * @param array $closures
131
     * @param array $conditionClosures
132
     * @param RenderingContextInterface $renderingContext
133
     * @return string
134
     */
135
    private static function evaluateElseClosures(array $closures, array $conditionClosures, RenderingContextInterface $renderingContext)
0 ignored issues
show
Unused Code introduced by
The parameter $renderingContext is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
136
    {
137
        foreach ($closures as $elseNodeIndex => $elseNodeClosure) {
138
            if (!isset($conditionClosures[$elseNodeIndex])) {
139
                return $elseNodeClosure();
140
            } else {
141
                if ($conditionClosures[$elseNodeIndex]()) {
142
                    return $elseNodeClosure();
143
                }
144
            }
145
        }
146
        return '';
147
    }
148
149
    /**
150
     * Returns value of "then" attribute.
151
     * If then attribute is not set, iterates through child nodes and renders ThenViewHelper.
152
     * If then attribute is not set and no ThenViewHelper and no ElseViewHelper is found, all child nodes are rendered
153
     *
154
     * @return mixed rendered ThenViewHelper or contents of <f:if> if no ThenViewHelper was found
155
     * @api
156
     */
157
    protected function renderThenChild()
158
    {
159
        if ($this->hasArgument('then')) {
160
            return $this->arguments['then'];
161
        }
162
163
        $elseViewHelperEncountered = false;
164
        foreach (($this->viewHelperNode ?? $this)->getChildNodes() as $childNode) {
165
            if ($childNode instanceof ThenViewHelper
166
                || ($childNode instanceof ViewHelperNode && substr($childNode->getViewHelperClassName(), -14) === 'ThenViewHelper')
167
            ) {
168
                $data = $childNode->evaluate($this->renderingContext);
169
                return $data;
170
            }
171
            if ($childNode instanceof ElseViewHelper
172
                || ($childNode instanceof ViewHelperNode && substr($childNode->getViewHelperClassName(), -14) === 'ElseViewHelper')
173
            ) {
174
                $elseViewHelperEncountered = true;
175
            }
176
        }
177
178
        if ($elseViewHelperEncountered) {
179
            return '';
180
        } else {
181
            return $this->renderChildren();
182
        }
183
    }
184
185
    /**
186
     * Returns value of "else" attribute.
187
     * If else attribute is not set, iterates through child nodes and renders ElseViewHelper.
188
     * If else attribute is not set and no ElseViewHelper is found, an empty string will be returned.
189
     *
190
     * @return string rendered ElseViewHelper or an empty string if no ThenViewHelper was found
191
     * @api
192
     */
193
    protected function renderElseChild()
194
    {
195
196
        if ($this->hasArgument('else')) {
197
            return $this->arguments['else'];
198
        }
199
200
        /** @var ViewHelperNode|NULL $elseNode */
201
        $elseNode = null;
202
        foreach (($this->viewHelperNode ?? $this)->getChildNodes() as $childNode) {
203
            if ($childNode instanceof ElseViewHelper
204
                || ($childNode instanceof ViewHelperNode && substr($childNode->getViewHelperClassName(), -14) === 'ElseViewHelper')
205
            ) {
206
                $arguments = $childNode->getParsedArguments();
207
                if (isset($arguments['if'])) {
208
                    $condition = $arguments['if'];
209
                    if ($condition instanceof NodeInterface) {
210
                        $condition = $condition->evaluate($this->renderingContext);
211
                    }
212
                    if ((bool)$condition === true) {
213
                        return $childNode->evaluate($this->renderingContext);
214
                    }
215
                } else {
216
                    $elseNode = $childNode;
217
                }
218
            }
219
        }
220
221
        return $elseNode instanceof NodeInterface ? $elseNode->evaluate($this->renderingContext) : '';
222
    }
223
224
    /**
225
     * The compiled ViewHelper adds two new ViewHelper arguments: __thenClosure and __elseClosure.
226
     * These contain closures which are be executed to render the then(), respectively else() case.
227
     *
228
     * @param string $argumentsName
229
     * @param string $closureName
230
     * @param string $initializationPhpCode
231
     * @param ViewHelperNode $node
232
     * @param TemplateCompiler $compiler
233
     * @return string
234
     */
235
    public function compile($argumentsName, $closureName, &$initializationPhpCode, ViewHelperNode $node, TemplateCompiler $compiler)
236
    {
237
        $thenViewHelperEncountered = $elseViewHelperEncountered = false;
238
        foreach ($node->getChildNodes() as $childNode) {
239
            if ($childNode instanceof ViewHelperNode || $childNode instanceof ViewHelperInterface) {
240
                $viewHelperClassName = $childNode instanceof ViewHelperNode ? $childNode->getViewHelperClassName() : get_class($childNode);
241
                if (substr($viewHelperClassName, -14) === 'ThenViewHelper') {
242
                    $thenViewHelperEncountered = true;
243
                    $childNodesAsClosure = $compiler->wrapChildNodesInClosure($childNode);
244
                    $initializationPhpCode .= sprintf('%s[\'__thenClosure\'] = %s;', $argumentsName, $childNodesAsClosure) . chr(10);
245
                } elseif (substr($viewHelperClassName, -14) === 'ElseViewHelper') {
246
                    $elseViewHelperEncountered = true;
247
                    $childNodesAsClosure = $compiler->wrapChildNodesInClosure($childNode);
248
                    $initializationPhpCode .= sprintf('%s[\'__elseClosures\'][] = %s;', $argumentsName, $childNodesAsClosure) . chr(10);
249
                    $arguments = $childNode->getParsedArguments();
0 ignored issues
show
Bug introduced by
The method getParsedArguments does only exist in TYPO3Fluid\Fluid\Core\Pa...ntaxTree\ViewHelperNode, but not in TYPO3Fluid\Fluid\Core\Vi...per\ViewHelperInterface.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
250
                    if (isset($arguments['if'])) {
251
                        // The "else" has an argument, indicating it has a secondary (elseif) condition.
252
                        // Compile a closure which will evaluate the condition.
253
                        $elseIfConditionAsClosure = $compiler->wrapViewHelperNodeArgumentEvaluationInClosure($childNode, 'if');
0 ignored issues
show
Bug introduced by
It seems like $childNode defined by $childNode on line 238 can also be of type object<TYPO3Fluid\Fluid\...er\ViewHelperInterface>; however, TYPO3Fluid\Fluid\Core\Co...ntEvaluationInClosure() does only seem to accept object<TYPO3Fluid\Fluid\...taxTree\ViewHelperNode>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
254
                        $initializationPhpCode .= sprintf('%s[\'__elseifClosures\'][] = %s;', $argumentsName, $elseIfConditionAsClosure) . chr(10);
255
                    }
256
                }
257
            }
258
        }
259
        if (!$thenViewHelperEncountered && !$elseViewHelperEncountered && !isset($node->getParsedArguments()['then'])) {
260
            $initializationPhpCode .= sprintf('%s[\'__thenClosure\'] = %s;', $argumentsName, $closureName) . chr(10);
261
        }
262
        return parent::compile($argumentsName, $closureName, $initializationPhpCode, $node, $compiler);
263
    }
264
}
265