Completed
Pull Request — master (#446)
by Claus
02:04
created

SwitchViewHelper::restoreSwitchState()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
nc 4
nop 0
dl 0
loc 9
rs 9.9666
c 0
b 0
f 0
1
<?php
2
namespace TYPO3Fluid\Fluid\ViewHelpers;
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\NodeFilterInterface;
11
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\NodeInterface;
12
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\ViewHelperNode;
13
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
14
15
/**
16
 * Switch view helper which can be used to render content depending on a value or expression.
17
 * Implements what a basic switch()-PHP-method does.
18
 *
19
 * An optional default case can be specified which is rendered if none of the "f:case" conditions matches.
20
 *
21
 * = Examples =
22
 *
23
 * <code title="Simple Switch statement">
24
 * <f:switch expression="{person.gender}">
25
 *   <f:case value="male">Mr.</f:case>
26
 *   <f:case value="female">Mrs.</f:case>
27
 *   <f:defaultCase>Mr. / Mrs.</f:defaultCase>
28
 * </f:switch>
29
 * </code>
30
 * <output>
31
 * "Mr.", "Mrs." or "Mr. / Mrs." (depending on the value of {person.gender})
32
 * </output>
33
 *
34
 * Note: Using this view helper can be a sign of weak architecture. If you end up using it extensively
35
 * you might want to consider restructuring your controllers/actions and/or use partials and sections.
36
 * E.g. the above example could be achieved with <f:render partial="title.{person.gender}" /> and the partials
37
 * "title.male.html", "title.female.html", ...
38
 * Depending on the scenario this can be easier to extend and possibly contains less duplication.
39
 *
40
 * @api
41
 */
42
class SwitchViewHelper extends AbstractViewHelper implements NodeFilterInterface
43
{
44
45
    /**
46
     * @var boolean
47
     */
48
    protected $escapeOutput = false;
49
50
    /**
51
     * @var mixed
52
     */
53
    protected $backupSwitchExpression = null;
54
55
    /**
56
     * @var boolean
57
     */
58
    protected $backupBreakState = false;
59
60
    /**
61
     * @return void
62
     */
63
    public function initializeArguments()
64
    {
65
        parent::initializeArguments();
66
        $this->registerArgument('expression', 'mixed', 'Expression to switch', true);
67
    }
68
69
    /**
70
     * @return string the rendered string
71
     * @api
72
     */
73
    public function render()
74
    {
75
        $expression = $this->arguments['expression'];
76
        $this->backupSwitchState();
77
        $variableContainer = $this->renderingContext->getViewHelperVariableContainer();
78
79
        $variableContainer->addOrUpdate(SwitchViewHelper::class, 'switchExpression', $expression);
80
        $variableContainer->addOrUpdate(SwitchViewHelper::class, 'break', false);
81
82
        $content = $this->retrieveContentFromChildNodes($this->viewHelperNode->getChildNodes());
83
84
        if ($variableContainer->exists(SwitchViewHelper::class, 'switchExpression')) {
85
            $variableContainer->remove(SwitchViewHelper::class, 'switchExpression');
86
        }
87
        if ($variableContainer->exists(SwitchViewHelper::class, 'break')) {
88
            $variableContainer->remove(SwitchViewHelper::class, 'break');
89
        }
90
91
        $this->restoreSwitchState();
92
        return $content;
93
    }
94
95
    /**
96
     * @param NodeInterface[] $childNodes
97
     * @return mixed
98
     */
99
    protected function retrieveContentFromChildNodes(array $childNodes)
100
    {
101
        $content = null;
102
        $defaultCaseViewHelperNode = null;
103
        foreach ($childNodes as $childNode) {
104
            if ($this->isDefaultCaseNode($childNode)) {
105
                $defaultCaseViewHelperNode = $childNode;
106
            }
107
            if (!$this->isCaseNode($childNode)) {
108
                continue;
109
            }
110
            $content = $childNode->evaluate($this->renderingContext);
111
            if ($this->viewHelperVariableContainer->get(SwitchViewHelper::class, 'break') === true) {
112
                $defaultCaseViewHelperNode = null;
113
                break;
114
            }
115
        }
116
117
        if ($defaultCaseViewHelperNode !== null) {
118
            $content = $defaultCaseViewHelperNode->evaluate($this->renderingContext);
119
        }
120
        return $content;
121
    }
122
123
    /**
124
     * @param NodeInterface $node
125
     * @return boolean
126
     */
127
    protected function isDefaultCaseNode(NodeInterface $node)
128
    {
129
        return ($node instanceof ViewHelperNode && $node->getViewHelperClassName() === DefaultCaseViewHelper::class);
130
    }
131
132
    /**
133
     * @param NodeInterface $node
134
     * @return boolean
135
     */
136
    protected function isCaseNode(NodeInterface $node)
137
    {
138
        return ($node instanceof ViewHelperNode && $node->getViewHelperClassName() === CaseViewHelper::class);
139
    }
140
141
    /**
142
     * Backups "switch expression" and "break" state of a possible parent switch ViewHelper to support nesting
143
     *
144
     * @return void
145
     */
146
    protected function backupSwitchState()
147
    {
148 View Code Duplication
        if ($this->renderingContext->getViewHelperVariableContainer()->exists(SwitchViewHelper::class, 'switchExpression')) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
149
            $this->backupSwitchExpression = $this->renderingContext->getViewHelperVariableContainer()->get(SwitchViewHelper::class, 'switchExpression');
150
        }
151 View Code Duplication
        if ($this->renderingContext->getViewHelperVariableContainer()->exists(SwitchViewHelper::class, 'break')) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
152
            $this->backupBreakState = $this->renderingContext->getViewHelperVariableContainer()->get(SwitchViewHelper::class, 'break');
153
        }
154
    }
155
156
    /**
157
     * Restores "switch expression" and "break" states that might have been backed up in backupSwitchState() before
158
     *
159
     * @return void
160
     */
161
    protected function restoreSwitchState()
162
    {
163
        if ($this->backupSwitchExpression !== null) {
164
            $this->renderingContext->getViewHelperVariableContainer()->addOrUpdate(SwitchViewHelper::class, 'switchExpression', $this->backupSwitchExpression);
165
        }
166
        if ($this->backupBreakState !== false) {
167
            $this->renderingContext->getViewHelperVariableContainer()->addOrUpdate(SwitchViewHelper::class, 'break', true);
168
        }
169
    }
170
171
    /**
172
     * The switch ViewHelper type only allows the "case" and "defaultCase" ViewHelper as child nodes. Anything else,
173
     * including text nodes (whitespace) is ignored and never gets compiled.
174
     *
175
     * @param NodeInterface $node
176
     * @return bool
177
     */
178
    public function allowsChildNodeType(NodeInterface $node): bool
179
    {
180
        return $this->isCaseNode($node) || $this->isDefaultCaseNode($node);
181
    }
182
183
    /**
184
     * Compiles the node structure to a native switch
185
     * statement which evaluates closures for each
186
     * case comparison and renders child node closures
187
     * only when value matches.
188
     *
189
     * @param string $argumentsName
190
     * @param string $closureName
191
     * @param string $initializationPhpCode
192
     * @param ViewHelperNode $node
193
     * @param TemplateCompiler $compiler
194
     * @return string
195
     */
196
    public function compile($argumentsName, $closureName, &$initializationPhpCode, ViewHelperNode $node, TemplateCompiler $compiler)
197
    {
198
        $phpCode = 'call_user_func_array(function($arguments) use ($renderingContext, $self) {' . PHP_EOL .
199
            'switch ($arguments[\'expression\']) {' . PHP_EOL;
200
        foreach ($node->getChildNodes() as $childNode) {
201
            if ($this->isDefaultCaseNode($childNode)) {
202
                $childrenClosure = $compiler->wrapChildNodesInClosure($childNode);
203
                $phpCode .= sprintf('default: return call_user_func(%s);', $childrenClosure) . PHP_EOL;
204
            } elseif ($this->isCaseNode($childNode)) {
205
                /** @var ViewHelperNode $childNode */
206
                $valueClosure = $compiler->wrapViewHelperNodeArgumentEvaluationInClosure($childNode, 'value');
207
                $childrenClosure = $compiler->wrapChildNodesInClosure($childNode);
208
                $phpCode .= sprintf(
209
                    'case call_user_func(%s): return call_user_func(%s);',
210
                    $valueClosure,
211
                    $childrenClosure,
212
                    $compiler->getNodeConverter()->convert($childNode)
213
                ) . PHP_EOL;
214
            }
215
        }
216
        $phpCode .= '}' . PHP_EOL;
217
        $phpCode .= sprintf('}, array(%s))', $argumentsName);
218
        return $phpCode;
219
    }
220
}
221