Passed
Pull Request — master (#494)
by Théo
02:07
created

ConfigurationFactory::loadConfigFile()   B

Complexity

Conditions 8
Paths 11

Size

Total Lines 45
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

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