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

ViewHelperResolver::addNamespace()   B

Complexity

Conditions 7
Paths 5

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
nc 5
nop 2
dl 0
loc 10
rs 8.8333
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\Parser\Exception as ParserException;
10
use TYPO3Fluid\Fluid\Core\Parser\Patterns;
11
12
/**
13
 * Class ViewHelperResolver
14
 *
15
 * Responsible for resolving instances of ViewHelpers and for
16
 * interacting with ViewHelpers; to translate ViewHelper names
17
 * into actual class names and resolve their ArgumentDefinitions.
18
 *
19
 * Replacing this class in for example a framework allows that
20
 * framework to be responsible for creating ViewHelper instances
21
 * and detecting possible arguments.
22
 */
23
class ViewHelperResolver
24
{
25
26
    /**
27
     * @var array
28
     */
29
    protected $resolvedViewHelperClassNames = [];
30
31
    /**
32
     * Namespaces requested by the template being rendered,
33
     * in [shortname => phpnamespace] format.
34
     *
35
     * @var array
36
     */
37
    protected $namespaces = [
38
        'f' => ['TYPO3Fluid\\Fluid\\ViewHelpers']
39
    ];
40
41
    /**
42
     * @var array
43
     */
44
    protected $aliases = [];
45
46
    /**
47
     * @return array
48
     */
49
    public function getNamespaces()
50
    {
51
        return $this->namespaces;
52
    }
53
54
    /**
55
     * Adds an alias of a ViewHelper, allowing you to call for example
56
     *
57
     * @param string $alias
58
     * @param string $namespace
59
     * @param string $identifier
60
     */
61
    public function addViewHelperAlias(string $alias, string $namespace, string $identifier)
62
    {
63
        $this->aliases[$alias] = [$namespace, $identifier];
64
    }
65
66
    public function isAliasRegistered(string $alias): bool
67
    {
68
        return isset($this->aliases[$alias]);
69
    }
70
71
    /**
72
     * Add a PHP namespace where ViewHelpers can be found and give
73
     * it an alias/identifier.
74
     *
75
     * The provided namespace can be either a single namespace or
76
     * an array of namespaces, as strings. The identifier/alias is
77
     * always a single, alpha-numeric ASCII string.
78
     *
79
     * Calling this method multiple times with different PHP namespaces
80
     * for the same alias causes that namespace to be *extended*,
81
     * meaning that the PHP namespace you provide second, third etc.
82
     * are also used in lookups and are used *first*, so that if any
83
     * of the namespaces you add contains a class placed and named the
84
     * same way as one that exists in an earlier namespace, then your
85
     * class gets used instead of the earlier one.
86
     *
87
     * Example:
88
     *
89
     * $resolver->addNamespace('my', 'My\Package\ViewHelpers');
90
     * // Any ViewHelpers under this namespace can now be accessed using for example {my:example()}
91
     * // Now, assuming you also have an ExampleViewHelper class in a different
92
     * // namespace and wish to make that ExampleViewHelper override the other:
93
     * $resolver->addNamespace('my', 'My\OtherPackage\ViewHelpers');
94
     * // Now, since ExampleViewHelper exists in both places but the
95
     * // My\OtherPackage\ViewHelpers namespace was added *last*, Fluid
96
     * // will find and use My\OtherPackage\ViewHelpers\ExampleViewHelper.
97
     *
98
     * Alternatively, setNamespaces() can be used to reset and redefine
99
     * all previously added namespaces - which is great for cases where
100
     * you need to remove or replace previously added namespaces. Be aware
101
     * that setNamespaces() also removes the default "f" namespace, so
102
     * when you use this method you should always include the "f" namespace.
103
     *
104
     * @param string $identifier
105
     * @param string|array $phpNamespace
106
     * @return void
107
     */
108
    public function addNamespace($identifier, $phpNamespace)
109
    {
110
        if (!array_key_exists($identifier, $this->namespaces) || $this->namespaces[$identifier] === null) {
111
            $this->namespaces[$identifier] = $phpNamespace === null ? null : (array) $phpNamespace;
112
        } elseif (is_array($phpNamespace)) {
113
            $this->namespaces[$identifier] = array_unique(array_merge($this->namespaces[$identifier], $phpNamespace));
114
        } elseif (isset($this->namespaces[$identifier]) && !in_array($phpNamespace, $this->namespaces[$identifier])) {
115
            $this->namespaces[$identifier][] = $phpNamespace;
116
        }
117
    }
118
119
    /**
120
     * Wrapper to allow adding namespaces in bulk *without* first
121
     * clearing the already added namespaces. Utility method mainly
122
     * used in compiled templates, where some namespaces can be added
123
     * from outside and some can be added from compiled values.
124
     *
125
     * @param array $namespaces
126
     * @return void
127
     */
128
    public function addNamespaces(array $namespaces)
129
    {
130
        foreach ($namespaces as $identifier => $namespace) {
131
            $this->addNamespace($identifier, $namespace);
132
        }
133
    }
134
135
    /**
136
     * Resolves the PHP namespace based on the Fluid xmlns namespace,
137
     * which can be either a URL matching the Patterns::NAMESPACEPREFIX
138
     * and Patterns::NAMESPACESUFFIX rules, or a PHP namespace. When
139
     * namespace is a PHP namespace it is optional to suffix it with
140
     * the "\ViewHelpers" segment, e.g. "My\Package" is as valid to
141
     * use as "My\Package\ViewHelpers" is.
142
     *
143
     * @param string $fluidNamespace
144
     * @return string
145
     */
146
    public function resolvePhpNamespaceFromFluidNamespace($fluidNamespace)
147
    {
148
        $namespace = $fluidNamespace;
149
        $suffixLength = strlen(Patterns::NAMESPACESUFFIX);
150
        $phpNamespaceSuffix = str_replace('/', '\\', Patterns::NAMESPACESUFFIX);
151
        $extractedSuffix = substr($fluidNamespace, 0 - $suffixLength);
152
        if (strpos($fluidNamespace, Patterns::NAMESPACEPREFIX) === 0 && $extractedSuffix === Patterns::NAMESPACESUFFIX) {
153
            // convention assumed: URL starts with prefix and ends with suffix
154
            $namespace = substr($fluidNamespace, strlen(Patterns::NAMESPACEPREFIX));
155
        }
156
        $namespace = str_replace('/', '\\', $namespace);
157
        if (substr($namespace, 0 - strlen($phpNamespaceSuffix)) !== $phpNamespaceSuffix) {
158
            $namespace .= $phpNamespaceSuffix;
159
        }
160
        return $namespace;
161
    }
162
163
    /**
164
     * Set all namespaces as an array of ['identifier' => ['Php\Namespace1', 'Php\Namespace2']]
165
     * namespace definitions. For convenience and legacy support, a
166
     * format of ['identifier' => 'Only\Php\Namespace'] is allowed,
167
     * but will internally convert the namespace to an array and
168
     * allow it to be extended by addNamespace().
169
     *
170
     * Note that when using this method the default "f" namespace is
171
     * also removed and so must be included in $namespaces or added
172
     * after using addNamespace(). Or, add the PHP namespaces that
173
     * belonged to "f" as a new alias and use that in your templates.
174
     *
175
     * Use getNamespaces() to get an array of currently added namespaces.
176
     *
177
     * @param array $namespaces
178
     * @return void
179
     */
180
    public function setNamespaces(array $namespaces)
181
    {
182
        $this->namespaces = [];
183
        foreach ($namespaces as $identifier => $phpNamespace) {
184
            $this->namespaces[$identifier] = $phpNamespace === null ? null : (array) $phpNamespace;
185
        }
186
    }
187
188
    /**
189
     * Validates the given namespaceIdentifier and returns FALSE
190
     * if the namespace is unknown, causing the tag to be rendered
191
     * without processing.
192
     *
193
     * @param string $namespaceIdentifier
194
     * @return boolean TRUE if the given namespace is valid, otherwise FALSE
195
     */
196
    public function isNamespaceValid($namespaceIdentifier)
197
    {
198
        if (!array_key_exists($namespaceIdentifier, $this->namespaces)) {
199
            return false;
200
        }
201
202
        return $this->namespaces[$namespaceIdentifier] !== null;
203
    }
204
205
    /**
206
     * Validates the given namespaceIdentifier and returns FALSE
207
     * if the namespace is unknown and not ignored
208
     *
209
     * @param string $namespaceIdentifier
210
     * @return boolean TRUE if the given namespace is valid, otherwise FALSE
211
     */
212
    public function isNamespaceValidOrIgnored($namespaceIdentifier)
213
    {
214
        if ($this->isNamespaceValid($namespaceIdentifier) === true) {
215
            return true;
216
        }
217
218
        if (array_key_exists($namespaceIdentifier, $this->namespaces)) {
219
            return true;
220
        }
221
222
        if ($this->isNamespaceIgnored($namespaceIdentifier)) {
223
            return true;
224
        }
225
226
        return false;
227
    }
228
229
    /**
230
     * @param string $namespaceIdentifier
231
     * @return boolean
232
     */
233
    public function isNamespaceIgnored($namespaceIdentifier)
234
    {
235
        if (array_key_exists($namespaceIdentifier, $this->namespaces) && $this->namespaces[$namespaceIdentifier] === null) {
236
            return true;
237
        }
238
        foreach (array_keys($this->namespaces) as $existingNamespaceIdentifier) {
239
            if (strpos($existingNamespaceIdentifier, '*') === false) {
240
                continue;
241
            }
242
            $pattern = '/' . str_replace(['.', '*'], ['\\.', '[a-zA-Z0-9\.]*'], $existingNamespaceIdentifier) . '/';
243
            if (preg_match($pattern, $namespaceIdentifier) === 1) {
244
                return true;
245
            }
246
        }
247
        return false;
248
    }
249
250
    /**
251
     * Resolves a ViewHelper class name by namespace alias and
252
     * Fluid-format identity, e.g. "f" and "format.htmlspecialchars".
253
     *
254
     * Looks in all PHP namespaces which have been added for the
255
     * provided alias, starting in the last added PHP namespace. If
256
     * a ViewHelper class exists in multiple PHP namespaces Fluid
257
     * will detect and use whichever one was added last.
258
     *
259
     * If no ViewHelper class can be detected in any of the added
260
     * PHP namespaces a Fluid Parser Exception is thrown.
261
     *
262
     * @param string|null $namespaceIdentifier
263
     * @param string $methodIdentifier
264
     * @return string|NULL
265
     * @throws ParserException
266
     */
267
    public function resolveViewHelperClassName($namespaceIdentifier, $methodIdentifier)
268
    {
269
        if (empty($namespaceIdentifier) && isset($this->aliases[$methodIdentifier])) {
270
            list ($namespaceIdentifier, $methodIdentifier) = $this->aliases[$methodIdentifier];
271
        }
272
        if (!isset($this->resolvedViewHelperClassNames[$namespaceIdentifier][$methodIdentifier])) {
273
            $resolvedViewHelperClassName = $this->resolveViewHelperName($namespaceIdentifier, $methodIdentifier);
274
            $actualViewHelperClassName = implode('\\', array_map('ucfirst', explode('.', $resolvedViewHelperClassName)));
275
            if (false === class_exists($actualViewHelperClassName) || $actualViewHelperClassName === false) {
276
                throw new ParserException(sprintf(
277
                    'The ViewHelper "<%s:%s>" could not be resolved.' . chr(10) .
278
                    'Based on your spelling, the system would load the class "%s", however this class does not exist. ' .
279
                    'We looked in the following namespaces: ' . implode(', ', $this->namespaces[$namespaceIdentifier] ?? ['none']) . '.',
280
                    $namespaceIdentifier,
281
                    $methodIdentifier,
282
                    $resolvedViewHelperClassName
283
                ), 1407060572);
284
            }
285
            $this->resolvedViewHelperClassNames[$namespaceIdentifier][$methodIdentifier] = $actualViewHelperClassName;
286
        }
287
        return $this->resolvedViewHelperClassNames[$namespaceIdentifier][$methodIdentifier];
288
    }
289
290
    /**
291
     * Can be overridden by custom implementations to change the way
292
     * classes are loaded when the class is a ViewHelper - for
293
     * example making it possible to use a DI-aware class loader.
294
     *
295
     * @param string $namespace
296
     * @param string $viewHelperShortName
297
     * @return ViewHelperInterface
298
     */
299
    public function createViewHelperInstance($namespace, $viewHelperShortName)
300
    {
301
        $className = $this->resolveViewHelperClassName($namespace, $viewHelperShortName);
302
        return $this->createViewHelperInstanceFromClassName($className);
303
    }
304
305
    /**
306
     * Wrapper to create a ViewHelper instance by class name. This is
307
     * the final method called when creating ViewHelper classes -
308
     * overriding this method allows custom constructors, dependency
309
     * injections etc. to be performed on the ViewHelper instance.
310
     *
311
     * @param string $viewHelperClassName
312
     * @return ViewHelperInterface
313
     */
314
    public function createViewHelperInstanceFromClassName($viewHelperClassName)
315
    {
316
        return new $viewHelperClassName();
317
    }
318
319
    /**
320
     * Return an array of ArgumentDefinition instances which describe
321
     * the arguments that the ViewHelper supports. By default, the
322
     * arguments are simply fetched from the ViewHelper - but custom
323
     * implementations can if necessary add/remove/replace arguments
324
     * which will be passed to the ViewHelper.
325
     *
326
     * @param ViewHelperInterface $viewHelper
327
     * @return ArgumentDefinition[]
328
     */
329
    public function getArgumentDefinitionsForViewHelper(ViewHelperInterface $viewHelper)
330
    {
331
        return $viewHelper->prepareArguments();
332
    }
333
334
    /**
335
     * Resolve a viewhelper name.
336
     *
337
     * @param string $namespaceIdentifier Namespace identifier for the view helper.
338
     * @param string $methodIdentifier Method identifier, might be hierarchical like "link.url"
339
     * @return string The fully qualified class name of the viewhelper
340
     */
341
    protected function resolveViewHelperName($namespaceIdentifier, $methodIdentifier)
342
    {
343
        $explodedViewHelperName = explode('.', $methodIdentifier);
344
        if (count($explodedViewHelperName) > 1) {
345
            $className = implode('\\', array_map('ucfirst', $explodedViewHelperName));
346
        } else {
347
            $className = ucfirst($explodedViewHelperName[0]);
348
        }
349
        $className .= 'ViewHelper';
350
351
        $namespaces = (array) $this->namespaces[$namespaceIdentifier];
352
353
        do {
354
            $name = rtrim(array_pop($namespaces), '\\') . '\\' . $className;
355
        } while (!class_exists($name) && count($namespaces));
356
357
        return $name;
358
    }
359
}
360