Passed
Push — master ( 35c1b5...e11383 )
by Théo
01:52
created

ConfigurationFactory   D

Complexity

Total Complexity 58

Size/Duplication

Total Lines 493
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 251
c 1
b 0
f 0
dl 0
loc 493
rs 4.5599
wmc 58

15 Methods

Rating   Name   Duplication   Size   Complexity  
A createWithPrefix() 0 11 1
A retrievePatchers() 0 31 5
A retrieveWhitelistedFiles() 0 35 6
A retrieveFilesWithContents() 0 30 3
A __construct() 0 3 1
A validateConfigKey() 0 10 2
B retrieveFilesFromPaths() 0 37 6
A createWithPaths() 0 20 1
A retrieveFinders() 0 33 5
C retrieveWhitelist() 0 83 11
A validateConfigKeys() 0 5 1
B loadConfigFile() 0 45 8
A retrievePrefix() 0 23 4
A generateRandomPrefix() 0 3 1
A create() 0 36 3

How to fix   Complexity   

Complex Class

Complex classes like ConfigurationFactory often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ConfigurationFactory, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the humbug/php-scoper package.
7
 *
8
 * Copyright (c) 2017 Théo FIDRY <[email protected]>,
9
 *                    Pádraic Brady <[email protected]>
10
 *
11
 * For the full copyright and license information, please view the LICENSE
12
 * file that was distributed with this source code.
13
 */
14
15
namespace Humbug\PhpScoper;
16
17
use Humbug\PhpScoper\Patcher\SymfonyPatcher;
18
use InvalidArgumentException;
19
use Iterator;
20
use RuntimeException;
21
use SplFileInfo;
22
use Symfony\Component\Filesystem\Filesystem;
23
use Symfony\Component\Finder\Finder;
24
use function array_filter;
25
use function array_key_exists;
26
use function array_keys;
27
use function array_map;
28
use function array_merge;
29
use function array_reduce;
30
use function array_unique;
31
use function array_unshift;
32
use function bin2hex;
33
use function dirname;
34
use function file_exists;
35
use function gettype;
36
use function in_array;
37
use function is_array;
38
use function is_bool;
39
use function is_callable;
40
use function is_dir;
41
use function is_file;
42
use function is_link;
43
use function is_readable;
44
use function is_string;
45
use function iterator_to_array;
46
use function random_bytes;
47
use function readlink as native_readlink;
48
use function realpath;
49
use function Safe\file_get_contents;
50
use function Safe\preg_match;
51
use function Safe\sprintf;
52
use function trim;
53
use const DIRECTORY_SEPARATOR;
54
55
final class ConfigurationFactory
56
{
57
    private const PREFIX_KEYWORD = 'prefix';
58
    private const WHITELISTED_FILES_KEYWORD = 'files-whitelist';
59
    private const FINDER_KEYWORD = 'finders';
60
    private const PATCHERS_KEYWORD = 'patchers';
61
    private const WHITELIST_KEYWORD = 'whitelist';
62
    private const WHITELIST_GLOBAL_CONSTANTS_KEYWORD = 'whitelist-global-constants';
63
    private const WHITELIST_GLOBAL_CLASSES_KEYWORD = 'whitelist-global-classes';
64
    private const WHITELIST_GLOBAL_FUNCTIONS_KEYWORD = 'whitelist-global-functions';
65
66
    private const KEYWORDS = [
67
        self::PREFIX_KEYWORD,
68
        self::WHITELISTED_FILES_KEYWORD,
69
        self::FINDER_KEYWORD,
70
        self::PATCHERS_KEYWORD,
71
        self::WHITELIST_KEYWORD,
72
        self::WHITELIST_GLOBAL_CONSTANTS_KEYWORD,
73
        self::WHITELIST_GLOBAL_CLASSES_KEYWORD,
74
        self::WHITELIST_GLOBAL_FUNCTIONS_KEYWORD,
75
    ];
76
77
    private Filesystem $fileSystem;
78
79
    public function __construct(Filesystem $fileSystem)
80
    {
81
        $this->fileSystem = $fileSystem;
82
    }
83
84
    /**
85
     * @param string|null $path  Absolute path to the configuration file.
86
     * @param string[]    $paths List of paths to append besides the one configured
87
     */
88
    public function create(?string $path = null, array $paths = []): Configuration
89
    {
90
        if (null === $path) {
91
            $config = [];
92
        } else {
93
            $config = $this->loadConfigFile($path);
94
        }
95
96
        self::validateConfigKeys($config);
97
98
        $prefix = self::retrievePrefix($config);
99
100
        $whitelistedFiles = null === $path
101
            ? []
102
            : $this->retrieveWhitelistedFiles(
103
                dirname($path),
104
                $config,
105
            );
106
107
        $patchers = self::retrievePatchers($config);
108
109
        array_unshift($patchers, new SymfonyPatcher());
110
111
        $whitelist = self::retrieveWhitelist($config);
112
113
        $finders = self::retrieveFinders($config);
114
        $filesFromPaths = self::retrieveFilesFromPaths($paths);
115
        $filesWithContents = self::retrieveFilesWithContents(chain($filesFromPaths, ...$finders));
116
117
        return new Configuration(
118
            $path,
119
            $prefix,
120
            $filesWithContents,
121
            $patchers,
122
            $whitelist,
123
            $whitelistedFiles,
124
        );
125
    }
126
127
    /**
128
     * @param string[] $paths
129
     */
130
    public function createWithPaths(Configuration $config, array $paths): Configuration
131
    {
132
        $filesWithContents = self::retrieveFilesWithContents(
133
            chain(
134
                self::retrieveFilesFromPaths(
135
                    array_unique($paths),
136
                ),
137
            ),
138
        );
139
140
        return new Configuration(
141
            $config->getPath(),
142
            $config->getPrefix(),
143
            array_merge(
144
                $config->getFilesWithContents(),
145
                $filesWithContents,
146
            ),
147
            $config->getPatchers(),
148
            $config->getWhitelist(),
149
            $config->getWhitelistedFiles(),
150
        );
151
    }
152
153
    public function createWithPrefix(Configuration $config, string $prefix): Configuration
154
    {
155
        $prefix = self::retrievePrefix([self::PREFIX_KEYWORD => $prefix]);
156
157
        return new Configuration(
158
            $config->getPath(),
159
            $prefix,
160
            $config->getFilesWithContents(),
161
            $config->getPatchers(),
162
            $config->getWhitelist(),
163
            $config->getWhitelistedFiles(),
164
        );
165
    }
166
167
    private function loadConfigFile(string $path): array
168
    {
169
        if (false === $this->fileSystem->isAbsolutePath($path)) {
170
            throw new InvalidArgumentException(
171
                sprintf(
172
                    'Expected the path of the configuration file to load to be an absolute path, got "%s" instead',
173
                    $path,
174
                ),
175
            );
176
        }
177
178
        if (false === file_exists($path)) {
179
            throw new InvalidArgumentException(
180
                sprintf(
181
                    'Expected the path of the configuration file to exists but the file "%s" could not be found',
182
                    $path,
183
                ),
184
            );
185
        }
186
187
        $isADirectoryLink = is_link($path)
188
            && false !== native_readlink($path)
189
            && is_file(native_readlink($path));
190
191
        if (false === is_file($path) && false === $isADirectoryLink) {
192
            throw new InvalidArgumentException(
193
                sprintf(
194
                    'Expected the path of the configuration file to be a file but "%s" appears to be a directory.',
195
                    $path
196
                )
197
            );
198
        }
199
200
        $config = include $path;
201
202
        if (false === is_array($config)) {
203
            throw new InvalidArgumentException(
204
                sprintf(
205
                    'Expected configuration to be an array, found "%s" instead.',
206
                    gettype($config)
207
                )
208
            );
209
        }
210
211
        return $config;
212
    }
213
214
    private static function validateConfigKeys(array $config): void
215
    {
216
        array_map(
217
            [self::class, 'validateConfigKey'],
218
            array_keys($config),
219
        );
220
    }
221
222
    private static function validateConfigKey(string $key): void
223
    {
224
        if (in_array($key, self::KEYWORDS, true)) {
225
            return;
226
        }
227
228
        throw new InvalidArgumentException(
229
            sprintf(
230
                'Invalid configuration key value "%s" found.',
231
                $key,
232
            ),
233
        );
234
    }
235
236
    private static function retrievePrefix(array $config): string
237
    {
238
        $prefix = $config[self::PREFIX_KEYWORD] ?? null;
239
240
        if (null === $prefix) {
241
            return self::generateRandomPrefix();
242
        }
243
244
        $prefix = trim($prefix);
245
246
        if ('' === $prefix) {
247
            return self::generateRandomPrefix();
248
        }
249
250
        // TODO: move the prefix validation to the Configuration class
251
        if (1 === preg_match('/^[\p{L}\d_]+$/u', $prefix)) {
252
            return $prefix;
253
        }
254
255
        throw new InvalidArgumentException(
256
            sprintf(
257
                'The prefix needs to be composed solely of letters and digits. Got "%s"',
258
                $prefix,
259
            ),
260
        );
261
    }
262
263
    /**
264
     * @return callable[]
265
     */
266
    private static function retrievePatchers(array $config): array
267
    {
268
        if (false === array_key_exists(self::PATCHERS_KEYWORD, $config)) {
269
            return [];
270
        }
271
272
        $patchers = $config[self::PATCHERS_KEYWORD];
273
274
        if (false === is_array($patchers)) {
275
            throw new InvalidArgumentException(
276
                sprintf(
277
                    'Expected patchers to be an array of callables, found "%s" instead.',
278
                    gettype($patchers)
279
                )
280
            );
281
        }
282
283
        foreach ($patchers as $index => $patcher) {
284
            if (is_callable($patcher)) {
285
                continue;
286
            }
287
288
            throw new InvalidArgumentException(
289
                sprintf(
290
                    'Expected patchers to be an array of callables, the "%d" element is not.',
291
                    $index
292
                )
293
            );
294
        }
295
296
        return $patchers;
297
    }
298
299
    private static function retrieveWhitelist(array $config): Whitelist
300
    {
301
        if (false === array_key_exists(self::WHITELIST_KEYWORD, $config)) {
302
            $whitelist = [];
303
        } else {
304
            $whitelist = $config[self::WHITELIST_KEYWORD];
305
306
            if (false === is_array($whitelist)) {
307
                throw new InvalidArgumentException(
308
                    sprintf(
309
                        'Expected whitelist to be an array of strings, found "%s" instead.',
310
                        gettype($whitelist)
311
                    )
312
                );
313
            }
314
315
            foreach ($whitelist as $index => $className) {
316
                if (is_string($className)) {
317
                    continue;
318
                }
319
320
                throw new InvalidArgumentException(
321
                    sprintf(
322
                        'Expected whitelist to be an array of string, the "%d" element is not.',
323
                        $index
324
                    )
325
                );
326
            }
327
        }
328
329
        if (false === array_key_exists(self::WHITELIST_GLOBAL_CONSTANTS_KEYWORD, $config)) {
330
            $whitelistGlobalConstants = true;
331
        } else {
332
            $whitelistGlobalConstants = $config[self::WHITELIST_GLOBAL_CONSTANTS_KEYWORD];
333
334
            if (false === is_bool($whitelistGlobalConstants)) {
335
                throw new InvalidArgumentException(
336
                    sprintf(
337
                        'Expected %s to be a boolean, found "%s" instead.',
338
                        self::WHITELIST_GLOBAL_CONSTANTS_KEYWORD,
339
                        gettype($whitelistGlobalConstants)
340
                    )
341
                );
342
            }
343
        }
344
345
        if (false === array_key_exists(self::WHITELIST_GLOBAL_CLASSES_KEYWORD, $config)) {
346
            $whitelistGlobalClasses = true;
347
        } else {
348
            $whitelistGlobalClasses = $config[self::WHITELIST_GLOBAL_CLASSES_KEYWORD];
349
350
            if (false === is_bool($whitelistGlobalClasses)) {
351
                throw new InvalidArgumentException(
352
                    sprintf(
353
                        'Expected %s to be a boolean, found "%s" instead.',
354
                        self::WHITELIST_GLOBAL_CLASSES_KEYWORD,
355
                        gettype($whitelistGlobalClasses)
356
                    )
357
                );
358
            }
359
        }
360
361
        if (false === array_key_exists(self::WHITELIST_GLOBAL_FUNCTIONS_KEYWORD, $config)) {
362
            $whitelistGlobalFunctions = true;
363
        } else {
364
            $whitelistGlobalFunctions = $config[self::WHITELIST_GLOBAL_FUNCTIONS_KEYWORD];
365
366
            if (false === is_bool($whitelistGlobalFunctions)) {
367
                throw new InvalidArgumentException(
368
                    sprintf(
369
                        'Expected %s to be a boolean, found "%s" instead.',
370
                        self::WHITELIST_GLOBAL_FUNCTIONS_KEYWORD,
371
                        gettype($whitelistGlobalFunctions)
372
                    )
373
                );
374
            }
375
        }
376
377
        return Whitelist::create(
378
            $whitelistGlobalConstants,
379
            $whitelistGlobalClasses,
380
            $whitelistGlobalFunctions,
381
            ...$whitelist,
382
        );
383
    }
384
385
    /**
386
     * @return string[] Absolute paths
387
     */
388
    private function retrieveWhitelistedFiles(string $dirPath, array $config): array
389
    {
390
        if (false === array_key_exists(self::WHITELISTED_FILES_KEYWORD, $config)) {
391
            return [];
392
        }
393
394
        $whitelistedFiles = $config[self::WHITELISTED_FILES_KEYWORD];
395
396
        if (false === is_array($whitelistedFiles)) {
397
            throw new InvalidArgumentException(
398
                sprintf(
399
                    'Expected whitelisted files to be an array of strings, found "%s" instead.',
400
                    gettype($whitelistedFiles)
401
                )
402
            );
403
        }
404
405
        foreach ($whitelistedFiles as $index => $file) {
406
            if (false === is_string($file)) {
407
                throw new InvalidArgumentException(
408
                    sprintf(
409
                        'Expected whitelisted files to be an array of string, the "%d" element is not.',
410
                        $index
411
                    )
412
                );
413
            }
414
415
            if (false === $this->fileSystem->isAbsolutePath($file)) {
416
                $file = $dirPath.DIRECTORY_SEPARATOR.$file;
417
            }
418
419
            $whitelistedFiles[$index] = realpath($file);
420
        }
421
422
        return array_filter($whitelistedFiles);
423
    }
424
425
    /**
426
     * @return Finder[]
427
     */
428
    private static function retrieveFinders(array $config): array
429
    {
430
        if (false === array_key_exists(self::FINDER_KEYWORD, $config)) {
431
            return [];
432
        }
433
434
        $finders = $config[self::FINDER_KEYWORD];
435
436
        if (false === is_array($finders)) {
437
            throw new InvalidArgumentException(
438
                sprintf(
439
                    'Expected finders to be an array of "%s", found "%s" instead.',
440
                    Finder::class,
441
                    gettype($finders)
442
                )
443
            );
444
        }
445
446
        foreach ($finders as $index => $finder) {
447
            if ($finder instanceof Finder) {
448
                continue;
449
            }
450
451
            throw new InvalidArgumentException(
452
                sprintf(
453
                    'Expected finders to be an array of "%s", the "%d" element is not.',
454
                    Finder::class,
455
                    $index
456
                )
457
            );
458
        }
459
460
        return $finders;
461
    }
462
463
    /**
464
     * @param string[] $paths
465
     *
466
     * @return iterable<SplFileInfo>
467
     */
468
    private static function retrieveFilesFromPaths(array $paths): iterable
469
    {
470
        if ([] === $paths) {
471
            return [];
472
        }
473
474
        $pathsToSearch = [];
475
        $filesToAppend = [];
476
477
        foreach ($paths as $path) {
478
            if (false === file_exists($path)) {
479
                throw new RuntimeException(
480
                    sprintf(
481
                        'Could not find the file "%s".',
482
                        $path
483
                    )
484
                );
485
            }
486
487
            if (is_dir($path)) {
488
                $pathsToSearch[] = $path;
489
            } else {
490
                $filesToAppend[] = $path;
491
            }
492
        }
493
494
        $finder = new Finder();
495
496
        $finder->files()
497
            ->in($pathsToSearch)
498
            ->append($filesToAppend)
499
            ->filter(
500
                static fn (SplFileInfo $fileInfo) => $fileInfo->isLink() ? false : null
501
            )
502
            ->sortByName();
503
504
        return $finder;
505
    }
506
507
    /**
508
     * @param Iterator<SplFileInfo> $files
509
     *
510
     * @return array<string, array{string, string}> Array of tuple with the first argument being the file path and the second its contents
1 ignored issue
show
Documentation Bug introduced by
The doc comment array<string, array{string, string}> at position 6 could not be parsed: Expected ':' at position 6, but found 'string'.
Loading history...
511
     */
512
    private static function retrieveFilesWithContents(Iterator $files): array
513
    {
514
        return array_reduce(
515
            iterator_to_array($files, false),
516
            static function (array $files, SplFileInfo $fileInfo): array {
517
                $file = $fileInfo->getRealPath();
518
519
                if (false === $file) {
520
                    throw new RuntimeException(
521
                        sprintf(
522
                            'Could not find the file "%s".',
523
                            (string) $fileInfo,
524
                        ),
525
                    );
526
                }
527
528
                if (false === is_readable($file)) {
529
                    throw new RuntimeException(
530
                        sprintf(
531
                            'Could not read the file "%s".',
532
                            $file,
533
                        ),
534
                    );
535
                }
536
537
                $files[$fileInfo->getRealPath()] = [$fileInfo->getRealPath(), file_get_contents($file)];
538
539
                return $files;
540
            },
541
            [],
542
        );
543
    }
544
545
    private static function generateRandomPrefix(): string
546
    {
547
        return '_PhpScoper'.bin2hex(random_bytes(6));
548
    }
549
}
550