Passed
Push — master ( 338f36...430aa2 )
by Théo
02:06
created

Configuration::retrieveWhitelist()   B

Complexity

Conditions 11
Paths 32

Size

Total Lines 79
Code Lines 47

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 35.2741

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 11
eloc 47
c 1
b 0
f 0
nc 32
nop 1
dl 0
loc 79
ccs 17
cts 41
cp 0.4146
crap 35.2741
rs 7.3166

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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