Passed
Push — master ( f59561...f05f0a )
by Théo
03:05
created

AddPrefixCommand::validateOutputDir()   B

Complexity

Conditions 8
Paths 14

Size

Total Lines 61

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 38.5117

Importance

Changes 0
Metric Value
cc 8
nc 14
nop 2
dl 0
loc 61
ccs 7
cts 32
cp 0.2188
crap 38.5117
rs 7.6064
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\Logger\ConsoleLogger;
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 ConsoleLogger(
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
        ConsoleLogger $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);
1 ignored issue
show
Bug introduced by
The variable $inputFilePath does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
197
198 12
            if (preg_match('~((?:.*)\/vendor)\/.*~', $outputFilePath, $matches)) {
199
                $vendorDirs[$matches[1]] = true;
200
            }
201
202 12
            $this->scopeFile(
203 12
                $inputFilePath,
204 12
                $inputContents,
1 ignored issue
show
Bug introduced by
The variable $inputContents does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
205 12
                $outputFilePath,
206 12
                $prefix,
207 12
                $patchers,
208 12
                $whitelist,
209 12
                $stopOnFailure,
210 12
                $logger
211
            );
212
        }
213
214 12
        $vendorDirs = array_keys($vendorDirs);
215
216 12
        usort(
217 12
            $vendorDirs,
218
            function ($a, $b) {
219
                return strlen($b) <=> strlen($a);
220 12
            }
221
        );
222
223 12
        $vendorDir = (0 === count($vendorDirs)) ? null : $vendorDirs[0];
224
225 12
        if (null !== $vendorDir) {
226
            $autoload = (new ScoperAutoloadGenerator($whitelist))->dump($prefix);
227
228
            $this->fileSystem->dumpFile($vendorDir.'/scoper-autoload.php', $autoload);
229
        }
230
    }
231
232
    /**
233
     * @param callable[] $patchers
234
     */
235 12
    private function scopeFile(
236
        string $inputFilePath,
237
        string $inputContents,
238
        string $outputFilePath,
239
        string $prefix,
240
        array $patchers,
241
        Whitelist $whitelist,
242
        bool $stopOnFailure,
243
        ConsoleLogger $logger
244
    ): void {
245
        try {
246 12
            $scoppedContent = $this->scoper->scope($inputFilePath, $inputContents, $prefix, $patchers, $whitelist);
247 2
        } catch (Throwable $throwable) {
248 2
            $exception = new ParsingException(
249 2
                sprintf(
250 2
                    'Could not parse the file "%s".',
251 2
                    $inputFilePath
252
                ),
253 2
                0,
254 2
                $throwable
255
            );
256
257 2
            if ($stopOnFailure) {
258
                throw $exception;
259
            }
260
261 2
            $logger->outputWarnOfFailure($inputFilePath, $exception);
262
263 2
            $scoppedContent = file_get_contents($inputFilePath);
264
        }
265
266 12
        $this->fileSystem->dumpFile($outputFilePath, $scoppedContent);
267
268 12
        if (false === isset($exception)) {
269 11
            $logger->outputSuccess($inputFilePath);
270
        }
271
    }
272
273 14
    private function validatePrefix(InputInterface $input): void
274
    {
275 14
        $prefix = $input->getOption(self::PREFIX_OPT);
276
277 14
        if (null !== $prefix && 1 === preg_match('/(?<prefix>.*?)\\\\*$/', $prefix, $matches)) {
278 13
            $prefix = $matches['prefix'];
279
        }
280
281 14
        $input->setOption(self::PREFIX_OPT, $prefix);
282
    }
283
284 14
    private function validatePaths(InputInterface $input): void
285
    {
286 14
        $cwd = getcwd();
287 14
        $fileSystem = $this->fileSystem;
288
289 14
        $paths = array_map(
290
            function (string $path) use ($cwd, $fileSystem) {
291 9
                if (false === $fileSystem->isAbsolutePath($path)) {
292
                    return $cwd.DIRECTORY_SEPARATOR.$path;
293
                }
294
295 9
                return $path;
296 14
            },
297 14
            $input->getArgument(self::PATH_ARG)
298
        );
299
300 14
        $input->setArgument(self::PATH_ARG, $paths);
301
    }
302
303 14
    private function validateOutputDir(InputInterface $input, OutputStyle $io): void
304
    {
305 14
        $outputDir = $input->getOption(self::OUTPUT_DIR_OPT);
306
307 14
        if (false === $this->fileSystem->isAbsolutePath($outputDir)) {
308 3
            $outputDir = getcwd().DIRECTORY_SEPARATOR.$outputDir;
309
        }
310
311 14
        $input->setOption(self::OUTPUT_DIR_OPT, $outputDir);
312
313 14
        if (false === $this->fileSystem->exists($outputDir)) {
314 14
            return;
315
        }
316
317
        if (false === is_writable($outputDir)) {
318
            throw new RuntimeException(
319
                sprintf(
320
                    'Expected "<comment>%s</comment>" to be writeable.',
321
                    $outputDir
322
                )
323
            );
324
        }
325
326
        if ($input->getOption(self::FORCE_OPT)) {
327
            $this->fileSystem->remove($outputDir);
328
329
            return;
330
        }
331
332
        if (false === is_dir($outputDir)) {
333
            $canDeleteFile = $io->confirm(
334
                sprintf(
335
                    'Expected "<comment>%s</comment>" to be a directory but found a file instead. It will be '
336
                    .'removed, do you wish to proceed?',
337
                    $outputDir
338
                ),
339
                false
340
            );
341
342
            if (false === $canDeleteFile) {
343
                return;
344
            }
345
346
            $this->fileSystem->remove($outputDir);
347
        } else {
348
            $canDeleteFile = $io->confirm(
349
                sprintf(
350
                    'The output directory "<comment>%s</comment>" already exists. Continuing will erase its'
351
                    .' content, do you wish to proceed?',
352
                    $outputDir
353
                ),
354
                false
355
            );
356
357
            if (false === $canDeleteFile) {
358
                return;
359
            }
360
361
            $this->fileSystem->remove($outputDir);
362
        }
363
    }
364
365 14
    private function retrieveConfig(InputInterface $input, OutputInterface $output, OutputStyle $io): Configuration
366
    {
367 14
        $prefix = $input->getOption(self::PREFIX_OPT);
368
369 14
        if ($input->getOption(self::NO_CONFIG_OPT)) {
370 10
            $io->writeln(
371 10
                'Loading without configuration file.',
372 10
                OutputInterface::VERBOSITY_DEBUG
373
            );
374
375 10
            $config = Configuration::load();
376
377 10
            if (null !== $prefix) {
378 9
                $config = $config->withPrefix($prefix);
379
            }
380
381 10
            if (null === $config->getPrefix()) {
382 1
                $config = $config->withPrefix($this->generateRandomPrefix());
383
            }
384
385 10
            return $this->retrievePaths($input, $config);
386
        }
387
388 4
        $configFile = $input->getOption(self::CONFIG_FILE_OPT);
389
390 4
        if (null === $configFile) {
391 3
            $configFile = $this->makeAbsolutePath(self::CONFIG_FILE_DEFAULT);
392
393 3
            if (false === file_exists($configFile) && false === $this->init) {
394
                $this->init = true;
395
396
                $initCommand = $this->getApplication()->find('init');
397
398
                $initInput = new StringInput('');
399
                $initInput->setInteractive($input->isInteractive());
400
401
                $initCommand->run($initInput, $output);
402
403
                $io->writeln(
404
                    sprintf(
405
                        'Config file "<comment>%s</comment>" not found. Skipping.',
406
                        $configFile
407
                    ),
408
                    OutputInterface::VERBOSITY_DEBUG
409
                );
410
411
                return self::retrieveConfig($input, $output, $io);
412
            }
413
414 3
            if ($this->init) {
415 3
                $configFile = null;
416
            }
417
        } else {
418 1
            $configFile = $this->makeAbsolutePath($configFile);
419
        }
420
421 4
        if (null === $configFile) {
422
            $io->writeln(
423
                'Loading without configuration file.',
424
                OutputInterface::VERBOSITY_DEBUG
425
            );
426 4
        } elseif (false === file_exists($configFile)) {
427 1
            throw new RuntimeException(
428 1
                sprintf(
429 1
                    'Could not find the configuration file "%s".',
430 1
                    $configFile
431
                )
432
            );
433
        } else {
434 3
            $io->writeln(
435 3
                sprintf(
436 3
                    'Using the configuration file "%s".',
437 3
                    $configFile
438
                ),
439 3
                OutputInterface::VERBOSITY_DEBUG
440
            );
441
        }
442
443 3
        $config = Configuration::load($configFile);
444 2
        $config = $this->retrievePaths($input, $config);
445
446 2
        if (null !== $prefix) {
447 2
            $config = $config->withPrefix($prefix);
448
        }
449
450 2
        if (null === $config->getPrefix()) {
451
            $config = $config->withPrefix($this->generateRandomPrefix());
452
        }
453
454 2
        return $config;
455
    }
456
457 12
    private function retrievePaths(InputInterface $input, Configuration $config): Configuration
458
    {
459
        // Checks if there is any path included and if note use the current working directory as the include path
460 12
        $paths = $input->getArgument(self::PATH_ARG);
461
462 12
        if (0 === count($paths) && 0 === count($config->getFilesWithContents())) {
463 3
            $paths = [getcwd()];
464
        }
465
466 12
        return $config->withPaths($paths);
467
    }
468
469 4
    private function makeAbsolutePath(string $path): string
470
    {
471 4
        if (false === $this->fileSystem->isAbsolutePath($path)) {
472 4
            $path = getcwd().DIRECTORY_SEPARATOR.$path;
473
        }
474
475 4
        return $path;
476
    }
477
478 1
    private function generateRandomPrefix(): string
479
    {
480 1
        return uniqid('_PhpScoper', false);
481
    }
482
}
483