• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

CPS-IT / handlebars / 20814129699

08 Jan 2026 10:44AM UTC coverage: 91.184% (-0.6%) from 91.734%
20814129699

Pull #510

github

web-flow
Merge ea78b03c7 into cbd2ccf77
Pull Request #510: [FEATURE] Support passing current rendering context to helper functions

43 of 54 new or added lines in 3 files covered. (79.63%)

1055 of 1157 relevant lines covered (91.18%)

5.51 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.8
/Classes/Renderer/Helper/HelperRegistry.php
1
<?php
2

3
declare(strict_types=1);
4

5
/*
6
 * This file is part of the TYPO3 CMS extension "handlebars".
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 CPSIT\Typo3Handlebars\Renderer\Helper;
19

20
use CPSIT\Typo3Handlebars\Exception;
21
use CPSIT\Typo3Handlebars\Renderer;
22
use DevTheorem\Handlebars;
23
use Psr\Log;
24
use TYPO3\CMS\Core;
25

26
/**
27
 * HelperRegistry
28
 *
29
 * @author Elias Häußler <e.haeussler@familie-redlich.de>
30
 * @license GPL-2.0-or-later
31
 */
32
final class HelperRegistry implements Core\SingletonInterface
33
{
34
    /**
35
     * @var array<string, \Closure(mixed..., Handlebars\HelperOptions): mixed>
36
     */
37
    private array $helpers = [];
38

39
    public function __construct(
32✔
40
        private readonly Log\LoggerInterface $logger,
41
    ) {}
32✔
42

43
    /**
44
     * @param array{object, string}|array{class-string, string}|callable|string|Helper $function
45
     */
46
    public function add(string $name, array|callable|string|Helper $function): void
31✔
47
    {
48
        try {
49
            $this->helpers[$name] = $this->decorateHelperFunction(
31✔
50
                $this->resolveHelperFunction($function),
31✔
51
            );
31✔
52
        } catch (Exception\InvalidHelperException | \ReflectionException $exception) {
4✔
53
            $this->logger->critical(
4✔
54
                'Error while registering Handlebars helper "' . $name . '".',
4✔
55
                [
4✔
56
                    'name' => $name,
4✔
57
                    'function' => $function,
4✔
58
                    'exception' => $exception,
4✔
59
                ],
4✔
60
            );
4✔
61
        }
62
    }
63

64
    /**
65
     * @return \Closure(mixed..., Handlebars\HelperOptions): mixed
66
     * @throws Exception\HelperIsNotRegistered
67
     */
68
    public function get(string $name): \Closure
26✔
69
    {
70
        if (!isset($this->helpers[$name])) {
26✔
71
            throw new Exception\HelperIsNotRegistered($name);
1✔
72
        }
73

74
        return $this->helpers[$name];
25✔
75
    }
76

77
    /**
78
     * @return array<string, \Closure(mixed..., Handlebars\HelperOptions): mixed>
79
     */
80
    public function getAll(): array
5✔
81
    {
82
        return $this->helpers;
5✔
83
    }
84

85
    public function has(string $name): bool
1✔
86
    {
87
        return isset($this->helpers[$name]);
1✔
88
    }
89

90
    /**
91
     * @param array{object, string}|array{class-string, string}|callable|string|Helper $function
92
     * @throws Exception\InvalidHelperException
93
     * @throws \ReflectionException
94
     */
95
    private function resolveHelperFunction(array|callable|string|Helper $function): callable
31✔
96
    {
97
        // Try to resolve the Helper function in this order:
98
        //
99
        // 1. callable
100
        // ├─ a. as string
101
        // └─ b. as closure or first class callable syntax
102
        // 2. invokable class
103
        // └─ a. as string (class-name)
104
        // └─ b. as object
105
        // 3. class implementing Helper interface
106
        // ├─ a. as string (class-name)
107
        // └─ b. as object
108
        // 4. class method
109
        // ├─ a. as string => class-name::method-name
110
        // ├─ b. as array => [class-name, method-name]
111
        // └─ c. as initialized array => [object, method-name]
112

113
        if (\is_string($function) && !str_contains($function, '::')) {
31✔
114
            // 1a. callable as string
115
            if (\is_callable($function)) {
8✔
116
                return $function;
5✔
117
            }
118

119
            // 2a. invokable class as string
120
            if (class_exists($function) && \is_callable($callable = Core\Utility\GeneralUtility::makeInstance($function))) {
3✔
121
                return $callable;
1✔
122
            }
123

124
            // 3a. class implementing Helper interface as string
125
            if (class_exists($function) && \is_a($function, Helper::class, true)) {
2✔
126
                return Core\Utility\GeneralUtility::makeInstance($function)->render(...);
1✔
127
            }
128
        }
129

130
        if ($function instanceof \Closure) {
24✔
131
            // 1b. callable as closure or first class callable syntax
132
            return $function;
13✔
133
        }
134

135
        if (is_callable($function) && is_object($function)) {
11✔
136
            // 2b. invokable class as object
137
            return $function;
1✔
138
        }
139

140
        // 3b. class implementing Helper interface as object
141
        if ($function instanceof Helper) {
10✔
142
            return $function->render(...);
1✔
143
        }
144

145
        $className = null;
9✔
146
        $methodName = null;
9✔
147

148
        // 4a. class method as string
149
        /* @phpstan-ignore booleanAnd.rightAlwaysFalse */
150
        if (\is_string($function) && str_contains($function, '::')) {
9✔
151
            [$className, $methodName] = explode('::', $function, 2);
4✔
152
        }
153

154
        // 4b. class method as array
155
        // 4c. class method as initialized array
156
        if (\is_array($function) && \count($function) === 2) {
9✔
157
            [$className, $methodName] = $function;
4✔
158
        }
159

160
        // Early return if either class name or method name cannot be resolved
161
        if ($className === null || $methodName === null) {
9✔
162
            throw Exception\InvalidHelperException::forUnsupportedType($function);
1✔
163
        }
164

165
        // Early return if method is not public
166
        $reflectionClass = new \ReflectionClass($className);
8✔
167
        $reflectionMethod = $reflectionClass->getMethod($methodName);
8✔
168
        if (!$reflectionMethod->isPublic()) {
6✔
169
            throw Exception\InvalidHelperException::forFunction($reflectionClass->name . '::' . $methodName);
1✔
170
        }
171

172
        // Instantiate class if not done yet
173
        if (\is_string($className) && !$reflectionMethod->isStatic()) {
5✔
174
            /** @var class-string $className */
175
            $helperClass = Core\Utility\GeneralUtility::makeInstance($className);
2✔
176
        } else {
177
            $helperClass = $className;
3✔
178
        }
179

180
        $callable = [$helperClass, $methodName];
5✔
181

182
        if (!\is_callable($callable)) {
5✔
NEW
183
            throw Exception\InvalidHelperException::forInvalidCallable($callable);
×
184
        }
185

186
        if ($reflectionMethod->isStatic()) {
5✔
187
            return $helperClass::$methodName(...);
2✔
188
        }
189

190
        return $helperClass->$methodName(...);
3✔
191
    }
192

193
    /**
194
     * @return \Closure(mixed..., Handlebars\HelperOptions): mixed
195
     */
196
    private function decorateHelperFunction(callable $function): \Closure
27✔
197
    {
198
        return static function () use ($function) {
27✔
199
            $arguments = \func_get_args();
11✔
200
            /** @var Handlebars\HelperOptions $options */
201
            $options = \array_pop($arguments);
11✔
202
            $renderingContext = $options->data['renderingContext'] ?? null;
11✔
203
            $parameters = self::mapFunctionParameters($function, $options, $renderingContext, $arguments);
11✔
204

205
            return $function(...$parameters);
9✔
206
        };
27✔
207
    }
208

209
    /**
210
     * @param list<mixed> $arguments
211
     * @return list<mixed>
212
     * @throws Exception\InvalidHelperException
213
     */
214
    private static function mapFunctionParameters(
11✔
215
        callable $function,
216
        Handlebars\HelperOptions $options,
217
        mixed $renderingContext,
218
        array $arguments,
219
    ): array {
220
        // Fall back to HelperOptions + runtime arguments if callable cannot be reflected
221
        if (!is_string($function) && !($function instanceof \Closure)) {
11✔
NEW
222
            return [$options, ...$arguments];
×
223
        }
224

225
        $reflectionFunction = new \ReflectionFunction($function);
11✔
226
        $parameters = [];
11✔
227
        $parameterMap = [
11✔
228
            Handlebars\HelperOptions::class => $options,
11✔
229
            Renderer\RenderingContext::class => $renderingContext,
11✔
230
        ];
11✔
231

232
        foreach ($reflectionFunction->getParameters() as $parameter) {
11✔
233
            $type = $parameter->getType();
10✔
234

235
            // Exit loop if we reached runtime arguments (those may be declared without a named type)
236
            if (!($type instanceof \ReflectionNamedType)) {
10✔
237
                break;
1✔
238
            }
239

240
            // Exit loop if we reached runtime arguments
241
            if (!\array_key_exists($type->getName(), $parameterMap)) {
9✔
242
                break;
4✔
243
            }
244

245
            $resolvedParameter = $parameterMap[$type->getName()];
9✔
246

247
            if ($resolvedParameter === null) {
9✔
248
                if ($type->allowsNull()) {
2✔
249
                    $parameters[] = null;
1✔
250
                    continue;
1✔
251
                }
252

253
                // Fail if a non-nullable parameter would receive null
254
                throw Exception\InvalidHelperException::forUnresolvableParameter($function, $parameter->getName());
1✔
255
            }
256

257
            // Fail if a parameter would receive a wrong type
258
            if (!\is_a($resolvedParameter, $type->getName(), true)) {
7✔
259
                throw Exception\InvalidHelperException::forUnresolvableParameter($function, $parameter->getName());
1✔
260
            }
261

262
            $parameters[] = $resolvedParameter;
6✔
263
        }
264

265
        return [...$parameters, ...$arguments];
9✔
266
    }
267
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc