Passed
Branch master (c976c6)
by Théo
03:51
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 0
Metric Value
cc 5
eloc 27
nc 12
nop 7
dl 0
loc 52
ccs 24
cts 28
cp 0.8571
crap 5.0729
rs 9.1768
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\Console\ScoperLogger;
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 ScoperLogger(
143 12
            $this->getApplication(),
144 12
            $io
145
        );
146
147 12
        $logger->outputScopingStart(
148 12
            $config->getPrefix(),
149 12
            $input->getArgument(self::PATH_ARG)
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
        ScoperLogger $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);
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,
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
        ScoperLogger $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)) {
1 ignored issue
show
Bug introduced by
It seems like $prefix can also be of type string[]; however, parameter $subject of preg_match() 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

278
        if (null !== $prefix && 1 === preg_match('/(?<prefix>.*?)\\\\*$/', /** @scrutinizer ignore-type */ $prefix, $matches)) {
Loading history...
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
                $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);
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