Passed
Push — master ( dc8be2...e43899 )
by Théo
03:04 queued 41s
created

ConfigurationFactory   D

Complexity

Total Complexity 58

Size/Duplication

Total Lines 477
Duplicated Lines 0 %

Importance

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