Passed
Push — master ( d65fca...61b862 )
by Théo
10:36 queued 08:16
created

AddPrefixCommand   B

Complexity

Total Complexity 40

Size/Duplication

Total Lines 439
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 12

Test Coverage

Coverage 78.22%

Importance

Changes 0
Metric Value
dl 0
loc 439
ccs 176
cts 225
cp 0.7822
rs 8.2608
c 0
b 0
f 0
wmc 40
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
C retrieveConfig() 0 72 8
A retrievePaths() 0 11 3
A makeAbsolutePath() 0 8 2

How to fix   Complexity   

Complex Class

Complex classes like AddPrefixCommand 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 AddPrefixCommand, 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\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 21
    public function __construct(Filesystem $fileSystem, Scoper $scoper)
53
    {
54 21
        parent::__construct();
55
56 21
        $this->fileSystem = $fileSystem;
57 21
        $this->scoper = $scoper;
58
    }
59
60
    /**
61
     * @inheritdoc
62
     */
63 21
    protected function configure(): void
64
    {
65 21
        parent::configure();
66
67
        $this
68 21
            ->setName('add-prefix')
69 21
            ->setDescription('Goes through all the PHP files found in the given paths to apply the given prefix to namespaces & FQNs.')
70 21
            ->addArgument(
71 21
                self::PATH_ARG,
72 21
                InputArgument::IS_ARRAY,
73 21
                'The path(s) to process.'
74
            )
75 21
            ->addOption(
76 21
                self::PREFIX_OPT,
77 21
                'p',
78 21
                InputOption::VALUE_REQUIRED,
79 21
                'The namespace prefix to add.'
80
            )
81 21
            ->addOption(
82 21
                self::OUTPUT_DIR_OPT,
83 21
                'o',
84 21
                InputOption::VALUE_REQUIRED,
85 21
                'The output directory in which the prefixed code will be dumped.',
86 21
                'build'
87
            )
88 21
            ->addOption(
89 21
                self::FORCE_OPT,
90 21
                'f',
91 21
                InputOption::VALUE_NONE,
92 21
                'Deletes any existing content in the output directory without any warning.'
93
            )
94 21
            ->addOption(
95 21
                self::STOP_ON_FAILURE_OPT,
96 21
                's',
97 21
                InputOption::VALUE_NONE,
98 21
                'Stops on failure.'
99
            )
100 21
            ->addOption(
101 21
                self::CONFIG_FILE_OPT,
102 21
                'c',
103 21
                InputOption::VALUE_REQUIRED,
104 21
                sprintf(
105 21
                    'Configuration file. Will use "%s" if found by default.',
106 21
                    self::CONFIG_FILE_DEFAULT
107
                )
108
            )
109 21
            ->addOption(
110 21
                self::NO_CONFIG_OPT,
111 21
                null,
112 21
                InputOption::VALUE_NONE,
113 21
            'Do not look for a configuration file.'
114
            )
115
        ;
116
    }
117
118
    /**
119
     * @inheritdoc
120
     */
121 19
    protected function execute(InputInterface $input, OutputInterface $output): int
122
    {
123 19
        $io = new SymfonyStyle($input, $output);
124 19
        $io->writeln('');
125
126 19
        $this->changeWorkingDirectory($input);
127
128 19
        $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
            $input->getOption(self::PREFIX_OPT),
142 12
            $input->getArgument(self::PATH_ARG)
143
        );
144
145
        try {
146 12
            $this->scopeFiles(
147 12
                $input->getOption(self::PREFIX_OPT),
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 12
            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 19
    private function validatePrefix(InputInterface $input): void
273
    {
274 19
        $prefix = $input->getOption(self::PREFIX_OPT);
275
276 19
        if (null === $prefix) {
277 1
            $prefix = uniqid('PhpScoper');
278
        } else {
279 18
            $prefix = trim($prefix);
280
        }
281
282 19
        if (1 === preg_match('/(?<prefix>.*?)\\\\*$/', $prefix, $matches)) {
283 19
            $prefix = $matches['prefix'];
284
        }
285
286 19
        if ('' === $prefix) {
287 5
            throw new RuntimeException(
288 5
                sprintf(
289 5
                    'Expected "%s" argument to be a non empty string.',
290 5
                    self::PREFIX_OPT
291
                )
292
            );
293
        }
294
295 14
        $input->setOption(self::PREFIX_OPT, $prefix);
296
    }
297
298 14
    private function validatePaths(InputInterface $input): void
299
    {
300 14
        $cwd = getcwd();
301 14
        $fileSystem = $this->fileSystem;
302
303 14
        $paths = array_map(
304 14
            function (string $path) use ($cwd, $fileSystem) {
305 9
                if (false === $fileSystem->isAbsolutePath($path)) {
306
                    return $cwd.DIRECTORY_SEPARATOR.$path;
307
                }
308
309 9
                return $path;
310 14
            },
311 14
            $input->getArgument(self::PATH_ARG)
312
        );
313
314 14
        $input->setArgument(self::PATH_ARG, $paths);
315
    }
316
317 14
    private function validateOutputDir(InputInterface $input, OutputStyle $io): void
318
    {
319 14
        $outputDir = $input->getOption(self::OUTPUT_DIR_OPT);
320
321 14
        if (false === $this->fileSystem->isAbsolutePath($outputDir)) {
322 3
            $outputDir = getcwd().DIRECTORY_SEPARATOR.$outputDir;
323
        }
324
325 14
        $input->setOption(self::OUTPUT_DIR_OPT, $outputDir);
326
327 14
        if (false === $this->fileSystem->exists($outputDir)) {
328 14
            return;
329
        }
330
331
        if (false === is_writable($outputDir)) {
332
            throw new RuntimeException(
333
                sprintf(
334
                    'Expected "<comment>%s</comment>" to be writeable.',
335
                    $outputDir
336
                )
337
            );
338
        }
339
340
        if ($input->getOption(self::FORCE_OPT)) {
341
            $this->fileSystem->remove($outputDir);
342
343
            return;
344
        }
345
346
        if (false === is_dir($outputDir)) {
347
            $canDeleteFile = $io->confirm(
348
                sprintf(
349
                    'Expected "<comment>%s</comment>" to be a directory but found a file instead. It will be '
350
                    .'removed, 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
        } else {
362
            $canDeleteFile = $io->confirm(
363
                sprintf(
364
                    'The output directory "<comment>%s</comment>" already exists. Continuing will erase its'
365
                    .' content, do you wish to proceed?',
366
                    $outputDir
367
                ),
368
                false
369
            );
370
371
            if (false === $canDeleteFile) {
372
                return;
373
            }
374
375
            $this->fileSystem->remove($outputDir);
376
        }
377
    }
378
379 14
    private function retrieveConfig(InputInterface $input, OutputInterface $output, OutputStyle $io): Configuration
380
    {
381 14
        if ($input->getOption(self::NO_CONFIG_OPT)) {
382 10
            $io->writeln(
383 10
                'Loading without configuration file.',
384 10
                OutputStyle::VERBOSITY_DEBUG
385
            );
386
387 10
            $config = Configuration::load();
388
389 10
            return $this->retrievePaths($input, $config);
390
        }
391
392 4
        $configFile = $input->getOption(self::CONFIG_FILE_OPT);
393
394 4
        if (null === $configFile) {
395 3
            $configFile = $this->makeAbsolutePath(self::CONFIG_FILE_DEFAULT);
396
397 3
            if (false === file_exists($configFile) && false === $this->init) {
398
                $this->init = true;
399
400
                $initCommand = $this->getApplication()->find('init');
401
402
                $initInput = new StringInput('');
403
                $initInput->setInteractive($input->isInteractive());
404
405
                $initCommand->run($initInput, $output);
406
407
                $io->writeln(
408
                    sprintf(
409
                        'Config file "<comment>%s</comment>" not found. Skipping.',
410
                        $configFile
411
                    ),
412
                    OutputStyle::VERBOSITY_DEBUG
413
                );
414
415
                return self::retrieveConfig($input, $output, $io);
416
            }
417
418 3
            if ($this->init) {
419 3
                $configFile = null;
420
            }
421
        } else {
422 1
            $configFile = $this->makeAbsolutePath($configFile);
423
        }
424
425 4
        if (null === $configFile) {
426
            $io->writeln(
427
                'Loading without configuration file.',
428
                OutputStyle::VERBOSITY_DEBUG
429
            );
430 4
        } elseif (false === file_exists($configFile)) {
431 1
            throw new RuntimeException(
432 1
                sprintf(
433 1
                    'Could not find the configuration file "%s".',
434 1
                    $configFile
435
                )
436
            );
437
        } else {
438 3
            $io->writeln(
439 3
                sprintf(
440 3
                    'Using the configuration file "%s".',
441 3
                    $configFile
442
                ),
443 3
                OutputStyle::VERBOSITY_DEBUG
444
            );
445
        }
446
447 3
        $config = Configuration::load($configFile);
448
449 2
        return $this->retrievePaths($input, $config);
450
    }
451
452 12
    private function retrievePaths(InputInterface $input, Configuration $config): Configuration
453
    {
454
        // Checks if there is any path included and if note use the current working directory as the include path
455 12
        $paths = $input->getArgument(self::PATH_ARG);
456
457 12
        if (0 === count($paths) && 0 === count($config->getFilesWithContents())) {
458 3
            $paths = [getcwd()];
459
        }
460
461 12
        return $config->withPaths($paths);
462
    }
463
464 4
    private function makeAbsolutePath(string $path): string
465
    {
466 4
        if (false === $this->fileSystem->isAbsolutePath($path)) {
467 4
            $path = getcwd().DIRECTORY_SEPARATOR.$path;
468
        }
469
470 4
        return $path;
471
    }
472
}
473