Passed
Pull Request — master (#482)
by Théo
01:57
created

AddPrefixCommand::getConfiguration()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 54
Code Lines 42

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 37
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 42
nc 1
nop 0
dl 0
loc 54
rs 9.248
c 0
b 0
f 0
ccs 37
cts 37
cp 1
crap 1

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

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

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

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