Passed
Branch master (c976c6)
by Théo
03:51
created

Configuration::validateConfigKeys()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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