Passed
Pull Request — master (#480)
by Théo
129:15 queued 45:26
created

AddPrefixCommand::scopeFiles()   A

Complexity

Conditions 5
Paths 12

Size

Total Lines 52
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 5.1158

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 5
eloc 27
c 2
b 0
f 0
nc 12
nop 7
dl 0
loc 52
ccs 20
cts 24
cp 0.8333
crap 5.1158
rs 9.1768

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

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