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

AddPrefixCommand::scopeFile()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 35
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 4.0039

Importance

Changes 0
Metric Value
cc 4
eloc 15
nc 5
nop 8
dl 0
loc 35
ccs 15
cts 16
cp 0.9375
crap 4.0039
rs 9.7666
c 0
b 0
f 0

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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