Passed
Pull Request — master (#489)
by Théo
02:17
created

ConfigurationFactory   C

Complexity

Total Complexity 56

Size/Duplication

Total Lines 482
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 246
c 2
b 0
f 0
dl 0
loc 482
rs 5.5199
wmc 56

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