Passed
Pull Request — master (#480)
by Théo
02:11
created

AddPrefixCommand::retrieveConfig()   C

Complexity

Conditions 12
Paths 32

Size

Total Lines 91
Code Lines 52

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 33
CRAP Score 17.012

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 12
eloc 52
nc 32
nop 1
dl 0
loc 91
rs 6.9666
c 2
b 0
f 0
ccs 33
cts 49
cp 0.6735
crap 17.012

How to fix   Long Method    Complexity   

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 Fidry\Console\Command\Command;
18
use Fidry\Console\Command\Configuration as CommandConfiguration;
19
use Fidry\Console\IO;
20
use Humbug\PhpScoper\Autoload\ScoperAutoloadGenerator;
21
use Humbug\PhpScoper\Configuration;
22
use Humbug\PhpScoper\Console\ScoperLogger;
23
use Humbug\PhpScoper\Scoper;
24
use Humbug\PhpScoper\Scoper\ConfigurableScoper;
25
use Humbug\PhpScoper\Throwable\Exception\ParsingException;
26
use Humbug\PhpScoper\Whitelist;
27
use Symfony\Component\Console\Exception\RuntimeException;
28
use Symfony\Component\Console\Input\InputArgument;
29
use Symfony\Component\Console\Input\InputOption;
30
use Symfony\Component\Console\Input\StringInput;
31
use Symfony\Component\Console\Output\OutputInterface;
32
use Symfony\Component\Filesystem\Filesystem;
33
use Throwable;
34
use function array_keys;
35
use function array_map;
36
use function bin2hex;
37
use function count;
38
use function file_exists;
39
use function Humbug\PhpScoper\get_common_path;
40
use function is_dir;
41
use function is_writable;
42
use function preg_match as native_preg_match;
43
use function random_bytes;
44
use function Safe\file_get_contents;
45
use function Safe\getcwd;
46
use function Safe\sprintf;
47
use function Safe\usort;
48
use function str_replace;
49
use function strlen;
50
use const DIRECTORY_SEPARATOR;
51
52
final class AddPrefixCommand implements Command
53
{
54
    private const PATH_ARG = 'paths';
55 16
    private const PREFIX_OPT = 'prefix';
56
    private const OUTPUT_DIR_OPT = 'output-dir';
57 16
    private const FORCE_OPT = 'force';
58
    private const STOP_ON_FAILURE_OPT = 'stop-on-failure';
59 16
    private const CONFIG_FILE_OPT = 'config';
60 16
    private const CONFIG_FILE_DEFAULT = 'scoper.inc.php';
61
    private const NO_CONFIG_OPT = 'no-config';
62
63
    private Filesystem $fileSystem;
64
    private ConfigurableScoper $scoper;
65
    private bool $init = false;
66 16
67
    public function __construct(Filesystem $fileSystem, Scoper $scoper)
68 16
    {
69
        $this->fileSystem = $fileSystem;
70
        $this->scoper = new ConfigurableScoper($scoper);
71 16
    }
72 16
73 16
    public function getConfiguration(): CommandConfiguration
74 16
    {
75 16
        return new CommandConfiguration(
76 16
            'add-prefix',
77
            'Goes through all the PHP files found in the given paths to apply the given prefix to namespaces & FQNs.',
78 16
            '',
79 16
            [
80 16
                new InputArgument(
81 16
                    self::PATH_ARG,
82 16
                    InputArgument::IS_ARRAY,
83
                    'The path(s) to process.'
84 16
                ),
85 16
            ],
86 16
            [
87 16
                ChangeableDirectory::createOption(),
88 16
                new InputOption(
89 16
                    self::PREFIX_OPT,
90
                    'p',
91 16
                    InputOption::VALUE_REQUIRED,
92 16
                    'The namespace prefix to add.',
93 16
                ),
94 16
                new InputOption(
95 16
                    self::OUTPUT_DIR_OPT,
96
                    'o',
97 16
                    InputOption::VALUE_REQUIRED,
98 16
                    'The output directory in which the prefixed code will be dumped.',
99 16
                    'build',
100 16
                ),
101 16
                new InputOption(
102
                    self::FORCE_OPT,
103 16
                    'f',
104 16
                    InputOption::VALUE_NONE,
105 16
                    'Deletes any existing content in the output directory without any warning.'
106 16
                ),
107 16
                new InputOption(
108 16
                    self::STOP_ON_FAILURE_OPT,
109 16
                    's',
110
                    InputOption::VALUE_NONE,
111
                    'Stops on failure.'
112 16
                ),
113 16
                new InputOption(
114 16
                    self::CONFIG_FILE_OPT,
115 16
                    'c',
116 16
                    InputOption::VALUE_REQUIRED,
117
                    sprintf(
118
                        'Conf,iguration file. Will use "%s" if found by default.',
119
                        self::CONFIG_FILE_DEFAULT
120
                    )
121
                ),
122
                new InputOption(
123
                    self::NO_CONFIG_OPT,
124 14
                    null,
125
                    InputOption::VALUE_NONE,
126 14
                    'Do not look for a configuration file.'
127 14
                ),
128
            ],
129 14
        );
130
    }
131 14
132 14
    public function execute(IO $io): int
133 14
    {
134
        $io->writeln('');
135 14
136 12
        ChangeableDirectory::changeWorkingDirectory($io);
137
138 12
        $this->validatePrefix($io);
139
        $this->validatePaths($io);
140
        $this->validateOutputDir($io);
141
142 12
        $config = $this->retrieveConfig($io);
143 12
        $output = $io->getStringOption(self::OUTPUT_DIR_OPT);
144 12
145
        if ([] !== $config->getWhitelistedFiles()) {
146
            $this->scoper = $this->scoper->withWhitelistedFiles(...$config->getWhitelistedFiles());
147 12
        }
148 12
149 12
        $logger = new ScoperLogger(
150
            // TODO
151
            $this->getApplication(),
0 ignored issues
show
Bug introduced by
The method getApplication() does not exist on Humbug\PhpScoper\Console\Command\AddPrefixCommand. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

151
            $this->/** @scrutinizer ignore-call */ 
152
                   getApplication(),

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
152
            $io,
153 12
        );
154 12
155 12
        $logger->outputScopingStart(
156 12
            $config->getPrefix(),
157 12
            $io->getStringArrayArgument(self::PATH_ARG)
158 12
        );
159 12
160 12
        try {
161
            $this->scopeFiles(
162
                $config->getPrefix(),
163
                $config->getFilesWithContents(),
164
                $output,
165
                $config->getPatchers(),
166
                $config->getWhitelist(),
167
                $io->getBooleanOption(self::STOP_ON_FAILURE_OPT),
168
                $logger
169
            );
170 12
        } catch (Throwable $throwable) {
171
            $this->fileSystem->remove($output);
172 12
173
            $logger->outputScopingEndWithFailure();
174
175
            throw $throwable;
176
        }
177
178 12
        $logger->outputScopingEnd();
179
180
        return 0;
181
    }
182
183
    /**
184
     * @var callable[]
185
     */
186
    private function scopeFiles(
187
        string $prefix,
188 12
        array $filesWithContents,
189
        string $output,
190 12
        array $patchers,
191
        Whitelist $whitelist,
192 12
        bool $stopOnFailure,
193 12
        ScoperLogger $logger
194
    ): void {
195 12
        // Creates output directory if does not already exist
196 12
        $this->fileSystem->mkdir($output);
197
198 12
        $logger->outputFileCount(count($filesWithContents));
199 12
200
        $vendorDirs = [];
201
        $commonPath = get_common_path(array_keys($filesWithContents));
202
203 12
        foreach ($filesWithContents as [$inputFilePath, $inputContents]) {
204 12
            $outputFilePath = $output.str_replace($commonPath, '', $inputFilePath);
205 12
206 12
            $pattern = '~((?:.*)\\'.DIRECTORY_SEPARATOR.'vendor)\\'.DIRECTORY_SEPARATOR.'.*~';
207 12
            if (native_preg_match($pattern, $outputFilePath, $matches)) {
208 12
                $vendorDirs[$matches[1]] = true;
209 12
            }
210 12
211 12
            $this->scopeFile(
212
                $inputFilePath,
213
                $inputContents,
214
                $outputFilePath,
215 12
                $prefix,
216
                $patchers,
217 12
                $whitelist,
218 12
                $stopOnFailure,
219
                $logger
220
            );
221 12
        }
222
223
        $vendorDirs = array_keys($vendorDirs);
224 12
225
        usort(
226 12
            $vendorDirs,
227
            static function ($a, $b) {
228
                return strlen($b) <=> strlen($a);
229
            }
230
        );
231
232
        $vendorDir = (0 === count($vendorDirs)) ? null : $vendorDirs[0];
233
234
        if (null !== $vendorDir) {
235
            $autoload = (new ScoperAutoloadGenerator($whitelist))->dump();
236 12
237
            $this->fileSystem->dumpFile($vendorDir.'/scoper-autoload.php', $autoload);
238
        }
239
    }
240
241
    /**
242
     * @param callable[] $patchers
243
     */
244
    private function scopeFile(
245
        string $inputFilePath,
246
        string $inputContents,
247 12
        string $outputFilePath,
248 2
        string $prefix,
249 2
        array $patchers,
250 2
        Whitelist $whitelist,
251 2
        bool $stopOnFailure,
252 2
        ScoperLogger $logger
253
    ): void {
254 2
        try {
255 2
            $scoppedContent = $this->scoper->scope($inputFilePath, $inputContents, $prefix, $patchers, $whitelist);
256
        } catch (Throwable $throwable) {
257
            $exception = new ParsingException(
258 2
                sprintf(
259
                    'Could not parse the file "%s".',
260
                    $inputFilePath
261
                ),
262 2
                0,
263
                $throwable
264 2
            );
265
266
            if ($stopOnFailure) {
267 12
                throw $exception;
268
            }
269 12
270 11
            $logger->outputWarnOfFailure($inputFilePath, $exception);
271
272
            $scoppedContent = file_get_contents($inputFilePath);
273
        }
274 14
275
        $this->fileSystem->dumpFile($outputFilePath, $scoppedContent);
276 14
277
        if (false === isset($exception)) {
278 14
            $logger->outputSuccess($inputFilePath);
279 13
        }
280
    }
281
282 14
    private function validatePrefix(IO $io): void
283
    {
284
        $prefix = $io->getNullableStringOption(self::PREFIX_OPT);
285 14
286
        if (null !== $prefix && 1 === native_preg_match('/(?<prefix>.*?)\\\\*$/', $prefix, $matches)) {
287 14
            $prefix = $matches['prefix'];
288 14
        }
289
290 14
        $io->getInput()->setOption(self::PREFIX_OPT, $prefix);
291
    }
292 9
293
    private function validatePaths(IO $io): void
294
    {
295
        $cwd = getcwd();
296 9
        $fileSystem = $this->fileSystem;
297 14
298 14
        $paths = array_map(
299
            static function (string $path) use ($cwd, $fileSystem) {
300
                if (false === $fileSystem->isAbsolutePath($path)) {
301 14
                    return $cwd.DIRECTORY_SEPARATOR.$path;
302
                }
303
304 14
                return $path;
305
            },
306 14
            $io->getStringArrayArgument(self::PATH_ARG)
307
        );
308 14
309 3
        $io->getInput()->setArgument(self::PATH_ARG, $paths);
310
    }
311
312 14
    private function validateOutputDir(IO $io): void
313
    {
314 14
        $outputDir = $io->getStringOption(self::OUTPUT_DIR_OPT);
315 14
316
        if (false === $this->fileSystem->isAbsolutePath($outputDir)) {
317
            $outputDir = getcwd().DIRECTORY_SEPARATOR.$outputDir;
318
        }
319
320
        $io->getInput()->setOption(self::OUTPUT_DIR_OPT, $outputDir);
321
322
        if (false === $this->fileSystem->exists($outputDir)) {
323
            return;
324
        }
325
326
        if (false === is_writable($outputDir)) {
327
            throw new RuntimeException(
328
                sprintf(
329
                    'Expected "<comment>%s</comment>" to be writeable.',
330
                    $outputDir
331
                )
332
            );
333
        }
334
335
        if ($io->getBooleanOption(self::FORCE_OPT)) {
336
            $this->fileSystem->remove($outputDir);
337
338
            return;
339
        }
340
341
        if (false === is_dir($outputDir)) {
342
            $canDeleteFile = $io->confirm(
343
                sprintf(
344
                    'Expected "<comment>%s</comment>" to be a directory but found a file instead. It will be '
345
                    .'removed, do you wish to proceed?',
346
                    $outputDir
347
                ),
348
                false
349
            );
350
351
            if (false === $canDeleteFile) {
352
                return;
353
            }
354
355
            $this->fileSystem->remove($outputDir);
356
        } else {
357
            $canDeleteFile = $io->confirm(
358
                sprintf(
359
                    'The output directory "<comment>%s</comment>" already exists. Continuing will erase its'
360
                    .' content, do you wish to proceed?',
361
                    $outputDir
362
                ),
363
                false
364
            );
365
366 14
            if (false === $canDeleteFile) {
367
                return;
368 14
            }
369
370 14
            $this->fileSystem->remove($outputDir);
371 10
        }
372 10
    }
373 10
374
    private function retrieveConfig(IO $io): Configuration
375
    {
376 10
        $prefix = $io->getStringOption(self::PREFIX_OPT);
377
378 10
        if ($io->getBooleanOption(self::NO_CONFIG_OPT)) {
379 9
            $io->writeln(
380
                'Loading without configuration file.',
381
                OutputInterface::VERBOSITY_DEBUG
382 10
            );
383 1
384
            $config = Configuration::load();
385
386 10
            if (null !== $prefix) {
0 ignored issues
show
introduced by
The condition null !== $prefix is always true.
Loading history...
387
                $config = $config->withPrefix($prefix);
388
            }
389 4
390
            if (null === $config->getPrefix()) {
391 4
                $config = $config->withPrefix(self::generateRandomPrefix());
392 3
            }
393
394 3
            return $this->retrievePaths($io, $config);
395
        }
396
397
        $configFile = $io->getNullableStringOption(self::CONFIG_FILE_OPT);
398
399
        if (null === $configFile) {
400
            $configFile = $this->makeAbsolutePath(self::CONFIG_FILE_DEFAULT);
401
402
            if (false === $this->init && false === file_exists($configFile)) {
403
                $this->init = true;
404
405
                // TODO
406
                $initCommand = $this->getApplication()->find('init');
407
408
                $initInput = new StringInput('');
409
                $initInput->setInteractive($io->isInteractive());
410
411
                $initCommand->run($initInput, $io->getOutput());
412
413
                $io->writeln(
414
                    sprintf(
415 3
                        'Config file "<comment>%s</comment>" not found. Skipping.',
416
                        $configFile
417
                    ),
418
                    OutputInterface::VERBOSITY_DEBUG
419 1
                );
420
421
                return self::retrieveConfig($io);
0 ignored issues
show
Bug Best Practice introduced by
The method Humbug\PhpScoper\Console...mmand::retrieveConfig() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

421
                return self::/** @scrutinizer ignore-call */ retrieveConfig($io);
Loading history...
422 4
            }
423
424
            if ($this->init) {
425
                $configFile = null;
426
            }
427 4
        } else {
428 1
            $configFile = $this->makeAbsolutePath($configFile);
429 1
        }
430 1
431 1
        if (null === $configFile) {
432
            $io->writeln(
433
                'Loading without configuration file.',
434
                OutputInterface::VERBOSITY_DEBUG
435 3
            );
436 3
        } elseif (false === file_exists($configFile)) {
437 3
            throw new RuntimeException(
438 3
                sprintf(
439
                    'Could not find the configuration file "%s".',
440 3
                    $configFile
441
                )
442
            );
443
        } else {
444 3
            $io->writeln(
445 2
                sprintf(
446
                    'Using the configuration file "%s".',
447 2
                    $configFile
448 2
                ),
449
                OutputInterface::VERBOSITY_DEBUG
450
            );
451 2
        }
452
453
        $config = Configuration::load($configFile);
454
        $config = $this->retrievePaths($io, $config);
455 2
456
        if (null !== $prefix) {
0 ignored issues
show
introduced by
The condition null !== $prefix is always true.
Loading history...
457
            $config = $config->withPrefix($prefix);
458 12
        }
459
460
        if (null === $config->getPrefix()) {
461 12
            $config = $config->withPrefix(self::generateRandomPrefix());
462
        }
463 12
464 3
        return $config;
465
    }
466
467 12
    private function retrievePaths(IO $io, Configuration $config): Configuration
468
    {
469
        // Checks if there is any path included and if note use the current working directory as the include path
470 4
        $paths = $io->getStringArrayArgument(self::PATH_ARG);
471
472 4
        if (0 === count($paths) && 0 === count($config->getFilesWithContents())) {
473 4
            $paths = [getcwd()];
474
        }
475
476 4
        return $config->withPaths($paths);
477
    }
478
479 1
    private function makeAbsolutePath(string $path): string
480
    {
481 1
        if (false === $this->fileSystem->isAbsolutePath($path)) {
482
            $path = getcwd().DIRECTORY_SEPARATOR.$path;
483
        }
484
485
        return $path;
486
    }
487
488
    private static function generateRandomPrefix(): string
489
    {
490
        return '_PhpScoper'.bin2hex(random_bytes(6));
491
    }
492
}
493