Completed
Push — master ( a986ff...246bce )
by Théo
08:48
created

AddPrefixCommand::scopeFiles()   B

Complexity

Conditions 5
Paths 12

Size

Total Lines 54

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 24
CRAP Score 5.0729

Importance

Changes 0
Metric Value
cc 5
nc 12
nop 7
dl 0
loc 54
ccs 24
cts 28
cp 0.8571
crap 5.0729
rs 8.6925
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\Scoper\ConfigurableScoper;
22
use Humbug\PhpScoper\Throwable\Exception\ParsingException;
23
use Humbug\PhpScoper\Whitelist;
24
use Symfony\Component\Console\Exception\RuntimeException;
25
use Symfony\Component\Console\Input\InputArgument;
26
use Symfony\Component\Console\Input\InputInterface;
27
use Symfony\Component\Console\Input\InputOption;
28
use Symfony\Component\Console\Input\StringInput;
29
use Symfony\Component\Console\Output\OutputInterface;
30
use Symfony\Component\Console\Style\OutputStyle;
31
use Symfony\Component\Console\Style\SymfonyStyle;
32
use Symfony\Component\Filesystem\Filesystem;
33
use Throwable;
34
use function count;
35
use function Humbug\PhpScoper\get_common_path;
36
37
final class AddPrefixCommand extends BaseCommand
38
{
39
    private const PATH_ARG = 'paths';
40
    private const PREFIX_OPT = 'prefix';
41
    private const OUTPUT_DIR_OPT = 'output-dir';
42
    private const FORCE_OPT = 'force';
43
    private const STOP_ON_FAILURE_OPT = 'stop-on-failure';
44
    private const CONFIG_FILE_OPT = 'config';
45
    private const CONFIG_FILE_DEFAULT = 'scoper.inc.php';
46
    private const NO_CONFIG_OPT = 'no-config';
47
48
    private $fileSystem;
49
    private $scoper;
50
    private $init = false;
51
52
    /**
53
     * @inheritdoc
54
     */
55 16
    public function __construct(Filesystem $fileSystem, Scoper $scoper)
56
    {
57 16
        parent::__construct();
58
59 16
        $this->fileSystem = $fileSystem;
60 16
        $this->scoper = new ConfigurableScoper($scoper);
61
    }
62
63
    /**
64
     * @inheritdoc
65
     */
66 16
    protected function configure(): void
67
    {
68 16
        parent::configure();
69
70
        $this
71 16
            ->setName('add-prefix')
72 16
            ->setDescription('Goes through all the PHP files found in the given paths to apply the given prefix to namespaces & FQNs.')
73 16
            ->addArgument(
74 16
                self::PATH_ARG,
75 16
                InputArgument::IS_ARRAY,
76 16
                'The path(s) to process.'
77
            )
78 16
            ->addOption(
79 16
                self::PREFIX_OPT,
80 16
                'p',
81 16
                InputOption::VALUE_REQUIRED,
82 16
                'The namespace prefix to add.'
83
            )
84 16
            ->addOption(
85 16
                self::OUTPUT_DIR_OPT,
86 16
                'o',
87 16
                InputOption::VALUE_REQUIRED,
88 16
                'The output directory in which the prefixed code will be dumped.',
89 16
                'build'
90
            )
91 16
            ->addOption(
92 16
                self::FORCE_OPT,
93 16
                'f',
94 16
                InputOption::VALUE_NONE,
95 16
                'Deletes any existing content in the output directory without any warning.'
96
            )
97 16
            ->addOption(
98 16
                self::STOP_ON_FAILURE_OPT,
99 16
                's',
100 16
                InputOption::VALUE_NONE,
101 16
                'Stops on failure.'
102
            )
103 16
            ->addOption(
104 16
                self::CONFIG_FILE_OPT,
105 16
                'c',
106 16
                InputOption::VALUE_REQUIRED,
107 16
                sprintf(
108 16
                    'Configuration file. Will use "%s" if found by default.',
109 16
                    self::CONFIG_FILE_DEFAULT
110
                )
111
            )
112 16
            ->addOption(
113 16
                self::NO_CONFIG_OPT,
114 16
                null,
115 16
                InputOption::VALUE_NONE,
116 16
            'Do not look for a configuration file.'
117
            )
118
        ;
119
    }
120
121
    /**
122
     * @inheritdoc
123
     */
124 14
    protected function execute(InputInterface $input, OutputInterface $output): int
125
    {
126 14
        $io = new SymfonyStyle($input, $output);
127 14
        $io->writeln('');
128
129 14
        $this->changeWorkingDirectory($input);
130
131 14
        $this->validatePrefix($input);
132 14
        $this->validatePaths($input);
133 14
        $this->validateOutputDir($input, $io);
134
135 14
        $config = $this->retrieveConfig($input, $output, $io);
136 12
        $output = $input->getOption(self::OUTPUT_DIR_OPT);
137
138 12
        if ([] !== $config->getWhitelistedFiles()) {
139
            $this->scoper = $this->scoper->withWhitelistedFiles(...$config->getWhitelistedFiles());
140
        }
141
142 12
        $logger = new ConsoleLogger(
143 12
            $this->getApplication(),
144 12
            $io
145
        );
146
147 12
        $logger->outputScopingStart(
148 12
            $config->getPrefix(),
149 12
            $input->getArgument(self::PATH_ARG)
1 ignored issue
show
Bug introduced by
It seems like $input->getArgument(self::PATH_ARG) targeting Symfony\Component\Consol...nterface::getArgument() can also be of type null or string; however, Humbug\PhpScoper\Logger\...r::outputScopingStart() does only seem to accept array<integer,string>, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
150
        );
151
152
        try {
153 12
            $this->scopeFiles(
154 12
                $config->getPrefix(),
155 12
                $config->getFilesWithContents(),
156 12
                $output,
157 12
                $config->getPatchers(),
158 12
                $config->getWhitelist(),
159 12
                $input->getOption(self::STOP_ON_FAILURE_OPT),
160 12
                $logger
161
            );
162
        } catch (Throwable $throwable) {
163
            $this->fileSystem->remove($output);
164
165
            $logger->outputScopingEndWithFailure();
166
167
            throw $throwable;
168
        }
169
170 12
        $logger->outputScopingEnd();
171
172 12
        return 0;
173
    }
174
175
    /**
176
     * @var callable[]
177
     */
178 12
    private function scopeFiles(
179
        string $prefix,
180
        array $filesWithContents,
181
        string $output,
182
        array $patchers,
183
        Whitelist $whitelist,
184
        bool $stopOnFailure,
185
        ConsoleLogger $logger
186
    ): void {
187
        // Creates output directory if does not already exist
188 12
        $this->fileSystem->mkdir($output);
189
190 12
        $logger->outputFileCount(count($filesWithContents));
191
192 12
        $vendorDirs = [];
193 12
        $commonPath = get_common_path(array_keys($filesWithContents));
194
195 12
        foreach ($filesWithContents as [$inputFilePath, $inputContents]) {
196 12
            $outputFilePath = $output.str_replace($commonPath, '', $inputFilePath);
1 ignored issue
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...
197
198 12
            $pattern = '~((?:.*)\\'.DIRECTORY_SEPARATOR.'vendor)\\'.DIRECTORY_SEPARATOR.'.*~';
199 12
            if (preg_match($pattern, $outputFilePath, $matches)) {
200
                $vendorDirs[$matches[1]] = true;
201
            }
202
203 12
            $this->scopeFile(
204 12
                $inputFilePath,
205 12
                $inputContents,
1 ignored issue
show
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...
206 12
                $outputFilePath,
207 12
                $prefix,
208 12
                $patchers,
209 12
                $whitelist,
210 12
                $stopOnFailure,
211 12
                $logger
212
            );
213
        }
214
215 12
        $vendorDirs = array_keys($vendorDirs);
216
217 12
        usort(
218 12
            $vendorDirs,
219
            static function ($a, $b) {
220
                return strlen($b) <=> strlen($a);
221 12
            }
222
        );
223
224 12
        $vendorDir = (0 === count($vendorDirs)) ? null : $vendorDirs[0];
225
226 12
        if (null !== $vendorDir) {
227
            $autoload = (new ScoperAutoloadGenerator($whitelist))->dump($prefix);
228
229
            $this->fileSystem->dumpFile($vendorDir.'/scoper-autoload.php', $autoload);
230
        }
231
    }
232
233
    /**
234
     * @param callable[] $patchers
235
     */
236 12
    private function scopeFile(
237
        string $inputFilePath,
238
        string $inputContents,
239
        string $outputFilePath,
240
        string $prefix,
241
        array $patchers,
242
        Whitelist $whitelist,
243
        bool $stopOnFailure,
244
        ConsoleLogger $logger
245
    ): void {
246
        try {
247 12
            $scoppedContent = $this->scoper->scope($inputFilePath, $inputContents, $prefix, $patchers, $whitelist);
248 2
        } catch (Throwable $throwable) {
249 2
            $exception = new ParsingException(
250 2
                sprintf(
251 2
                    'Could not parse the file "%s".',
252 2
                    $inputFilePath
253
                ),
254 2
                0,
255 2
                $throwable
256
            );
257
258 2
            if ($stopOnFailure) {
259
                throw $exception;
260
            }
261
262 2
            $logger->outputWarnOfFailure($inputFilePath, $exception);
263
264 2
            $scoppedContent = file_get_contents($inputFilePath);
265
        }
266
267 12
        $this->fileSystem->dumpFile($outputFilePath, $scoppedContent);
268
269 12
        if (false === isset($exception)) {
270 11
            $logger->outputSuccess($inputFilePath);
271
        }
272
    }
273
274 14
    private function validatePrefix(InputInterface $input): void
275
    {
276 14
        $prefix = $input->getOption(self::PREFIX_OPT);
277
278 14
        if (null !== $prefix && 1 === preg_match('/(?<prefix>.*?)\\\\*$/', $prefix, $matches)) {
279 13
            $prefix = $matches['prefix'];
280
        }
281
282 14
        $input->setOption(self::PREFIX_OPT, $prefix);
283
    }
284
285 14
    private function validatePaths(InputInterface $input): void
286
    {
287 14
        $cwd = getcwd();
288 14
        $fileSystem = $this->fileSystem;
289
290 14
        $paths = array_map(
291
            static function (string $path) use ($cwd, $fileSystem) {
292 9
                if (false === $fileSystem->isAbsolutePath($path)) {
293
                    return $cwd.DIRECTORY_SEPARATOR.$path;
294
                }
295
296 9
                return $path;
297 14
            },
298 14
            $input->getArgument(self::PATH_ARG)
299
        );
300
301 14
        $input->setArgument(self::PATH_ARG, $paths);
302
    }
303
304 14
    private function validateOutputDir(InputInterface $input, OutputStyle $io): void
305
    {
306 14
        $outputDir = $input->getOption(self::OUTPUT_DIR_OPT);
307
308 14
        if (false === $this->fileSystem->isAbsolutePath($outputDir)) {
309 3
            $outputDir = getcwd().DIRECTORY_SEPARATOR.$outputDir;
310
        }
311
312 14
        $input->setOption(self::OUTPUT_DIR_OPT, $outputDir);
313
314 14
        if (false === $this->fileSystem->exists($outputDir)) {
315 14
            return;
316
        }
317
318
        if (false === is_writable($outputDir)) {
319
            throw new RuntimeException(
320
                sprintf(
321
                    'Expected "<comment>%s</comment>" to be writeable.',
322
                    $outputDir
323
                )
324
            );
325
        }
326
327
        if ($input->getOption(self::FORCE_OPT)) {
328
            $this->fileSystem->remove($outputDir);
329
330
            return;
331
        }
332
333
        if (false === is_dir($outputDir)) {
334
            $canDeleteFile = $io->confirm(
335
                sprintf(
336
                    'Expected "<comment>%s</comment>" to be a directory but found a file instead. It will be '
337
                    .'removed, do you wish to proceed?',
338
                    $outputDir
339
                ),
340
                false
341
            );
342
343
            if (false === $canDeleteFile) {
344
                return;
345
            }
346
347
            $this->fileSystem->remove($outputDir);
348
        } else {
349
            $canDeleteFile = $io->confirm(
350
                sprintf(
351
                    'The output directory "<comment>%s</comment>" already exists. Continuing will erase its'
352
                    .' content, do you wish to proceed?',
353
                    $outputDir
354
                ),
355
                false
356
            );
357
358
            if (false === $canDeleteFile) {
359
                return;
360
            }
361
362
            $this->fileSystem->remove($outputDir);
363
        }
364
    }
365
366 14
    private function retrieveConfig(InputInterface $input, OutputInterface $output, OutputStyle $io): Configuration
367
    {
368 14
        $prefix = $input->getOption(self::PREFIX_OPT);
369
370 14
        if ($input->getOption(self::NO_CONFIG_OPT)) {
371 10
            $io->writeln(
372 10
                'Loading without configuration file.',
373 10
                OutputInterface::VERBOSITY_DEBUG
374
            );
375
376 10
            $config = Configuration::load();
377
378 10
            if (null !== $prefix) {
379 9
                $config = $config->withPrefix($prefix);
380
            }
381
382 10
            if (null === $config->getPrefix()) {
383 1
                $config = $config->withPrefix($this->generateRandomPrefix());
384
            }
385
386 10
            return $this->retrievePaths($input, $config);
387
        }
388
389 4
        $configFile = $input->getOption(self::CONFIG_FILE_OPT);
390
391 4
        if (null === $configFile) {
392 3
            $configFile = $this->makeAbsolutePath(self::CONFIG_FILE_DEFAULT);
393
394 3
            if (false === file_exists($configFile) && false === $this->init) {
395
                $this->init = true;
396
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
                    OutputInterface::VERBOSITY_DEBUG
410
                );
411
412
                return self::retrieveConfig($input, $output, $io);
413
            }
414
415 3
            if ($this->init) {
416 3
                $configFile = null;
417
            }
418
        } else {
419 1
            $configFile = $this->makeAbsolutePath($configFile);
420
        }
421
422 4
        if (null === $configFile) {
423
            $io->writeln(
424
                'Loading without configuration file.',
425
                OutputInterface::VERBOSITY_DEBUG
426
            );
427 4
        } elseif (false === file_exists($configFile)) {
428 1
            throw new RuntimeException(
429 1
                sprintf(
430 1
                    'Could not find the configuration file "%s".',
431 1
                    $configFile
432
                )
433
            );
434
        } else {
435 3
            $io->writeln(
436 3
                sprintf(
437 3
                    'Using the configuration file "%s".',
438 3
                    $configFile
439
                ),
440 3
                OutputInterface::VERBOSITY_DEBUG
441
            );
442
        }
443
444 3
        $config = Configuration::load($configFile);
445 2
        $config = $this->retrievePaths($input, $config);
446
447 2
        if (null !== $prefix) {
448 2
            $config = $config->withPrefix($prefix);
449
        }
450
451 2
        if (null === $config->getPrefix()) {
452
            $config = $config->withPrefix($this->generateRandomPrefix());
453
        }
454
455 2
        return $config;
456
    }
457
458 12
    private function retrievePaths(InputInterface $input, Configuration $config): Configuration
459
    {
460
        // Checks if there is any path included and if note use the current working directory as the include path
461 12
        $paths = $input->getArgument(self::PATH_ARG);
462
463 12
        if (0 === count($paths) && 0 === count($config->getFilesWithContents())) {
464 3
            $paths = [getcwd()];
465
        }
466
467 12
        return $config->withPaths($paths);
1 ignored issue
show
Bug introduced by
It seems like $paths defined by $input->getArgument(self::PATH_ARG) on line 461 can also be of type null or string; however, Humbug\PhpScoper\Configuration::withPaths() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
468
    }
469
470 4
    private function makeAbsolutePath(string $path): string
471
    {
472 4
        if (false === $this->fileSystem->isAbsolutePath($path)) {
473 4
            $path = getcwd().DIRECTORY_SEPARATOR.$path;
474
        }
475
476 4
        return $path;
477
    }
478
479 1
    private function generateRandomPrefix(): string
480
    {
481 1
        return uniqid('_PhpScoper', false);
482
    }
483
}
484