Passed
Push — master ( 91166f...c0f813 )
by Théo
03:26 queued 01:21
created

AddPrefixCommand::execute()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 48
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 3.054

Importance

Changes 0
Metric Value
cc 3
eloc 30
nc 4
nop 1
dl 0
loc 48
ccs 18
cts 22
cp 0.8182
crap 3.054
rs 9.44
c 0
b 0
f 0
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(),
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

165
            /** @scrutinizer ignore-type */ $config->getPrefix(),
Loading history...
166
            self::getPathArguments($io),
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->getStringOption(self::PREFIX_OPT);
294
295
        if (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
            self::getPathArguments($io),
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 = self::getPathArguments($io);
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
    /**
499
     * @return string[]
500
     */
501
    private static function getPathArguments(IO $io): array
502
    {
503
        return $io->getStringArrayArgument(self::PATH_ARG);
504
    }
505
506
    private static function generateRandomPrefix(): string
507
    {
508
        return '_PhpScoper'.bin2hex(random_bytes(6));
509
    }
510
}
511