Completed
Push — master ( f9f508...eedf13 )
by Théo
07:31 queued 04:23
created

AddPrefixCommand   B

Complexity

Total Complexity 39

Size/Duplication

Total Lines 426
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 12

Test Coverage

Coverage 77.42%

Importance

Changes 0
Metric Value
dl 0
loc 426
ccs 168
cts 217
cp 0.7742
rs 8.2857
c 0
b 0
f 0
wmc 39
lcom 2
cbo 12

11 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 7 1
A configure() 0 54 1
B execute() 0 46 2
B scopeFile() 0 37 4
A validatePrefix() 0 10 3
B validateOutputDir() 0 61 8
C retrieveConfig() 0 74 8
A retrievePaths() 0 11 3
A makeAbsolutePath() 0 8 2
B scopeFiles() 0 55 5
A validatePaths() 0 18 2
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\Console\Command;
16
17
use Humbug\PhpScoper\Autoload\ScoperAutoloadGenerator;
18
use Humbug\PhpScoper\Configuration;
19
use Humbug\PhpScoper\Logger\ConsoleLogger;
20
use Humbug\PhpScoper\Scoper;
21
use Humbug\PhpScoper\Throwable\Exception\ParsingException;
22
use Symfony\Component\Console\Exception\RuntimeException;
23
use Symfony\Component\Console\Input\InputArgument;
24
use Symfony\Component\Console\Input\InputInterface;
25
use Symfony\Component\Console\Input\InputOption;
26
use Symfony\Component\Console\Input\StringInput;
27
use Symfony\Component\Console\Output\OutputInterface;
28
use Symfony\Component\Console\Style\OutputStyle;
29
use Symfony\Component\Console\Style\SymfonyStyle;
30
use Symfony\Component\Filesystem\Filesystem;
31
use Throwable;
32
use function Humbug\PhpScoper\get_common_path;
33
34
final class AddPrefixCommand extends BaseCommand
35
{
36
    private const PATH_ARG = 'paths';
37
    private const PREFIX_OPT = 'prefix';
38
    private const OUTPUT_DIR_OPT = 'output-dir';
39
    private const FORCE_OPT = 'force';
40
    private const STOP_ON_FAILURE_OPT = 'stop-on-failure';
41
    private const CONFIG_FILE_OPT = 'config';
42
    private const CONFIG_FILE_DEFAULT = 'scoper.inc.php';
43
    private const NO_CONFIG_OPT = 'no-config';
44
45
    private $fileSystem;
46
    private $scoper;
47
    private $init = false;
48
49
    /**
50
     * @inheritdoc
51
     */
52 16
    public function __construct(Filesystem $fileSystem, Scoper $scoper)
53
    {
54 16
        parent::__construct();
55
56 16
        $this->fileSystem = $fileSystem;
57 16
        $this->scoper = $scoper;
58
    }
59
60
    /**
61
     * @inheritdoc
62
     */
63 16
    protected function configure(): void
64
    {
65 16
        parent::configure();
66
67
        $this
68 16
            ->setName('add-prefix')
69 16
            ->setDescription('Goes through all the PHP files found in the given paths to apply the given prefix to namespaces & FQNs.')
70 16
            ->addArgument(
71 16
                self::PATH_ARG,
72 16
                InputArgument::IS_ARRAY,
73 16
                'The path(s) to process.'
74
            )
75 16
            ->addOption(
76 16
                self::PREFIX_OPT,
77 16
                'p',
78 16
                InputOption::VALUE_REQUIRED,
79 16
                'The namespace prefix to add.'
80
            )
81 16
            ->addOption(
82 16
                self::OUTPUT_DIR_OPT,
83 16
                'o',
84 16
                InputOption::VALUE_REQUIRED,
85 16
                'The output directory in which the prefixed code will be dumped.',
86 16
                'build'
87
            )
88 16
            ->addOption(
89 16
                self::FORCE_OPT,
90 16
                'f',
91 16
                InputOption::VALUE_NONE,
92 16
                'Deletes any existing content in the output directory without any warning.'
93
            )
94 16
            ->addOption(
95 16
                self::STOP_ON_FAILURE_OPT,
96 16
                's',
97 16
                InputOption::VALUE_NONE,
98 16
                'Stops on failure.'
99
            )
100 16
            ->addOption(
101 16
                self::CONFIG_FILE_OPT,
102 16
                'c',
103 16
                InputOption::VALUE_REQUIRED,
104 16
                sprintf(
105 16
                    'Configuration file. Will use "%s" if found by default.',
106 16
                    self::CONFIG_FILE_DEFAULT
107
                )
108
            )
109 16
            ->addOption(
110 16
                self::NO_CONFIG_OPT,
111 16
                null,
112 16
                InputOption::VALUE_NONE,
113 16
            'Do not look for a configuration file.'
114
            )
115
        ;
116
    }
117
118
    /**
119
     * @inheritdoc
120
     */
121 14
    protected function execute(InputInterface $input, OutputInterface $output): int
122
    {
123 14
        $io = new SymfonyStyle($input, $output);
124 14
        $io->writeln('');
125
126 14
        $this->changeWorkingDirectory($input);
127
128 14
        $this->validatePrefix($input);
129 14
        $this->validatePaths($input);
130 14
        $this->validateOutputDir($input, $io);
131
132 14
        $config = $this->retrieveConfig($input, $output, $io);
133 12
        $output = $input->getOption(self::OUTPUT_DIR_OPT);
134
135 12
        $logger = new ConsoleLogger(
136 12
            $this->getApplication(),
137 12
            $io
138
        );
139
140 12
        $logger->outputScopingStart(
141 12
            $config->getPrefix(),
142 12
            $input->getArgument(self::PATH_ARG)
143
        );
144
145
        try {
146 12
            $this->scopeFiles(
147 12
                $config->getPrefix(),
148 12
                $config->getFilesWithContents(),
149 12
                $output,
150 12
                $config->getPatchers(),
151 12
                $config->getWhitelist(),
152 12
                $input->getOption(self::STOP_ON_FAILURE_OPT),
153 12
                $logger
154
            );
155
        } catch (Throwable $throwable) {
156
            $this->fileSystem->remove($output);
157
158
            $logger->outputScopingEndWithFailure();
159
160
            throw $throwable;
161
        }
162
163 12
        $logger->outputScopingEnd();
164
165 12
        return 0;
166
    }
167
168 12
    private function scopeFiles(
169
        string $prefix,
170
        array $filesWithContents,
171
        string $output,
172
        array $patchers,
173
        array $whitelist,
174
        bool $stopOnFailure,
175
        ConsoleLogger $logger
176
    ): void {
177
        // Creates output directory if does not already exist
178 12
        $this->fileSystem->mkdir($output);
179
180 12
        $logger->outputFileCount(count($filesWithContents));
181
182 12
        $vendorDirs = [];
183 12
        $commonPath = get_common_path(array_keys($filesWithContents));
184
185 12
        foreach ($filesWithContents as $fileWithContents) {
186 12
            [$inputFilePath, $inputContents] = $fileWithContents;
0 ignored issues
show
Bug introduced by
The variable $inputFilePath does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $inputContents does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
187
188 12
            $outputFilePath = $output.str_replace($commonPath, '', $inputFilePath);
189
190 12
            if (preg_match('~((?:.*)\/vendor)\/.*~', $outputFilePath, $matches)) {
191
                $vendorDirs[$matches[1]] = true;
192
            }
193
194 12
            $this->scopeFile(
195 12
                $inputFilePath,
196 12
                $inputContents,
197 12
                $outputFilePath,
198 12
                $prefix,
199 12
                $patchers,
200 12
                $whitelist,
201 12
                $stopOnFailure,
202 12
                $logger
203
            );
204
        }
205
206 12
        $vendorDirs = array_keys($vendorDirs);
207
208 12
        usort(
209 12
            $vendorDirs,
210
            function ($a, $b) {
211
                return strlen($b) <=> strlen($a);
212 12
            }
213
        );
214
215 12
        $vendorDir = (0 === count($vendorDirs)) ? null : $vendorDirs[0];
216
217 12
        if (null !== $vendorDir) {
218
            $autoload = (new ScoperAutoloadGenerator($whitelist))->dump($prefix);
219
220
            $this->fileSystem->dumpFile($vendorDir.'/scoper-autoload.php', $autoload);
221
        }
222
    }
223
224
    /**
225
     * @param string        $inputFilePath
226
     * @param string        $outputFilePath
227
     * @param string        $inputContents
228
     * @param string        $prefix
229
     * @param callable[]    $patchers
230
     * @param string[]      $whitelist
231
     * @param bool          $stopOnFailure
232
     * @param ConsoleLogger $logger
233
     */
234 12
    private function scopeFile(
235
        string $inputFilePath,
236
        string $inputContents,
237
        string $outputFilePath,
238
        string $prefix,
239
        array $patchers,
240
        array $whitelist,
241
        bool $stopOnFailure,
242
        ConsoleLogger $logger
243
    ): void {
244
        try {
245 12
            $scoppedContent = $this->scoper->scope($inputFilePath, $inputContents, $prefix, $patchers, $whitelist);
246 2
        } catch (Throwable $error) {
247 2
            $exception = new ParsingException(
248 2
                sprintf(
249 2
                    'Could not parse the file "%s".',
250 2
                    $inputFilePath
251
                ),
252 2
                0,
253 2
                $error
254
            );
255
256 2
            if ($stopOnFailure) {
257
                throw $exception;
258
            }
259
260 2
            $logger->outputWarnOfFailure($inputFilePath, $exception);
261
262 2
            $scoppedContent = file_get_contents($inputFilePath);
263
        }
264
265 12
        $this->fileSystem->dumpFile($outputFilePath, $scoppedContent);
266
267 12
        if (false === isset($exception)) {
268 11
            $logger->outputSuccess($inputFilePath);
269
        }
270
    }
271
272 14
    private function validatePrefix(InputInterface $input): void
273
    {
274 14
        $prefix = $input->getOption(self::PREFIX_OPT);
275
276 14
        if (null !== $prefix && 1 === preg_match('/(?<prefix>.*?)\\\\*$/', $prefix, $matches)) {
277 13
            $prefix = $matches['prefix'];
278
        }
279
280 14
        $input->setOption(self::PREFIX_OPT, $prefix);
281
    }
282
283 14
    private function validatePaths(InputInterface $input): void
284
    {
285 14
        $cwd = getcwd();
286 14
        $fileSystem = $this->fileSystem;
287
288 14
        $paths = array_map(
289
            function (string $path) use ($cwd, $fileSystem) {
290 9
                if (false === $fileSystem->isAbsolutePath($path)) {
291
                    return $cwd.DIRECTORY_SEPARATOR.$path;
292
                }
293
294 9
                return $path;
295 14
            },
296 14
            $input->getArgument(self::PATH_ARG)
297
        );
298
299 14
        $input->setArgument(self::PATH_ARG, $paths);
300
    }
301
302 14
    private function validateOutputDir(InputInterface $input, OutputStyle $io): void
303
    {
304 14
        $outputDir = $input->getOption(self::OUTPUT_DIR_OPT);
305
306 14
        if (false === $this->fileSystem->isAbsolutePath($outputDir)) {
307 3
            $outputDir = getcwd().DIRECTORY_SEPARATOR.$outputDir;
308
        }
309
310 14
        $input->setOption(self::OUTPUT_DIR_OPT, $outputDir);
311
312 14
        if (false === $this->fileSystem->exists($outputDir)) {
313 14
            return;
314
        }
315
316
        if (false === is_writable($outputDir)) {
317
            throw new RuntimeException(
318
                sprintf(
319
                    'Expected "<comment>%s</comment>" to be writeable.',
320
                    $outputDir
321
                )
322
            );
323
        }
324
325
        if ($input->getOption(self::FORCE_OPT)) {
326
            $this->fileSystem->remove($outputDir);
327
328
            return;
329
        }
330
331
        if (false === is_dir($outputDir)) {
332
            $canDeleteFile = $io->confirm(
333
                sprintf(
334
                    'Expected "<comment>%s</comment>" to be a directory but found a file instead. It will be '
335
                    .'removed, do you wish to proceed?',
336
                    $outputDir
337
                ),
338
                false
339
            );
340
341
            if (false === $canDeleteFile) {
342
                return;
343
            }
344
345
            $this->fileSystem->remove($outputDir);
346
        } else {
347
            $canDeleteFile = $io->confirm(
348
                sprintf(
349
                    'The output directory "<comment>%s</comment>" already exists. Continuing will erase its'
350
                    .' content, do you wish to proceed?',
351
                    $outputDir
352
                ),
353
                false
354
            );
355
356
            if (false === $canDeleteFile) {
357
                return;
358
            }
359
360
            $this->fileSystem->remove($outputDir);
361
        }
362
    }
363
364 14
    private function retrieveConfig(InputInterface $input, OutputInterface $output, OutputStyle $io): Configuration
365
    {
366 14
        if ($input->getOption(self::NO_CONFIG_OPT)) {
367 10
            $io->writeln(
368 10
                'Loading without configuration file.',
369 10
                OutputStyle::VERBOSITY_DEBUG
370
            );
371
372 10
            $config = Configuration::load();
373 10
            $config = $config->withPrefix($input->getOption(self::PREFIX_OPT));
374
375 10
            return $this->retrievePaths($input, $config);
376
        }
377
378 4
        $configFile = $input->getOption(self::CONFIG_FILE_OPT);
379
380 4
        if (null === $configFile) {
381 3
            $configFile = $this->makeAbsolutePath(self::CONFIG_FILE_DEFAULT);
382
383 3
            if (false === file_exists($configFile) && false === $this->init) {
384
                $this->init = true;
385
386
                $initCommand = $this->getApplication()->find('init');
387
388
                $initInput = new StringInput('');
389
                $initInput->setInteractive($input->isInteractive());
390
391
                $initCommand->run($initInput, $output);
392
393
                $io->writeln(
394
                    sprintf(
395
                        'Config file "<comment>%s</comment>" not found. Skipping.',
396
                        $configFile
397
                    ),
398
                    OutputStyle::VERBOSITY_DEBUG
399
                );
400
401
                return self::retrieveConfig($input, $output, $io);
402
            }
403
404 3
            if ($this->init) {
405 3
                $configFile = null;
406
            }
407
        } else {
408 1
            $configFile = $this->makeAbsolutePath($configFile);
409
        }
410
411 4
        if (null === $configFile) {
412
            $io->writeln(
413
                'Loading without configuration file.',
414
                OutputStyle::VERBOSITY_DEBUG
415
            );
416 4
        } elseif (false === file_exists($configFile)) {
417 1
            throw new RuntimeException(
418 1
                sprintf(
419 1
                    'Could not find the configuration file "%s".',
420 1
                    $configFile
421
                )
422
            );
423
        } else {
424 3
            $io->writeln(
425 3
                sprintf(
426 3
                    'Using the configuration file "%s".',
427 3
                    $configFile
428
                ),
429 3
                OutputStyle::VERBOSITY_DEBUG
430
            );
431
        }
432
433 3
        $config = Configuration::load($configFile);
434 2
        $config = $this->retrievePaths($input, $config);
435
436 2
        return $config->withPrefix($input->getOption(self::PREFIX_OPT));
437
    }
438
439 12
    private function retrievePaths(InputInterface $input, Configuration $config): Configuration
440
    {
441
        // Checks if there is any path included and if note use the current working directory as the include path
442 12
        $paths = $input->getArgument(self::PATH_ARG);
443
444 12
        if (0 === count($paths) && 0 === count($config->getFilesWithContents())) {
445 3
            $paths = [getcwd()];
446
        }
447
448 12
        return $config->withPaths($paths);
449
    }
450
451 4
    private function makeAbsolutePath(string $path): string
452
    {
453 4
        if (false === $this->fileSystem->isAbsolutePath($path)) {
454 4
            $path = getcwd().DIRECTORY_SEPARATOR.$path;
455
        }
456
457 4
        return $path;
458
    }
459
}
460