Passed
Push — master ( b98030...ead925 )
by Théo
04:12 queued 02:03
created

ConfigurationFactory   C

Complexity

Total Complexity 53

Size/Duplication

Total Lines 431
Duplicated Lines 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 207
dl 0
loc 431
rs 6.96
c 3
b 0
f 0
wmc 53

16 Methods

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