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

Configuration::retrievePrefix()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 22
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 6.5971

Importance

Changes 0
Metric Value
cc 4
eloc 12
c 0
b 0
f 0
nc 4
nop 1
dl 0
loc 22
ccs 5
cts 11
cp 0.4545
crap 6.5971
rs 9.8666
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