Completed
Pull Request — master (#470)
by Claus
06:03
created

ViewHelperResolver::addViewHelperAlias()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 3
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
declare(strict_types=1);
3
namespace TYPO3Fluid\Fluid\Core\ViewHelper;
4
5
/*
6
 * This file belongs to the package "TYPO3 Fluid".
7
 * See LICENSE.txt that was shipped with this package.
8
 */
9
10
use TYPO3Fluid\Fluid\Component\Argument\ArgumentCollection;
11
use TYPO3Fluid\Fluid\Component\ComponentInterface;
12
use TYPO3Fluid\Fluid\Component\Error\ChildNotFoundException;
13
use TYPO3Fluid\Fluid\Core\Parser\Exception;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, TYPO3Fluid\Fluid\Core\ViewHelper\Exception.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
14
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
15
use TYPO3Fluid\Fluid\ViewHelpers\AtomViewHelper;
16
use TYPO3Fluid\Fluid\ViewHelpers\RenderViewHelper;
17
use TYPO3Fluid\Fluid\ViewHelpers\SectionViewHelper;
18
19
/**
20
 * Class ViewHelperResolver
21
 *
22
 * Responsible for resolving instances of ViewHelpers and for
23
 * interacting with ViewHelpers; to translate ViewHelper names
24
 * into actual class names and resolve their ArgumentDefinitions.
25
 *
26
 * Replacing this class in for example a framework allows that
27
 * framework to be responsible for creating ViewHelper instances
28
 * and detecting possible arguments.
29
 */
30
class ViewHelperResolver
31
{
32
    /**
33
     * @var RenderingContextInterface
34
     */
35
    protected $renderingContext;
36
37
    /**
38
     * @var array
39
     */
40
    protected $resolvedViewHelperClassNames = [];
41
42
    /**
43
     * Atom paths indexed by namespace, in
44
     * [shortname => [path1, path2, ...]] format.
45
     * @var array
46
     */
47
    protected $atoms = [];
48
49
    /**
50
     * Namespaces requested by the template being rendered,
51
     * in [shortname => [phpnamespace1, phpnamespace2, ...]] format.
52
     *
53
     * @var array
54
     */
55
    protected $namespaces = [
56
        'f' => ['TYPO3Fluid\\Fluid\\ViewHelpers']
57
    ];
58
59
    /**
60
     * @var array
61
     */
62
    protected $aliases = [
63
        'html' => ['f', 'html'],
64
        'raw' => ['f', 'format.raw'],
65
    ];
66
67
    public function __construct(RenderingContextInterface $renderingContext)
68
    {
69
        $this->renderingContext = $renderingContext;
70
    }
71
72
    public function addAtomPath(string $namespace, string $path): void
73
    {
74
        if (!in_array($path, $this->atoms[$namespace] ?? [], true)) {
75
            $this->atoms[$namespace][] = $path;
76
        }
77
    }
78
79
    /**
80
     * Add all Atom paths as array-in-array, with the first level key
81
     * being the namespace and the value being an array of paths.
82
     *
83
     * Example:
84
     *
85
     * $resolver->addAtomPaths(
86
     *   [
87
     *     'my' => [
88
     *       'path/first/',
89
     *       'path/second/',
90
     *     ],
91
     *     'other' => [
92
     *       'path/third/',
93
     *     ],
94
     *   ]
95
     * );
96
     *
97
     * @param iterable|string[][] $paths
98
     */
99
    public function addAtomPaths(iterable $paths): void
100
    {
101
        foreach ($paths as $namespace => $collection) {
102
            foreach ($collection as $path) {
103
                $this->addAtomPath($namespace, $path);
104
            }
105
        }
106
    }
107
108
    public function resolveAtom(string $namespace, string $name): ComponentInterface
109
    {
110
        $file = $this->resolveAtomFile($namespace, $name);
111
        if (!$file) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $file of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
112
            $paths = empty($this->atoms[$namespace]) ? 'none' : implode(', ', $this->atoms[$namespace]);
113
            throw new ChildNotFoundException(
114
                'Atom "' . $namespace . ':' . $name . '" could not be resolved. We looked in: ' . $paths,
115
                1564404340
116
            );
117
        }
118
        return $this->renderingContext->getTemplateParser()->parseFile($file)->setName($namespace . ':' . $name);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface TYPO3Fluid\Fluid\Component\ComponentInterface as the method setName() does only exist in the following implementations of said interface: TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\EntryNode.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
119
    }
120
121
    public function resolveAtomFile(string $namespace, string $name): ?string
122
    {
123
        if (!isset($this->atoms[$namespace])) {
124
            return null;
125
        }
126
        $expectedFileParts = explode('.', $name);
127
        foreach (array_reverse($this->atoms[$namespace]) as $path) {
128
            $parts = $expectedFileParts;
129
            $subPath = $path;
130
            while ($expectedFilePart = array_shift($parts)) {
131
                $subPath .= '/' . $expectedFilePart;
132
                if (!is_dir($subPath)) {
133
                    break;
134
                }
135
            }
136
            $filePathAndFilename = $subPath . '.html';
137
            if (file_exists($filePathAndFilename)) {
138
                return $filePathAndFilename;
139
            }
140
        }
141
        return null;
142
    }
143
144
    /**
145
     * @return array|string[][]
146
     */
147
    public function getAtoms(): array
148
    {
149
        return $this->atoms;
150
    }
151
152
    /**
153
     * @return array
154
     */
155
    public function getNamespaces(): array
156
    {
157
        return $this->namespaces;
158
    }
159
160
    /**
161
     * Adds an alias of a ViewHelper, allowing you to call for example
162
     *
163
     * @param string $alias
164
     * @param string $namespace
165
     * @param string $identifier
166
     */
167
    public function addViewHelperAlias(string $alias, string $namespace, string $identifier)
168
    {
169
        $this->aliases[$alias] = [$namespace, $identifier];
170
    }
171
172
    public function isAliasRegistered(string $alias): bool
173
    {
174
        return isset($this->aliases[$alias]);
175
    }
176
177
    /**
178
     * Add a PHP namespace where ViewHelpers can be found and give
179
     * it an alias/identifier.
180
     *
181
     * The provided namespace can be either a single namespace or
182
     * an array of namespaces, as strings. The identifier/alias is
183
     * always a single, alpha-numeric ASCII string.
184
     *
185
     * Calling this method multiple times with different PHP namespaces
186
     * for the same alias causes that namespace to be *extended*,
187
     * meaning that the PHP namespace you provide second, third etc.
188
     * are also used in lookups and are used *first*, so that if any
189
     * of the namespaces you add contains a class placed and named the
190
     * same way as one that exists in an earlier namespace, then your
191
     * class gets used instead of the earlier one.
192
     *
193
     * Example:
194
     *
195
     * $resolver->addNamespace('my', 'My\Package\ViewHelpers');
196
     * // Any ViewHelpers under this namespace can now be accessed using for example {my:example()}
197
     * // Now, assuming you also have an ExampleViewHelper class in a different
198
     * // namespace and wish to make that ExampleViewHelper override the other:
199
     * $resolver->addNamespace('my', 'My\OtherPackage\ViewHelpers');
200
     * // Now, since ExampleViewHelper exists in both places but the
201
     * // My\OtherPackage\ViewHelpers namespace was added *last*, Fluid
202
     * // will find and use My\OtherPackage\ViewHelpers\ExampleViewHelper.
203
     *
204
     * Alternatively, setNamespaces() can be used to reset and redefine
205
     * all previously added namespaces - which is great for cases where
206
     * you need to remove or replace previously added namespaces. Be aware
207
     * that setNamespaces() also removes the default "f" namespace, so
208
     * when you use this method you should always include the "f" namespace.
209
     *
210
     * @param string $identifier
211
     * @param string|array $phpNamespace
212
     * @return void
213
     */
214
    public function addNamespace(string $identifier, $phpNamespace): void
215
    {
216
        if (!array_key_exists($identifier, $this->namespaces) || $this->namespaces[$identifier] === null) {
217
            $this->namespaces[$identifier] = $phpNamespace === null ? null : (array) $phpNamespace;
218
        } elseif (is_array($phpNamespace)) {
219
            $this->namespaces[$identifier] = array_unique(array_merge($this->namespaces[$identifier], $phpNamespace));
220
        } elseif (isset($this->namespaces[$identifier]) && !in_array($phpNamespace, $this->namespaces[$identifier], true)) {
221
            $this->namespaces[$identifier][] = $phpNamespace;
222
        }
223
        $this->resolvedViewHelperClassNames = [];
224
    }
225
226
    /**
227
     * Wrapper to allow adding namespaces in bulk *without* first
228
     * clearing the already added namespaces. Utility method mainly
229
     * used in compiled templates, where some namespaces can be added
230
     * from outside and some can be added from compiled values.
231
     *
232
     * @param array $namespaces
233
     * @return void
234
     */
235
    public function addNamespaces(array $namespaces): void
236
    {
237
        foreach ($namespaces as $identifier => $namespace) {
238
            $this->addNamespace($identifier, $namespace);
239
        }
240
    }
241
242
    public function removeNamespace(string $identifier, $phpNamespace): void
243
    {
244
        if (($key = array_search($phpNamespace, $this->namespaces[$identifier], true)) !== false) {
245
            unset($this->namespaces[$identifier][$key]);
246
            if (empty($this->namespaces[$identifier])) {
247
                unset($this->namespaces[$identifier]);
248
            }
249
        }
250
    }
251
252
    /**
253
     * Resolves the PHP namespace based on the Fluid xmlns namespace,
254
     * which can be either a URL matching the Patterns::NAMESPACEPREFIX
255
     * and Patterns::NAMESPACESUFFIX rules, or a PHP namespace. When
256
     * namespace is a PHP namespace it is optional to suffix it with
257
     * the "\ViewHelpers" segment, e.g. "My\Package" is as valid to
258
     * use as "My\Package\ViewHelpers" is.
259
     *
260
     * @param string $fluidNamespace
261
     * @return string
262
     */
263
    public function resolvePhpNamespaceFromFluidNamespace(string $fluidNamespace): string
264
    {
265
        $prefix = 'http://typo3.org/ns/';
266
        $suffix = '/ViewHelpers';
267
        $namespace = $fluidNamespace;
268
        $suffixLength = strlen($suffix);
269
        $phpNamespaceSuffix = str_replace('/', '\\', $suffix);
270
        $extractedSuffix = substr($fluidNamespace, 0 - $suffixLength);
271
        if (strpos($fluidNamespace, $prefix) === 0 && $extractedSuffix === $suffix) {
272
            // convention assumed: URL starts with prefix and ends with suffix
273
            $namespace = substr($fluidNamespace, strlen($prefix));
274
        }
275
        $namespace = str_replace('/', '\\', $namespace);
276
        if (substr($namespace, 0 - strlen($phpNamespaceSuffix)) !== $phpNamespaceSuffix) {
277
            $namespace .= $phpNamespaceSuffix;
278
        }
279
        return $namespace;
280
    }
281
282
    /**
283
     * Set all namespaces as an array of ['identifier' => ['Php\Namespace1', 'Php\Namespace2']]
284
     * namespace definitions. For convenience and legacy support, a
285
     * format of ['identifier' => 'Only\Php\Namespace'] is allowed,
286
     * but will internally convert the namespace to an array and
287
     * allow it to be extended by addNamespace().
288
     *
289
     * Note that when using this method the default "f" namespace is
290
     * also removed and so must be included in $namespaces or added
291
     * after using addNamespace(). Or, add the PHP namespaces that
292
     * belonged to "f" as a new alias and use that in your templates.
293
     *
294
     * Use getNamespaces() to get an array of currently added namespaces.
295
     *
296
     * @param array $namespaces
297
     * @return void
298
     */
299
    public function setNamespaces(array $namespaces): void
300
    {
301
        $this->namespaces = [];
302
        foreach ($namespaces as $identifier => $phpNamespace) {
303
            $this->namespaces[$identifier] = $phpNamespace === null ? null : (array) $phpNamespace;
304
        }
305
    }
306
307
    /**
308
     * Validates the given namespaceIdentifier and returns FALSE
309
     * if the namespace is unknown, causing the tag to be rendered
310
     * without processing.
311
     *
312
     * @param string $namespaceIdentifier
313
     * @return boolean TRUE if the given namespace is valid, otherwise FALSE
314
     */
315
    public function isNamespaceValid(string $namespaceIdentifier): bool
316
    {
317
        if (!array_key_exists($namespaceIdentifier, $this->namespaces)) {
318
            return false;
319
        }
320
321
        return $this->namespaces[$namespaceIdentifier] !== null;
322
    }
323
324
    /**
325
     * Validates the given namespaceIdentifier and returns FALSE
326
     * if the namespace is unknown and not ignored
327
     *
328
     * @param string $namespaceIdentifier
329
     * @return boolean TRUE if the given namespace is valid, otherwise FALSE
330
     */
331
    public function isNamespaceValidOrIgnored(string $namespaceIdentifier): bool
332
    {
333
        if ($this->isNamespaceValid($namespaceIdentifier)) {
334
            return true;
335
        }
336
337
        if (array_key_exists($namespaceIdentifier, $this->namespaces) || array_key_exists($namespaceIdentifier, $this->atoms)) {
338
            return true;
339
        }
340
        return $this->isNamespaceIgnored($namespaceIdentifier);
341
    }
342
343
    /**
344
     * @param string $namespaceIdentifier
345
     * @return boolean
346
     */
347
    public function isNamespaceIgnored(string $namespaceIdentifier): bool
348
    {
349
        if (array_key_exists($namespaceIdentifier, $this->namespaces) && $this->namespaces[$namespaceIdentifier] === null) {
350
            return true;
351
        }
352
        foreach (array_keys($this->namespaces) as $existingNamespaceIdentifier) {
353
            if (strpos($existingNamespaceIdentifier, '*') === false) {
354
                continue;
355
            }
356
            $pattern = '/' . str_replace(['.', '*'], ['\\.', '[a-zA-Z0-9\.]*'], $existingNamespaceIdentifier) . '/';
357
            if (preg_match($pattern, $namespaceIdentifier) === 1) {
358
                return true;
359
            }
360
        }
361
        return false;
362
    }
363
364
    /**
365
     * Resolves a ViewHelper class name by namespace alias and
366
     * Fluid-format identity, e.g. "f" and "format.htmlspecialchars".
367
     *
368
     * Looks in all PHP namespaces which have been added for the
369
     * provided alias, starting in the last added PHP namespace. If
370
     * a ViewHelper class exists in multiple PHP namespaces Fluid
371
     * will detect and use whichever one was added last.
372
     *
373
     * If no ViewHelper class can be detected in any of the added
374
     * PHP namespaces a Fluid Parser Exception is thrown.
375
     *
376
     * @param string|null $namespaceIdentifier
377
     * @param string $methodIdentifier
378
     * @return string|null
379
     * @throws Exception
380
     */
381
    public function resolveViewHelperClassName(?string $namespaceIdentifier, string $methodIdentifier): ?string
382
    {
383
        if (empty($namespaceIdentifier) && isset($this->aliases[$methodIdentifier])) {
384
            list ($namespaceIdentifier, $methodIdentifier) = $this->aliases[$methodIdentifier];
385
        }
386
        if (!isset($this->resolvedViewHelperClassNames[$namespaceIdentifier][$methodIdentifier])) {
387
            $actualViewHelperClassName = false;
388
389
            $explodedViewHelperName = explode('.', $methodIdentifier);
390
            $className = implode('\\', array_map('ucfirst', $explodedViewHelperName));
391
            $className .= 'ViewHelper';
392
393
            if (!empty($this->namespaces[$namespaceIdentifier])) {
394
                foreach (array_reverse($this->namespaces[$namespaceIdentifier]) as $namespace) {
395
                    $name = $namespace . '\\' . $className;
396
                    if (class_exists($name)) {
397
                        $actualViewHelperClassName = $name;
398
                        break;
399
                    }
400
                }
401
            }
402
403
            if ($actualViewHelperClassName === false) {
404
405
                // Consult Atoms registrations. If namespace and method match an Atom, return RenderViewHelper.
406
                try {
407
                    $atom = $this->resolveAtom($namespaceIdentifier, $methodIdentifier);
0 ignored issues
show
Unused Code introduced by
$atom is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
408
                    #$atomFile = $this->resolveAtomFile($namespaceIdentifier, $methodIdentifier);
409
                    return AtomViewHelper::class;
410
                } catch (ChildNotFoundException $exception) {
411
                    // Silenced; not resolving an atom simply throws the ViewHelper exception below.
412
                }
413
414
                throw new Exception(sprintf(
415
                    'The ViewHelper "<%s:%s>" could not be resolved.' . chr(10) .
416
                    'We looked in the following namespaces: %s.',
417
                    $namespaceIdentifier,
418
                    $methodIdentifier,
419
                    implode(', ', $this->namespaces[$namespaceIdentifier] ?? ['none'])
420
                ), 1407060572);
421
            }
422
423
            $this->resolvedViewHelperClassNames[$namespaceIdentifier][$methodIdentifier] = $actualViewHelperClassName;
424
        }
425
        return $this->resolvedViewHelperClassNames[$namespaceIdentifier][$methodIdentifier];
426
    }
427
428
    /**
429
     * Can be overridden by custom implementations to change the way
430
     * classes are loaded when the class is a ViewHelper - for
431
     * example making it possible to use a DI-aware class loader.
432
     *
433
     * If null is passed as namespace, only registered ViewHelper
434
     * aliases are checked against the $viewHelperShortName.
435
     *
436
     * @param string|null $namespace
437
     * @param string $viewHelperShortName
438
     * @return ComponentInterface
439
     */
440
    public function createViewHelperInstance(?string $namespace, string $viewHelperShortName): ComponentInterface
441
    {
442
        if (!empty($namespace) && isset($this->atoms[$namespace])) {
443
            try {
444
                $atomFile = $this->resolveAtomFile($namespace, $viewHelperShortName);
445
                if ($atomFile) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $atomFile of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
446
                    $atom = $this->renderingContext->getTemplateParser()->parseFile($atomFile)->setName($namespace . ':' . $viewHelperShortName);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface TYPO3Fluid\Fluid\Component\ComponentInterface as the method setName() does only exist in the following implementations of said interface: TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\EntryNode.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
447
                    $instance = $this->createViewHelperInstanceFromClassName(AtomViewHelper::class);
448
                    $instance->getArguments()->setDefinitions($atom->getArguments()->getDefinitions())['file'] = $atomFile;
449
                    return $instance;
450
                }
451
            } catch (ChildNotFoundException $exception) {
452
                // Suppressed; failing to resolve an atom resolves a ViewHelper class instead.
453
            }
454
        }
455
        $className = $this->resolveViewHelperClassName($namespace, $viewHelperShortName);
456
        $instance = $this->createViewHelperInstanceFromClassName($className);
457
        return $instance;
458
    }
459
460
    /**
461
     * Wrapper to create a ViewHelper instance by class name. This is
462
     * the final method called when creating ViewHelper classes -
463
     * overriding this method allows custom constructors, dependency
464
     * injections etc. to be performed on the ViewHelper instance.
465
     *
466
     * @param string $viewHelperClassName
467
     * @return ComponentInterface
468
     */
469
    public function createViewHelperInstanceFromClassName(string $viewHelperClassName): ComponentInterface
470
    {
471
        return new $viewHelperClassName();
472
    }
473
}
474