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

CPS-IT / handlebars / 23363698435

20 Mar 2026 09:40PM UTC coverage: 89.861% (-0.4%) from 90.285%
23363698435

Pull #543

github

web-flow
Merge 223a31852 into 86597a598
Pull Request #543: [TASK] Update codebase to match PHPStan max level

139 of 157 new or added lines in 43 files covered. (88.54%)

26 existing lines in 11 files now uncovered.

1356 of 1509 relevant lines covered (89.86%)

6.82 hits per line

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

90.91
/Classes/Frontend/Assets/AssetHandler.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\Frontend\Assets;
19

20
use CPSIT\Typo3Handlebars\Exception;
21
use TYPO3\CMS\Core;
22

23
/**
24
 * AssetHandler
25
 *
26
 * Processes asset configuration and registers assets with TYPO3's AssetCollector.
27
 * Supports JavaScript, CSS, and inline variants with full attribute and option support.
28
 *
29
 * @author Vladimir Falcon Piva <v.falcon@familie-redlich.de>
30
 * @license GPL-2.0-or-later
31
 */
32
final readonly class AssetHandler
33
{
34
    private const KNOWN_OPTIONS = ['priority', 'useNonce'];
35

36
    public function __construct(
32✔
37
        private Core\Page\AssetCollector $assetCollector,
38
    ) {}
32✔
39

40
    /**
41
     * Process and register assets from configuration.
42
     *
43
     * @param array<int|string, mixed> $assetsConfiguration Plain asset configuration array
44
     * @throws Exception\InvalidAssetConfigurationException
45
     */
46
    public function collectAssets(array $assetsConfiguration): void
31✔
47
    {
48
        foreach ($assetsConfiguration as $typeKey => $assets) {
31✔
49
            if (!is_iterable($assets)) {
30✔
NEW
UNCOV
50
                continue;
×
51
            }
52

53
            // Validate and get AssetType (validates everything)
54
            $assetType = $this->validateAssetAndResolveAssetType($typeKey, $assets);
30✔
55

56
            // Process validated assets
57
            foreach ($assets as $identifier => $configuration) {
25✔
58
                $this->processAsset((string)$identifier, $assetType, $configuration);
25✔
59
            }
60
        }
61
    }
62

63
    /**
64
     * @param array{source: string, attributes?: array<string, string>, options?: array<string, string>} $configuration
65
     */
66
    private function processAsset(string $identifier, AssetType $type, array $configuration): void
25✔
67
    {
68
        $source = trim((string)$configuration['source']);
25✔
69
        $attributes = $this->processAttributes($configuration['attributes'] ?? [], $type);
25✔
70
        $options = $this->processOptions($configuration['options'] ?? []);
25✔
71

72
        match ($type) {
73
            AssetType::JavaScript => $this->assetCollector->addJavaScript($identifier, $source, $attributes, $options),
25✔
74
            AssetType::InlineJavaScript => $this->assetCollector->addInlineJavaScript($identifier, $source, $attributes, $options),
14✔
75
            AssetType::Css => $this->assetCollector->addStyleSheet($identifier, $source, $attributes, $options),
11✔
76
            AssetType::InlineCss => $this->assetCollector->addInlineStyleSheet($identifier, $source, $attributes, $options),
4✔
77
        };
78
    }
79

80
    /**
81
     * Validate complete asset type configuration and resolve configured asset type.
82
     *
83
     * Validates:
84
     * - Asset type key is valid
85
     * - Assets is an array
86
     * - Each identifier is valid (string, not empty)
87
     * - Each asset config is valid (array with source)
88
     *
89
     * @param string|int $typeKey The asset type key from config
90
     * @param mixed $assets The assets configuration for this type
91
     * @return AssetType The validated AssetType enum
92
     * @throws Exception\InvalidAssetConfigurationException
93
     * @phpstan-assert array<string, array{source: string, attributes?: array<string, string>, options?: array<string, string>}> $assets
94
     */
95
    private function validateAssetAndResolveAssetType(string|int $typeKey, mixed $assets): AssetType
30✔
96
    {
97
        $assetType = AssetType::tryFrom((string)$typeKey);
30✔
98

99
        if ($assetType === null) {
30✔
100
            throw Exception\InvalidAssetConfigurationException::forUnknownAssetType((string)$typeKey);
1✔
101
        }
102

103
        if (!is_array($assets)) {
29✔
104
            throw Exception\InvalidAssetConfigurationException::forInvalidAssetsArray($assetType);
×
105
        }
106

107
        foreach ($assets as $identifier => $assetConfig) {
29✔
108
            if (!is_string($identifier) || trim($identifier) === '') {
29✔
UNCOV
109
                throw Exception\InvalidAssetConfigurationException::forInvalidIdentifier($assetType);
×
110
            }
111

112
            if (!is_array($assetConfig)) {
29✔
UNCOV
113
                throw Exception\InvalidAssetConfigurationException::forInvalidConfiguration($identifier, $assetType);
×
114
            }
115

116
            if (!is_string($assetConfig['source'] ?? null) || trim($assetConfig['source']) === '') {
29✔
117
                throw Exception\InvalidAssetConfigurationException::forMissingSource($identifier, $assetType);
4✔
118
            }
119
        }
120

121
        return $assetType;
25✔
122
    }
123

124
    /**
125
     * Process and normalize HTML attributes.
126
     *
127
     * @return array<string, string>
128
     */
129
    private function processAttributes(mixed $attributes, AssetType $type): array
25✔
130
    {
131
        if (!is_array($attributes)) {
25✔
UNCOV
132
            return [];
×
133
        }
134

135
        $processed = [];
25✔
136

137
        foreach ($attributes as $name => $value) {
25✔
138
            $name = (string)$name;
12✔
139
            $this->addAttribute($processed, $name, $value, $type);
12✔
140
        }
141

142
        return $processed;
25✔
143
    }
144

145
    /**
146
     * @param array<string, string> $processed
147
     */
148
    private function addAttribute(array &$processed, string $name, mixed $value, AssetType $type): void
12✔
149
    {
150
        if ($type->isBooleanAttribute($name)) {
12✔
151
            $this->addBooleanAttribute($processed, $name, $value);
7✔
152
        } else {
153
            $this->addRegularAttribute($processed, $name, $value);
9✔
154
        }
155
    }
156

157
    /**
158
     * Add a boolean attribute (e.g., async, defer, disabled).
159
     *
160
     * @param array<string, string> $processed
161
     */
162
    private function addBooleanAttribute(array &$processed, string $name, mixed $value): void
7✔
163
    {
164
        if ((bool)$value === true) {
7✔
165
            $processed[$name] = $name;
7✔
166
        }
167
    }
168

169
    /**
170
     * @param array<string, string> $processed
171
     */
172
    private function addRegularAttribute(array &$processed, string $name, mixed $value): void
9✔
173
    {
174
        if (is_scalar($value) && (string)$value !== '') {
9✔
175
            $processed[$name] = (string)$value;
9✔
176
        }
177
    }
178

179
    /**
180
     * Process AssetCollector options.
181
     *
182
     * @param array<string, mixed> $options
183
     * @return array<string, bool>
184
     */
185
    private function processOptions(array $options): array
25✔
186
    {
187
        $processed = [];
25✔
188

189
        foreach (self::KNOWN_OPTIONS as $option) {
25✔
190
            if (isset($options[$option])) {
25✔
191
                $processed[$option] = (bool)$options[$option];
4✔
192
            }
193
        }
194

195
        return $processed;
25✔
196
    }
197
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc