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

AddPrefixCommand::scopeFiles()   A

Complexity

Conditions 5
Paths 12

Size

Total Lines 52
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 24
CRAP Score 5.0729

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 24
cts 28
cp 0.8571
crap 5.0729
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\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