Passed
Push — master ( 01e41e...21756f )
by Théo
02:37
created

AddPrefixCommand   B

Complexity

Total Complexity 37

Size/Duplication

Total Lines 427
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 12

Test Coverage

Coverage 79.82%

Importance

Changes 0
Metric Value
dl 0
loc 427
rs 8.6
c 0
b 0
f 0
ccs 174
cts 218
cp 0.7982
wmc 37
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 scopeFiles() 0 55 5
B scopeFile() 0 37 4
B validatePrefix() 0 25 4
A validatePaths() 0 18 2
B validateOutputDir() 0 61 8
B retrieveConfig() 0 61 5
A retrievePaths() 0 11 3
A makeAbsolutePath() 0 8 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\Console\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
48
    /**
49
     * @inheritdoc
50
     */
51 21
    public function __construct(Filesystem $fileSystem, Scoper $scoper)
52
    {
53 21
        parent::__construct();
54
55 21
        $this->fileSystem = $fileSystem;
56 21
        $this->scoper = $scoper;
57
    }
58
59
    /**
60
     * @inheritdoc
61
     */
62 21
    protected function configure(): void
63
    {
64 21
        parent::configure();
65
66
        $this
67 21
            ->setName('add-prefix')
68 21
            ->setDescription('Goes through all the PHP files found in the given paths to apply the given prefix to namespaces & FQNs.')
69 21
            ->addArgument(
70 21
                self::PATH_ARG,
71 21
                InputArgument::IS_ARRAY,
72 21
                'The path(s) to process.'
73
            )
74 21
            ->addOption(
75 21
                self::PREFIX_OPT,
76 21
                'p',
77 21
                InputOption::VALUE_REQUIRED,
78 21
                'The namespace prefix to add.'
79
            )
80 21
            ->addOption(
81 21
                self::OUTPUT_DIR_OPT,
82 21
                'o',
83 21
                InputOption::VALUE_REQUIRED,
84 21
                'The output directory in which the prefixed code will be dumped.',
85 21
                'build'
86
            )
87 21
            ->addOption(
88 21
                self::FORCE_OPT,
89 21
                'f',
90 21
                InputOption::VALUE_NONE,
91 21
                'Deletes any existing content in the output directory without any warning.'
92
            )
93 21
            ->addOption(
94 21
                self::STOP_ON_FAILURE_OPT,
95 21
                's',
96 21
                InputOption::VALUE_NONE,
97 21
                'Stops on failure.'
98
            )
99 21
            ->addOption(
100 21
                self::CONFIG_FILE_OPT,
101 21
                'c',
102 21
                InputOption::VALUE_REQUIRED,
103 21
                sprintf(
104 21
                    'Configuration file. Will use "%s" if found by default.',
105 21
                    self::CONFIG_FILE_DEFAULT
106
                )
107
            )
108 21
            ->addOption(
109 21
                self::NO_CONFIG_OPT,
110 21
                null,
111 21
                InputOption::VALUE_NONE,
112 21
            'Do not look for a configuration file.'
113
            )
114
        ;
115
    }
116
117
    /**
118
     * @inheritdoc
119
     */
120 19
    protected function execute(InputInterface $input, OutputInterface $output): int
121
    {
122 19
        $io = new SymfonyStyle($input, $output);
123 19
        $io->writeln('');
124
125 19
        $this->changeWorkingDirectory($input);
126
127 19
        $this->validatePrefix($input);
128 14
        $this->validatePaths($input);
129 14
        $this->validateOutputDir($input, $io);
130
131 14
        $config = $this->retrieveConfig($input, $output, $io);
132 12
        $output = $input->getOption(self::OUTPUT_DIR_OPT);
133
134 12
        $logger = new ConsoleLogger(
135 12
            $this->getApplication(),
136 12
            $io
137
        );
138
139 12
        $logger->outputScopingStart(
140 12
            $input->getOption(self::PREFIX_OPT),
141 12
            $input->getArgument(self::PATH_ARG)
142
        );
143
144
        try {
145 12
            $this->scopeFiles(
146 12
                $input->getOption(self::PREFIX_OPT),
147 12
                $config->getFilesWithContents(),
148 12
                $output,
149 12
                $config->getPatchers(),
150 12
                $config->getWhitelist(),
151 12
                $input->getOption(self::STOP_ON_FAILURE_OPT),
152 12
                $logger
153
            );
154
        } catch (Throwable $throwable) {
155
            $this->fileSystem->remove($output);
156
157
            $logger->outputScopingEndWithFailure();
158
159
            throw $throwable;
160
        }
161
162 12
        $logger->outputScopingEnd();
163
164 12
        return 0;
165
    }
166
167 12
    private function scopeFiles(
168
        string $prefix,
169
        array $filesWithContents,
170
        string $output,
171
        array $patchers,
172
        array $whitelist,
173
        bool $stopOnFailure,
174
        ConsoleLogger $logger
175
    ): void {
176
        // Creates output directory if does not already exist
177 12
        $this->fileSystem->mkdir($output);
178
179 12
        $logger->outputFileCount(count($filesWithContents));
180
181 12
        $vendorDirs = [];
182 12
        $commonPath = get_common_path(array_keys($filesWithContents));
183
184 12
        foreach ($filesWithContents as $fileWithContents) {
185 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...
186
187 12
            $outputFilePath = $output.str_replace($commonPath, '', $inputFilePath);
188
189 12
            if (preg_match('~((?:.*)\/vendor)\/.*~', $outputFilePath, $matches)) {
190
                $vendorDirs[$matches[1]] = true;
191
            }
192
193 12
            $this->scopeFile(
194 12
                $inputFilePath,
195 12
                $inputContents,
196 12
                $outputFilePath,
197 12
                $prefix,
198 12
                $patchers,
199 12
                $whitelist,
200 12
                $stopOnFailure,
201 12
                $logger
202
            );
203
        }
204
205 12
        $vendorDirs = array_keys($vendorDirs);
206
207 12
        usort(
208 12
            $vendorDirs,
209 12
            function ($a, $b) {
210
                return strlen($b) <=> strlen($a);
211 12
            }
212
        );
213
214 12
        $vendorDir = (0 === count($vendorDirs)) ? null : $vendorDirs[0];
215
216 12
        if (null !== $vendorDir) {
217
            $autoload = (new ScoperAutoloadGenerator($whitelist))->dump($prefix);
218
219
            $this->fileSystem->dumpFile($vendorDir.'/scoper-autoload.php', $autoload);
220
        }
221
    }
222
223
    /**
224
     * @param string        $inputFilePath
225
     * @param string        $outputFilePath
226
     * @param string        $inputContents
227
     * @param string        $prefix
228
     * @param callable[]    $patchers
229
     * @param string[]      $whitelist
230
     * @param bool          $stopOnFailure
231
     * @param ConsoleLogger $logger
232
     */
233 12
    private function scopeFile(
234
        string $inputFilePath,
235
        string $inputContents,
236
        string $outputFilePath,
237
        string $prefix,
238
        array $patchers,
239
        array $whitelist,
240
        bool $stopOnFailure,
241
        ConsoleLogger $logger
242
    ): void {
243
        try {
244 12
            $scoppedContent = $this->scoper->scope($inputFilePath, $inputContents, $prefix, $patchers, $whitelist);
245 2
        } catch (Throwable $error) {
246 2
            $exception = new ParsingException(
247 2
                sprintf(
248 2
                    'Could not parse the file "%s".',
249 2
                    $inputFilePath
250
                ),
251 2
                0,
252 2
                $error
253
            );
254
255 2
            if ($stopOnFailure) {
256
                throw $exception;
257
            }
258
259 2
            $logger->outputWarnOfFailure($inputFilePath, $exception);
260
261 2
            $scoppedContent = file_get_contents($inputFilePath);
262
        }
263
264 12
        $this->fileSystem->dumpFile($outputFilePath, $scoppedContent);
265
266 12
        if (false === isset($exception)) {
267 11
            $logger->outputSuccess($inputFilePath);
268
        }
269
    }
270
271 19
    private function validatePrefix(InputInterface $input): void
272
    {
273 19
        $prefix = $input->getOption(self::PREFIX_OPT);
274
275 19
        if (null === $prefix) {
276 1
            $prefix = uniqid('PhpScoper');
277
        } else {
278 18
            $prefix = trim($prefix);
279
        }
280
281 19
        if (1 === preg_match('/(?<prefix>.*?)\\\\*$/', $prefix, $matches)) {
282 19
            $prefix = $matches['prefix'];
283
        }
284
285 19
        if ('' === $prefix) {
286 5
            throw new RuntimeException(
287 5
                sprintf(
288 5
                    'Expected "%s" argument to be a non empty string.',
289 5
                    self::PREFIX_OPT
290
                )
291
            );
292
        }
293
294 14
        $input->setOption(self::PREFIX_OPT, $prefix);
295
    }
296
297 14
    private function validatePaths(InputInterface $input): void
298
    {
299 14
        $cwd = getcwd();
300 14
        $fileSystem = $this->fileSystem;
301
302 14
        $paths = array_map(
303 14
            function (string $path) use ($cwd, $fileSystem) {
304 9
                if (false === $fileSystem->isAbsolutePath($path)) {
305
                    return $cwd.DIRECTORY_SEPARATOR.$path;
306
                }
307
308 9
                return $path;
309 14
            },
310 14
            $input->getArgument(self::PATH_ARG)
311
        );
312
313 14
        $input->setArgument(self::PATH_ARG, $paths);
314
    }
315
316 14
    private function validateOutputDir(InputInterface $input, OutputStyle $io): void
317
    {
318 14
        $outputDir = $input->getOption(self::OUTPUT_DIR_OPT);
319
320 14
        if (false === $this->fileSystem->isAbsolutePath($outputDir)) {
321 3
            $outputDir = getcwd().DIRECTORY_SEPARATOR.$outputDir;
322
        }
323
324 14
        $input->setOption(self::OUTPUT_DIR_OPT, $outputDir);
325
326 14
        if (false === $this->fileSystem->exists($outputDir)) {
327 14
            return;
328
        }
329
330
        if (false === is_writable($outputDir)) {
331
            throw new RuntimeException(
332
                sprintf(
333
                    'Expected "<comment>%s</comment>" to be writeable.',
334
                    $outputDir
335
                )
336
            );
337
        }
338
339
        if ($input->getOption(self::FORCE_OPT)) {
340
            $this->fileSystem->remove($outputDir);
341
342
            return;
343
        }
344
345
        if (false === is_dir($outputDir)) {
346
            $canDeleteFile = $io->confirm(
347
                sprintf(
348
                    'Expected "<comment>%s</comment>" to be a directory but found a file instead. It will be '
349
                    .'removed, do you wish to proceed?',
350
                    $outputDir
351
                ),
352
                false
353
            );
354
355
            if (false === $canDeleteFile) {
356
                return;
357
            }
358
359
            $this->fileSystem->remove($outputDir);
360
        } else {
361
            $canDeleteFile = $io->confirm(
362
                sprintf(
363
                    'The output directory "<comment>%s</comment>" already exists. Continuing will erase its'
364
                    .' content, do you wish to proceed?',
365
                    $outputDir
366
                ),
367
                false
368
            );
369
370
            if (false === $canDeleteFile) {
371
                return;
372
            }
373
374
            $this->fileSystem->remove($outputDir);
375
        }
376
    }
377
378 14
    private function retrieveConfig(InputInterface $input, OutputInterface $output, OutputStyle $io): Configuration
379
    {
380 14
        if ($input->getOption(self::NO_CONFIG_OPT)) {
381 10
            $io->writeln(
382 10
                'Loading without configuration file.',
383 10
                OutputStyle::VERBOSITY_DEBUG
384
            );
385
386 10
            $config = Configuration::load();
387
388 10
            return $this->retrievePaths($input, $config);
389
        }
390
391 4
        $configFile = $input->getOption(self::CONFIG_FILE_OPT);
392
393 4
        if (null === $configFile) {
394 3
            $configFile = $this->makeAbsolutePath(self::CONFIG_FILE_DEFAULT);
395
396 3
            if (false === file_exists($configFile)) {
397
                $initCommand = $this->getApplication()->find('init');
398
399
                $initInput = new StringInput('');
400
                $initInput->setInteractive($input->isInteractive());
401
402
                $initCommand->run($initInput, $output);
403
404
                $io->writeln(
405
                    sprintf(
406
                        'Config file "<comment>%s</comment>" not found. Skipping.',
407
                        $configFile
408
                    ),
409
                    OutputStyle::VERBOSITY_DEBUG
410
                );
411
412 3
                return Configuration::load(null);
413
            }
414
        } else {
415 1
            $configFile = $this->makeAbsolutePath($configFile);
416
        }
417
418 4
        if (false === file_exists($configFile)) {
419 1
            throw new RuntimeException(
420 1
                sprintf(
421 1
                    'Could not find the configuration file "<comment>%s</comment>".',
422 1
                    $configFile
423
                )
424
            );
425
        }
426
427 3
        $io->writeln(
428 3
            sprintf(
429 3
                'Using the configuration file "<comment>%s</comment>".',
430 3
                $configFile
431
            ),
432 3
            OutputStyle::VERBOSITY_DEBUG
433
        );
434
435 3
        $config = Configuration::load($configFile);
436
437 2
        return $this->retrievePaths($input, $config);
438
    }
439
440 12
    private function retrievePaths(InputInterface $input, Configuration $config): Configuration
441
    {
442
        // Checks if there is any path included and if note use the current working directory as the include path
443 12
        $paths = $input->getArgument(self::PATH_ARG);
444
445 12
        if (0 === count($paths) && 0 === count($config->getFilesWithContents())) {
446 3
            $paths = [getcwd()];
447
        }
448
449 12
        return $config->withPaths($paths);
450
    }
451
452 4
    private function makeAbsolutePath(string $path): string
453
    {
454 4
        if (false === $this->fileSystem->isAbsolutePath($path)) {
455 4
            $path = getcwd().DIRECTORY_SEPARATOR.$path;
456
        }
457
458 4
        return $path;
459
    }
460
}
461