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

CPS-IT / frontend-asset-handler / 15251341592

26 May 2025 10:12AM UTC coverage: 99.233%. Remained the same
15251341592

Pull #659

github

web-flow
Merge c6e7b1587 into 8b95d2cff
Pull Request #659: [TASK] Update phpunit/phpunit to v12

2459 of 2478 relevant lines covered (99.23%)

15.09 hits per line

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

98.63
/src/Command/ConfigAssetsCommand.php
1
<?php
2

3
declare(strict_types=1);
4

5
/*
6
 * This file is part of the Composer package "fr/frontend-asset-handling".
7
 *
8
 * Copyright (C) 2021 Elias Häußler <e.haeussler@familie-redlich.de>
9
 *
10
 * This program is free software: you can redistribute it and/or modify
11
 * it under the terms of the GNU General Public License as published by
12
 * the Free Software Foundation, either version 3 of the License, or
13
 * (at your option) any later version.
14
 *
15
 * This program is distributed in the hope that it will be useful,
16
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18
 * GNU General Public License for more details.
19
 *
20
 * You should have received a copy of the GNU General Public License
21
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
22
 */
23

24
namespace CPSIT\FrontendAssetHandler\Command;
25

26
use CPSIT\FrontendAssetHandler\Config;
27
use CPSIT\FrontendAssetHandler\DependencyInjection;
28
use CPSIT\FrontendAssetHandler\Exception;
29
use CPSIT\FrontendAssetHandler\Helper;
30
use CPSIT\FrontendAssetHandler\Json;
31
use Ergebnis\Json\Printer;
32
use Ergebnis\Json\SchemaValidator;
33
use JsonException;
34
use Symfony\Component\Console;
35

36
use function count;
37
use function explode;
38

39
/**
40
 * ConfigAssetsCommand.
41
 *
42
 * @author Elias Häußler <e.haeussler@familie-redlich.de>
43
 * @license GPL-3.0-or-later
44
 */
45
final class ConfigAssetsCommand extends BaseAssetsCommand
46
{
47
    private const SUCCESSFUL = 0;
48
    private const ERROR_INVALID_PATH = 1;
49
    private const ERROR_EMPTY_PATH = 2;
50
    private const ERROR_CONFLICTING_PARAMETERS = 4;
51
    private const ERROR_INVALID_CONFIG = 8;
52

53
    private const PATH_PREFIX_PATTERN = '#^/?frontend-assets/#';
54

55
    private Console\Style\SymfonyStyle $io;
56

57
    public function __construct(
20✔
58
        DependencyInjection\Cache\ContainerCache $cache,
59
        Config\ConfigFacade $configFacade,
60
        Config\Parser\Parser $configParser,
61
        private readonly Json\SchemaValidator $validator,
62
        private readonly Printer\Printer $printer,
63
    ) {
64
        parent::__construct('config', $cache, $configFacade, $configParser);
20✔
65
    }
66

67
    protected function configure(): void
20✔
68
    {
69
        $this->setDescription('Read, write or validate asset definitions from asset configuration file.');
20✔
70

71
        $this->addArgument(
20✔
72
            'path',
20✔
73
            Console\Input\InputArgument::OPTIONAL,
20✔
74
            'Configuration path to be read or written',
20✔
75
        );
20✔
76
        $this->addArgument(
20✔
77
            'newValue',
20✔
78
            Console\Input\InputArgument::OPTIONAL,
20✔
79
            'New value to be written to given configuration path',
20✔
80
        );
20✔
81
        $this->addOption(
20✔
82
            'unset',
20✔
83
            null,
20✔
84
            Console\Input\InputOption::VALUE_NONE,
20✔
85
            'Unset given asset configuration',
20✔
86
        );
20✔
87
        $this->addOption(
20✔
88
            'validate',
20✔
89
            null,
20✔
90
            Console\Input\InputOption::VALUE_NONE,
20✔
91
            'Validate given asset configuration',
20✔
92
        );
20✔
93
        $this->addOption(
20✔
94
            'json',
20✔
95
            null,
20✔
96
            Console\Input\InputOption::VALUE_NONE,
20✔
97
            'Treat new value as JSON-encoded string',
20✔
98
        );
20✔
99
        $this->addOption(
20✔
100
            'process-values',
20✔
101
            null,
20✔
102
            Console\Input\InputOption::VALUE_NONE,
20✔
103
            'Run value processors when reading or validating asset configuration',
20✔
104
        );
20✔
105
    }
106

107
    protected function execute(Console\Input\InputInterface $input, Console\Output\OutputInterface $output): int
20✔
108
    {
109
        $this->io = new Console\Style\SymfonyStyle($input, $output);
20✔
110

111
        $path = trim((string) $input->getArgument('path'), "/ \n\r\t\v\x00");
20✔
112
        $newValue = $input->getArgument('newValue');
20✔
113
        $unset = $input->getOption('unset');
20✔
114
        $validate = $input->getOption('validate');
20✔
115
        $json = $input->getOption('json');
20✔
116
        $processValues = $input->getOption('process-values');
20✔
117

118
        // Strip "frontend-assets" prefix
119
        $path = preg_replace(self::PATH_PREFIX_PATTERN, '', $path);
20✔
120

121
        // @codeCoverageIgnoreStart
122
        if (null === $path) {
123
            $this->io->error('The configuration path is invalid.');
124

125
            return self::ERROR_INVALID_PATH;
126
        }
127
        // @codeCoverageIgnoreEnd
128

129
        if ('' === $path && !$validate) {
20✔
130
            $this->io->error('The configuration path must not be empty.');
1✔
131

132
            return self::ERROR_EMPTY_PATH;
1✔
133
        }
134
        if (($unset || $validate) && null !== $newValue) {
19✔
135
            $this->io->error('You cannot write or validate and unset a configuration value one at a time.');
3✔
136

137
            return self::ERROR_CONFLICTING_PARAMETERS;
3✔
138
        }
139
        if ($unset && $validate) {
16✔
140
            $this->io->error('You cannot write and validate configuration value one at a time.');
1✔
141

142
            return self::ERROR_CONFLICTING_PARAMETERS;
1✔
143
        }
144

145
        // Unset configuration value
146
        if ($unset) {
15✔
147
            $finalPath = $this->writeConfiguration($path, null);
5✔
148

149
            $this->io->success(
3✔
150
                sprintf('Configuration at %s was successfully unset.', $this->decoratePath($finalPath)),
3✔
151
            );
3✔
152

153
            return self::SUCCESSFUL;
3✔
154
        }
155

156
        // Validate configuration value
157
        if ($validate) {
10✔
158
            try {
159
                $this->readConfiguration(processValues: $processValues);
3✔
160
            } catch (Exception\InvalidConfigurationException $exception) {
2✔
161
                $validationResult = $this->configParser->getLastValidationErrors();
1✔
162

163
                if ($validationResult->isValid()) {
1✔
164
                    throw $exception;
×
165
                }
166

167
                $this->io->error('Your asset configuration is invalid.');
1✔
168
                $this->io->table(
1✔
169
                    ['Config path', 'Error'],
1✔
170
                    array_map($this->validationErrorToTableRow(...), $validationResult->errors()),
1✔
171
                );
1✔
172

173
                return self::ERROR_INVALID_CONFIG;
1✔
174
            }
175

176
            $this->io->success('Your asset configuration is valid.');
1✔
177

178
            return self::SUCCESSFUL;
1✔
179
        }
180

181
        // Read configuration value
182
        if (null === $newValue) {
7✔
183
            [$finalPath, $value] = $this->readConfiguration($path, $processValues);
4✔
184

185
            $this->io->writeln([
2✔
186
                sprintf(
2✔
187
                    'Current configuration value of asset definition <comment>%s</comment>:',
2✔
188
                    $this->decoratePath($finalPath),
2✔
189
                ),
2✔
190
                '',
2✔
191
                $this->printer->print(json_encode($value, JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR)),
2✔
192
                '',
2✔
193
            ]);
2✔
194

195
            return self::SUCCESSFUL;
2✔
196
        }
197

198
        // Write configuration value
199
        if ($json) {
3✔
200
            $jsonValue = \Ergebnis\Json\Json::fromString($newValue);
2✔
201
            $finalPath = $this->writeConfiguration($path, $jsonValue->decoded());
2✔
202

203
            $this->io->writeln([
1✔
204
                sprintf(
1✔
205
                    '<info>Configuration at <comment>%s</comment> was successfully written:</info>',
1✔
206
                    $this->decoratePath($finalPath),
1✔
207
                ),
1✔
208
                '',
1✔
209
                $this->printer->print($jsonValue->encoded()),
1✔
210
                '',
1✔
211
            ]);
1✔
212
        } else {
213
            $finalPath = $this->writeConfiguration($path, $newValue);
1✔
214

215
            $this->io->success(
1✔
216
                sprintf('Configuration at %s was successfully set to "%s".', $this->decoratePath($finalPath), $newValue),
1✔
217
            );
1✔
218
        }
219

220
        return self::SUCCESSFUL;
2✔
221
    }
222

223
    private function writeConfiguration(string $path, mixed $newValue): string
8✔
224
    {
225
        $config = $this->loadConfig(processValues: false);
8✔
226
        $assetDefinitions = $config['frontend-assets'];
7✔
227
        $finalPath = $this->buildAndValidatePath($assetDefinitions, $path);
7✔
228

229
        if (!$this->doWrite($config, $finalPath, $newValue)) {
7✔
230
            throw Exception\FilesystemFailureException::forFailedWriteOperation($finalPath);
×
231
        }
232

233
        return $finalPath;
5✔
234
    }
235

236
    /**
237
     * @return array{string, mixed}
238
     *
239
     * @throws Exception\InvalidConfigurationException
240
     * @throws Exception\MissingConfigurationException
241
     * @throws JsonException
242
     */
243
    private function readConfiguration(string $path = '', bool $processValues = false): array
7✔
244
    {
245
        $config = $this->loadConfig(processValues: $processValues);
7✔
246
        $assetDefinitions = $config['frontend-assets'];
5✔
247

248
        if ('' === $path) {
5✔
249
            return $assetDefinitions;
1✔
250
        }
251

252
        $finalPath = $this->buildAndValidatePath($assetDefinitions, $path);
4✔
253
        $value = Helper\ArrayHelper::getArrayValueByPath($assetDefinitions, $finalPath);
3✔
254

255
        return [$finalPath, $value];
2✔
256
    }
257

258
    /**
259
     * @throws Exception\InvalidConfigurationException
260
     * @throws JsonException
261
     */
262
    private function doWrite(Config\Config $config, string $path, mixed $newValue): bool
7✔
263
    {
264
        if (null === $newValue) {
7✔
265
            // Unset config
266
            $config['frontend-assets'] = Helper\ArrayHelper::unsetArrayValueByPath($config['frontend-assets'], $path);
4✔
267
        } else {
268
            $config['frontend-assets'] = Helper\ArrayHelper::setArrayValueByPath($config['frontend-assets'], $path, $newValue);
3✔
269
        }
270

271
        // Re-index array to avoid invalid schema errors
272
        $config['frontend-assets'] = array_values($config['frontend-assets']);
7✔
273

274
        // Validate new config
275
        if (!$this->validator->validate($config)) {
7✔
276
            throw Exception\InvalidConfigurationException::asReported($this->validator->getLastValidationErrors()->errors());
2✔
277
        }
278

279
        return $this->configFacade->write($config);
5✔
280
    }
281

282
    /**
283
     * @param array<int, array<string, mixed>> $assetDefinitions
284
     *
285
     * @throws Exception\InvalidConfigurationException
286
     */
287
    private function buildAndValidatePath(array $assetDefinitions, string $path): string
11✔
288
    {
289
        $pathSegments = str_getcsv($path, '/', escape: '\\');
11✔
290
        $strictPath = is_numeric($pathSegments[0]);
11✔
291

292
        if (!$strictPath) {
11✔
293
            // Path must not be ambiguous
294
            if (count($assetDefinitions) > 1) {
2✔
295
                throw Exception\InvalidConfigurationException::forAmbiguousKey($path);
1✔
296
            }
297
            array_splice($pathSegments, 0, 0, '0');
1✔
298
        }
299

300
        return implode('/', $pathSegments);
10✔
301
    }
302

303
    private function decoratePath(string $path): string
8✔
304
    {
305
        $pathSegments = array_map(fn (string $segment): string => sprintf('[%s]', $segment), explode('/', $path));
8✔
306

307
        return implode('', $pathSegments);
8✔
308
    }
309

310
    /**
311
     * @return array{string, string}
312
     */
313
    private function validationErrorToTableRow(SchemaValidator\ValidationError $error): array
1✔
314
    {
315
        $path = $error->jsonPointer()->toJsonString();
1✔
316
        $decoratedPath = $this->decoratePath(preg_replace(self::PATH_PREFIX_PATTERN, '', $path) ?? $path);
1✔
317

318
        return [
1✔
319
            sprintf('<comment>%s</comment>', $decoratedPath),
1✔
320
            $error->message()->toString(),
1✔
321
        ];
1✔
322
    }
323
}
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

© 2025 Coveralls, Inc