Completed
Push — master ( 3cb0d3...960090 )
by
unknown
15:11
created

AbstractEnhancer::modifyRoutePath()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 17
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 12
nc 4
nop 1
dl 0
loc 17
rs 9.8666
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the TYPO3 CMS project.
7
 *
8
 * It is free software; you can redistribute it and/or modify it under
9
 * the terms of the GNU General Public License, either version 2
10
 * of the License, or any later version.
11
 *
12
 * For the full copyright and license information, please read the
13
 * LICENSE.txt file that was distributed with this source code.
14
 *
15
 * The TYPO3 project - inspiring people to share!
16
 */
17
18
namespace TYPO3\CMS\Core\Routing\Enhancer;
19
20
use TYPO3\CMS\Core\Routing\Aspect\AspectInterface;
21
use TYPO3\CMS\Core\Routing\Aspect\ModifiableAspectInterface;
22
use TYPO3\CMS\Core\Routing\Route;
23
24
/**
25
 * Abstract Enhancer, useful for custom enhancers
26
 */
27
abstract class AbstractEnhancer implements EnhancerInterface
28
{
29
    /**
30
     * @var AspectInterface[]
31
     */
32
    protected $aspects = [];
33
34
    /**
35
     * @var VariableProcessor
36
     */
37
    protected $variableProcessor;
38
39
    /**
40
     * @param Route $route
41
     * @param AspectInterface[] $aspects
42
     * @param string|null $namespace
43
     */
44
    protected function applyRouteAspects(Route $route, array $aspects, string $namespace = null)
45
    {
46
        if (empty($aspects)) {
47
            return;
48
        }
49
        $aspects = $this->getVariableProcessor()
50
            ->deflateKeys($aspects, $namespace, $route->getArguments());
51
        $route->setAspects($aspects);
52
    }
53
54
    /**
55
     * @param Route $route
56
     * @param array $requirements
57
     * @param string|null $namespace
58
     */
59
    protected function applyRequirements(Route $route, array $requirements, string $namespace = null)
60
    {
61
        $requirements = $this->getVariableProcessor()
62
            ->deflateKeys($requirements, $namespace, $route->getArguments());
63
        // only keep requirements that are actually part of the current route path
64
        $requirements = $this->filterValuesByPathVariables($route, $requirements);
65
        // Symfony's behavior on applying pattern for parameters just concerns values
66
        // to be passed either to URL or to internal parameters - they are always the
67
        // same, without any transformation.
68
        //
69
        // TYPO3 extends ("enhances") this behavior by making a difference between values
70
        // for generation (resulting in a URL) and matching (resulting in query parameters)
71
        // having the following implications and meaning:
72
        //
73
        // + since requirements in classic Symfony focus on parameters in URLs
74
        //   and aspects define a mapping between URL part (e.g. 'some-example-news')
75
        //   and the corresponding internal argument (e.g. 'tx_news_pi1[news]=123')
76
        // + thus, the requirement definition cannot be used for resolving and generating
77
        //   a route at the same time (it would have to be e.g. `[\w_._]+` AND `\d+`)
78
        //
79
        // Symfony's default regular expression pattern `[^/]+` (see
80
        // `RouteCompiler::compilePattern()`) has to be overridden with `.+` to
81
        // allow URI parameters like `some-example-news/january` as well.
82
        //
83
        // Existing `requirements` for TYPO3 route enhancers are not modified, only those
84
        // that are not defined and would use Symfony's default pattern.
85
        $requirements = $this->defineValuesByAspect($route, $requirements, '.+');
86
        $route->setRequirements($requirements);
87
    }
88
89
    /**
90
     * Only keeps values that actually have been used as variables in route path.
91
     *
92
     * + routePath: '/list/{page}' ('page' used as variable in route path)
93
     * + values: ['entity' => 'entity...', 'page' => 'page...', 'other' => 'other...']
94
     * + result: ['page' => 'page...']
95
     *
96
     * @param Route $route
97
     * @param array $values
98
     * @return array
99
     */
100
    protected function filterValuesByPathVariables(Route $route, array $values): array
101
    {
102
        return array_intersect_key(
103
            $values,
104
            array_flip($route->compile()->getPathVariables())
105
        );
106
    }
107
108
    /**
109
     * Overrides items having an aspect definition with a given
110
     * $overrideValue in target $targetValue array.
111
     *
112
     * @param Route $route
113
     * @param array $values
114
     * @param string $targetValue
115
     * @return array
116
     */
117
    protected function overrideValuesByAspect(Route $route, array $values, string $targetValue): array
118
    {
119
        foreach (array_keys($route->getAspects()) as $variableName) {
120
            $values[$variableName] = $targetValue;
121
        }
122
        return $values;
123
    }
124
125
    /**
126
     * Define items having an aspect definition in case they are not defined
127
     * with a given $targetValue in target $targetValue array.
128
     *
129
     * @param Route $route
130
     * @param array $values
131
     * @param string $targetValue
132
     * @return array
133
     */
134
    protected function defineValuesByAspect(Route $route, array $values, string $targetValue): array
135
    {
136
        foreach (array_keys($route->getAspects()) as $variableName) {
137
            if (isset($values[$variableName])) {
138
                continue;
139
            }
140
            $values[$variableName] = $targetValue;
141
        }
142
        return $values;
143
    }
144
145
    /**
146
     * Modify the route path to add the variable names with the aspects, e.g.
147
     *
148
     * + `/{locale_modifier}/{product_title}`  -> `/products/{product_title}`
149
     * + `/{!locale_modifier}/{product_title}` -> `/products/{product_title}`
150
     *
151
     * @param string $routePath
152
     * @return string
153
     */
154
    protected function modifyRoutePath(string $routePath): string
155
    {
156
        $substitutes = [];
157
        foreach ($this->aspects as $variableName => $aspect) {
158
            if (!$aspect instanceof ModifiableAspectInterface) {
159
                continue;
160
            }
161
            $value = $aspect->modify();
162
            if ($value !== null) {
163
                $substitutes['{' . $variableName . '}'] = $value;
164
                $substitutes['{!' . $variableName . '}'] = $value;
165
            }
166
        }
167
        return str_replace(
168
            array_keys($substitutes),
169
            array_values($substitutes),
170
            $routePath
171
        );
172
    }
173
174
    /**
175
     * Retrieves type from processed route and modifies remaining query parameters.
176
     *
177
     * @param Route $route
178
     * @param array $remainingQueryParameters reference to remaining query parameters
179
     * @return string
180
     */
181
    protected function resolveType(Route $route, array &$remainingQueryParameters): string
182
    {
183
        $type = $remainingQueryParameters['type'] ?? 0;
184
        $decoratedParameters = $route->getOption('_decoratedParameters');
185
        if (isset($decoratedParameters['type'])) {
186
            $type = $decoratedParameters['type'];
187
            unset($decoratedParameters['type']);
188
            $remainingQueryParameters = array_replace_recursive(
189
                $remainingQueryParameters,
190
                $decoratedParameters
0 ignored issues
show
Bug introduced by
$decoratedParameters of type string is incompatible with the type array expected by parameter $array1 of array_replace_recursive(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

190
                /** @scrutinizer ignore-type */ $decoratedParameters
Loading history...
191
            );
192
        }
193
        return (string)$type;
194
    }
195
196
    /**
197
     * @return VariableProcessor
198
     */
199
    protected function getVariableProcessor(): VariableProcessor
200
    {
201
        if (isset($this->variableProcessor)) {
202
            return $this->variableProcessor;
203
        }
204
        return $this->variableProcessor = new VariableProcessor();
205
    }
206
207
    /**
208
     * {@inheritdoc}
209
     */
210
    public function setAspects(array $aspects): void
211
    {
212
        $this->aspects = $aspects;
213
    }
214
215
    /**
216
     * {@inheritdoc}
217
     */
218
    public function getAspects(): array
219
    {
220
        return $this->aspects;
221
    }
222
}
223