Passed
Pull Request — master (#483)
by Théo
11:00
created

ConsoleScoper::scopeFile()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 40
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 20
c 1
b 0
f 0
nc 5
nop 6
dl 0
loc 40
rs 9.6
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Humbug\PhpScoper\Console;
6
7
use Fidry\Console\Application\Application;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Humbug\PhpScoper\Console\Application. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
8
use Fidry\Console\IO;
9
use Humbug\PhpScoper\Autoload\ScoperAutoloadGenerator;
10
use Humbug\PhpScoper\Configuration;
11
use Humbug\PhpScoper\Scoper;
12
use Humbug\PhpScoper\Throwable\Exception\ParsingException;
13
use Symfony\Component\Filesystem\Filesystem;
14
use Throwable;
15
use function array_column;
16
use function array_keys;
17
use function array_map;
18
use function count;
19
use function Humbug\PhpScoper\get_common_path;
20
use function preg_match as native_preg_match;
21
use function Safe\file_get_contents;
22
use function Safe\sprintf;
23
use function Safe\usort;
24
use function str_replace;
25
use function strlen;
26
use const DIRECTORY_SEPARATOR;
27
28
/**
29
 * @private
30
 */
31
final class ConsoleScoper
32
{
33
    private const VENDOR_DIR_PATTERN = '~((?:.*)\\'.DIRECTORY_SEPARATOR.'vendor)\\'.DIRECTORY_SEPARATOR.'.*~';
34
35
    private Filesystem $fileSystem;
36
    private Application $application;
37
    private Scoper $scoper;
38
39
    public function __construct(
40
        Filesystem $fileSystem,
41
        Application $application,
42
        Scoper $scoper
43
    )
44
    {
45
        $this->fileSystem = $fileSystem;
46
        $this->application = $application;
47
        $this->scoper = $scoper;
48
    }
49
50
    public function scope(
51
        IO $io,
52
        Configuration $config,
53
        array $paths,
54
        string $outputDir,
55
        bool $stopOnFailure
56
    ): void
57
    {
58
        $logger = new ScoperLogger(
59
            $this->application,
60
            $io,
61
        );
62
63
        $logger->outputScopingStart(
64
            $config->getPrefix(),
65
            $paths,
66
        );
67
68
        try {
69
            $this->scopeFiles(
70
                $config,
71
                $outputDir,
72
                $stopOnFailure,
73
                $logger,
74
            );
75
        } catch (Throwable $throwable) {
76
            $this->fileSystem->remove($outputDir);
77
78
            $logger->outputScopingEndWithFailure();
79
80
            throw $throwable;
81
        }
82
83
        $logger->outputScopingEnd();
84
    }
85
86
    private function scopeFiles(
87
        Configuration $config,
88
        string $outputDir,
89
        bool $stopOnFailure,
90
        ScoperLogger $logger
91
    ): void {
92
        // Creates output directory if does not already exist
93
        $this->fileSystem->mkdir($outputDir);
94
95
        $files = self::getFiles($config, $outputDir);
96
97
        $logger->outputFileCount(count($files));
98
99
        foreach ($files as [$inputFilePath, $inputContents, $outputFilePath]) {
100
            $this->scopeFile(
101
                $inputFilePath,
102
                $inputContents,
103
                $outputFilePath,
104
                $config,
105
                $stopOnFailure,
106
                $logger,
107
            );
108
        }
109
110
        $vendorDir = self::findVendorDir(array_column($files, 2));
111
112
        if (null !== $vendorDir) {
113
            $autoload = (new ScoperAutoloadGenerator($config->getWhitelist()))->dump();
114
115
            $this->fileSystem->dumpFile(
116
                $vendorDir.DIRECTORY_SEPARATOR.'scoper-autoload.php',
117
                $autoload,
118
            );
119
        }
120
    }
121
122
    /**
123
     * @return array<array{string, string, string}>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<array{string, string, string}> at position 4 could not be parsed: Expected ':' at position 4, but found 'string'.
Loading history...
124
     */
125
    private static function getFiles(Configuration $config, string $outputDir): array
126
    {
127
        $filesWithContent = $config->getFilesWithContents();
128
        $commonPath = get_common_path(array_keys($filesWithContent));
129
130
        return array_map(
131
            static fn (array $inputFileTuple) => [
132
                $inputFileTuple[0],
133
                $inputFileTuple[1],
134
                $outputDir.str_replace($commonPath, '', $inputFileTuple[0]),
135
            ],
136
            $filesWithContent,
137
        );
138
    }
139
140
    private static function findVendorDir(array $outputFilePaths): ?string
141
    {
142
        $vendorDirsAsKeys = [];
143
144
        foreach ($outputFilePaths as $filePath) {
145
            if (native_preg_match(self::VENDOR_DIR_PATTERN, $filePath, $matches)) {
146
                $vendorDirsAsKeys[$matches[1]] = true;
147
            }
148
        }
149
150
        $vendorDirs = array_keys($vendorDirsAsKeys);
151
152
        usort(
153
            $vendorDirs,
154
            static fn ($a, $b) => strlen($b) <=> strlen($a),
155
        );
156
157
        return (0 === count($vendorDirs)) ? null : $vendorDirs[0];
158
    }
159
160
    private function scopeFile(
161
        string $inputFilePath,
162
        string $inputContents,
163
        string $outputFilePath,
164
        Configuration $config,
165
        bool $stopOnFailure,
166
        ScoperLogger $logger
167
    ): void {
168
        try {
169
            $scoppedContent = $this->scoper->scope(
170
                $inputFilePath,
171
                $inputContents,
172
                (string) $config->getPrefix(),
173
                $config->getPatchers(),
174
                $config->getWhitelist(),
175
            );
176
        } catch (Throwable $throwable) {
177
            $exception = new ParsingException(
178
                sprintf(
179
                    'Could not parse the file "%s".',
180
                    $inputFilePath
181
                ),
182
                0,
183
                $throwable,
184
            );
185
186
            if ($stopOnFailure) {
187
                throw $exception;
188
            }
189
190
            $logger->outputWarnOfFailure($inputFilePath, $exception);
191
192
            // Fallback on unchanged content
193
            $scoppedContent = file_get_contents($inputFilePath);
194
        }
195
196
        $this->fileSystem->dumpFile($outputFilePath, $scoppedContent);
197
198
        if (false === isset($exception)) {
199
            $logger->outputSuccess($inputFilePath);
200
        }
201
    }
202
}
203