Passed
Push — master ( 371c66...7106c5 )
by Théo
01:55
created

ConfigurationFactory::retrieveExcludedFiles()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 35
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 18
nc 6
nop 2
dl 0
loc 35
rs 9.0444
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\Configuration;
16
17
use Humbug\PhpScoper\Patcher\ComposerPatcher;
18
use Humbug\PhpScoper\Patcher\Patcher;
19
use Humbug\PhpScoper\Patcher\PatcherChain;
20
use Humbug\PhpScoper\Patcher\SymfonyPatcher;
21
use InvalidArgumentException;
22
use RuntimeException;
23
use SplFileInfo;
24
use Symfony\Component\Filesystem\Filesystem;
25
use Symfony\Component\Finder\Finder;
26
use function array_filter;
27
use function array_key_exists;
28
use function array_keys;
29
use function array_map;
30
use function array_merge;
31
use function array_unique;
32
use function array_unshift;
33
use function array_values;
34
use function bin2hex;
35
use function dirname;
36
use function file_exists;
37
use function gettype;
38
use function Humbug\PhpScoper\chain;
39
use function in_array;
40
use function is_array;
41
use function is_callable;
42
use function is_dir;
43
use function is_file;
44
use function is_link;
45
use function is_readable;
46
use function is_string;
47
use function random_bytes;
48
use function readlink as native_readlink;
49
use function realpath;
50
use function Safe\file_get_contents;
51
use function Safe\sprintf;
52
use function trim;
53
use const DIRECTORY_SEPARATOR;
54
use const SORT_STRING;
55
56
final class ConfigurationFactory
57
{
58
    private Filesystem $fileSystem;
59
    private SymbolsConfigurationFactory $configurationWhitelistFactory;
60
61
    public function __construct(
62
        Filesystem $fileSystem,
63
        SymbolsConfigurationFactory $configurationWhitelistFactory
64
    ) {
65
        $this->fileSystem = $fileSystem;
66
        $this->configurationWhitelistFactory = $configurationWhitelistFactory;
67
    }
68
69
    /**
70
     * @param string|null $path  Absolute path to the configuration file.
71
     * @param string[]    $paths List of paths to append besides the one configured
72
     */
73
    public function create(?string $path = null, array $paths = []): Configuration
74
    {
75
        if (null === $path) {
76
            $config = [];
77
        } else {
78
            $config = $this->loadConfigFile($path);
79
        }
80
81
        self::validateConfigKeys($config);
82
83
        $prefix = self::retrievePrefix($config);
84
85
        $excludedFiles = null === $path
86
            ? []
87
            : $this->retrieveExcludedFiles(
88
                dirname($path),
89
                $config,
90
            );
91
92
        $patchers = self::retrievePatchers($config);
93
94
        array_unshift($patchers, new SymfonyPatcher());
95
        array_unshift($patchers, new ComposerPatcher());
96
97
        $symbolsConfiguration = $this->configurationWhitelistFactory->createSymbolsConfiguration($config);
98
99
        $finders = self::retrieveFinders($config);
100
        $filesFromPaths = self::retrieveFilesFromPaths($paths);
101
        $filesWithContents = self::retrieveFilesWithContents(chain($filesFromPaths, ...$finders));
102
103
        return new Configuration(
104
            $path,
105
            $prefix,
106
            $filesWithContents,
107
            self::retrieveFilesWithContents($excludedFiles),
108
            new PatcherChain($patchers),
109
            $symbolsConfiguration,
110
        );
111
    }
112
113
    /**
114
     * @param string[] $paths
115
     */
116
    public function createWithPaths(Configuration $config, array $paths): Configuration
117
    {
118
        $filesWithContents = self::retrieveFilesWithContents(
119
            chain(
120
                self::retrieveFilesFromPaths(
121
                    array_unique($paths, SORT_STRING),
122
                ),
123
            ),
124
        );
125
126
        return new Configuration(
127
            $config->getPath(),
128
            $config->getPrefix(),
129
            array_merge(
130
                $config->getFilesWithContents(),
131
                $filesWithContents,
132
            ),
133
            $config->getExcludedFilesWithContents(),
134
            $config->getPatcher(),
135
            $config->getSymbolsConfiguration(),
136
        );
137
    }
138
139
    public function createWithPrefix(Configuration $config, string $prefix): Configuration
140
    {
141
        $prefix = self::retrievePrefix([ConfigurationKeys::PREFIX_KEYWORD => $prefix]);
142
143
        return new Configuration(
144
            $config->getPath(),
145
            $prefix,
146
            $config->getFilesWithContents(),
147
            $config->getExcludedFilesWithContents(),
148
            $config->getPatcher(),
149
            $config->getSymbolsConfiguration(),
150
        );
151
    }
152
153
    private function loadConfigFile(string $path): array
154
    {
155
        if (!$this->fileSystem->isAbsolutePath($path)) {
156
            throw new InvalidArgumentException(
157
                sprintf(
158
                    'Expected the path of the configuration file to load to be an absolute path, got "%s" instead',
159
                    $path,
160
                ),
161
            );
162
        }
163
164
        if (!file_exists($path)) {
165
            throw new InvalidArgumentException(
166
                sprintf(
167
                    'Expected the path of the configuration file to exists but the file "%s" could not be found',
168
                    $path,
169
                ),
170
            );
171
        }
172
173
        $isADirectoryLink = is_link($path)
174
            && false !== native_readlink($path)
175
            && is_file(native_readlink($path));
176
177
        if (!$isADirectoryLink && !is_file($path)) {
178
            throw new InvalidArgumentException(
179
                sprintf(
180
                    'Expected the path of the configuration file to be a file but "%s" appears to be a directory.',
181
                    $path,
182
                ),
183
            );
184
        }
185
186
        $config = include $path;
187
188
        if (!is_array($config)) {
189
            throw new InvalidArgumentException(
190
                sprintf(
191
                    'Expected configuration to be an array, found "%s" instead.',
192
                    gettype($config),
193
                ),
194
            );
195
        }
196
197
        return $config;
198
    }
199
200
    private static function validateConfigKeys(array $config): void
201
    {
202
        array_map(
203
            static fn (string $key) => self::validateConfigKey($key),
204
            array_keys($config),
205
        );
206
    }
207
208
    private static function validateConfigKey(string $key): void
209
    {
210
        if (in_array($key, ConfigurationKeys::KEYWORDS, true)) {
211
            return;
212
        }
213
214
        throw new InvalidArgumentException(
215
            sprintf(
216
                'Invalid configuration key value "%s" found.',
217
                $key,
218
            ),
219
        );
220
    }
221
222
    private static function retrievePrefix(array $config): string
223
    {
224
        $prefix = trim((string) ($config[ConfigurationKeys::PREFIX_KEYWORD] ?? ''));
225
226
        if ('' === $prefix) {
227
            return self::generateRandomPrefix();
228
        }
229
230
        return $prefix;
231
    }
232
233
    /**
234
     * @return array<(callable(string,string,string): string)|Patcher>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<(callable(string,s...ring): string)|Patcher> at position 3 could not be parsed: Expected ')' at position 3, but found 'callable'.
Loading history...
235
     */
236
    private static function retrievePatchers(array $config): array
237
    {
238
        if (!array_key_exists(ConfigurationKeys::PATCHERS_KEYWORD, $config)) {
239
            return [];
240
        }
241
242
        $patchers = $config[ConfigurationKeys::PATCHERS_KEYWORD];
243
244
        if (!is_array($patchers)) {
245
            throw new InvalidArgumentException(
246
                sprintf(
247
                    'Expected patchers to be an array of callables, found "%s" instead.',
248
                    gettype($patchers),
249
                ),
250
            );
251
        }
252
253
        foreach ($patchers as $index => $patcher) {
254
            if (is_callable($patcher)) {
255
                continue;
256
            }
257
258
            throw new InvalidArgumentException(
259
                sprintf(
260
                    'Expected patchers to be an array of callables, the "%d" element is not.',
261
                    $index,
262
                ),
263
            );
264
        }
265
266
        return $patchers;
267
    }
268
269
    /**
270
     * @return string[] Absolute paths
271
     */
272
    private function retrieveExcludedFiles(string $dirPath, array $config): array
273
    {
274
        if (!array_key_exists(ConfigurationKeys::EXCLUDED_FILES_KEYWORD, $config)) {
275
            return [];
276
        }
277
278
        $excludedFiles = $config[ConfigurationKeys::EXCLUDED_FILES_KEYWORD];
279
280
        if (!is_array($excludedFiles)) {
281
            throw new InvalidArgumentException(
282
                sprintf(
283
                    'Expected excluded files to be an array of strings, found "%s" instead.',
284
                    gettype($excludedFiles),
285
                ),
286
            );
287
        }
288
289
        foreach ($excludedFiles as $index => $file) {
290
            if (!is_string($file)) {
291
                throw new InvalidArgumentException(
292
                    sprintf(
293
                        'Expected excluded files to be an array of string, the "%d" element is not.',
294
                        $index,
295
                    ),
296
                );
297
            }
298
299
            if (!$this->fileSystem->isAbsolutePath($file)) {
300
                $file = $dirPath.DIRECTORY_SEPARATOR.$file;
301
            }
302
303
            $excludedFiles[$index] = realpath($file);
304
        }
305
306
        return array_filter($excludedFiles);
307
    }
308
309
    /**
310
     * @return Finder[]
311
     */
312
    private static function retrieveFinders(array $config): array
313
    {
314
        if (!array_key_exists(ConfigurationKeys::FINDER_KEYWORD, $config)) {
315
            return [];
316
        }
317
318
        $finders = $config[ConfigurationKeys::FINDER_KEYWORD];
319
320
        if (!is_array($finders)) {
321
            throw new InvalidArgumentException(
322
                sprintf(
323
                    'Expected finders to be an array of "%s", found "%s" instead.',
324
                    Finder::class,
325
                    gettype($finders),
326
                ),
327
            );
328
        }
329
330
        foreach ($finders as $index => $finder) {
331
            if ($finder instanceof Finder) {
332
                continue;
333
            }
334
335
            throw new InvalidArgumentException(
336
                sprintf(
337
                    'Expected finders to be an array of "%s", the "%d" element is not.',
338
                    Finder::class,
339
                    $index,
340
                ),
341
            );
342
        }
343
344
        return $finders;
345
    }
346
347
    /**
348
     * @param string[] $paths
349
     *
350
     * @return iterable<SplFileInfo>
351
     */
352
    private static function retrieveFilesFromPaths(array $paths): iterable
353
    {
354
        if ([] === $paths) {
355
            return [];
356
        }
357
358
        $pathsToSearch = [];
359
        $filesToAppend = [];
360
361
        foreach ($paths as $path) {
362
            if (!file_exists($path)) {
363
                throw new RuntimeException(
364
                    sprintf(
365
                        'Could not find the file "%s".',
366
                        $path,
367
                    ),
368
                );
369
            }
370
371
            if (is_dir($path)) {
372
                $pathsToSearch[] = $path;
373
            } else {
374
                $filesToAppend[] = $path;
375
            }
376
        }
377
378
        $finder = new Finder();
379
380
        $finder->files()
381
            ->in($pathsToSearch)
382
            ->append($filesToAppend)
383
            ->filter(
384
                static fn (SplFileInfo $fileInfo) => $fileInfo->isLink() ? false : null,
385
            )
386
            ->sortByName();
387
388
        return $finder;
389
    }
390
391
    /**
392
     * @param iterable<SplFileInfo|string> $files
393
     *
394
     * @return array<string, array{string, string}> Array of tuple with the first argument being the file path and the second its contents
395
     */
396
    private static function retrieveFilesWithContents(iterable $files): array
397
    {
398
        $filesWithContents = [];
399
400
        foreach ($files as $filePathOrFileInfo) {
401
            $filePath = $filePathOrFileInfo instanceof SplFileInfo
402
                ? $filePathOrFileInfo->getRealPath()
403
                : realpath($filePathOrFileInfo);
404
405
            if (!$filePath) {
406
                throw new RuntimeException(
407
                    sprintf(
408
                        'Could not find the file "%s".',
409
                        (string) $filePathOrFileInfo,
410
                    ),
411
                );
412
            }
413
414
            if (!is_readable($filePath)) {
415
                throw new RuntimeException(
416
                    sprintf(
417
                        'Could not read the file "%s".',
418
                        $filePath,
419
                    ),
420
                );
421
            }
422
423
            $filesWithContents[$filePath] = [$filePath, file_get_contents($filePath)];
424
        }
425
426
        return $filesWithContents;
427
    }
428
429
    private static function generateRandomPrefix(): string
430
    {
431
        return '_PhpScoper'.bin2hex(random_bytes(6));
432
    }
433
}
434