Passed
Push — master ( 157651...e7eb4b )
by Théo
03:29
created

AddPrefixCommand::validatePaths()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 2.004

Importance

Changes 0
Metric Value
cc 2
nc 1
nop 1
dl 0
loc 18
ccs 9
cts 10
cp 0.9
crap 2.004
rs 9.6666
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 Humbug\PhpScoper\Autoload\ScoperAutoloadGenerator;
18
use Humbug\PhpScoper\Configuration;
19
use Humbug\PhpScoper\Logger\ConsoleLogger;
20
use Humbug\PhpScoper\Scoper;
21
use Humbug\PhpScoper\Throwable\Exception\ParsingException;
22
use Humbug\PhpScoper\Whitelist;
23
use Symfony\Component\Console\Exception\RuntimeException;
24
use Symfony\Component\Console\Input\InputArgument;
25
use Symfony\Component\Console\Input\InputInterface;
26
use Symfony\Component\Console\Input\InputOption;
27
use Symfony\Component\Console\Input\StringInput;
28
use Symfony\Component\Console\Output\OutputInterface;
29
use Symfony\Component\Console\Style\OutputStyle;
30
use Symfony\Component\Console\Style\SymfonyStyle;
31
use Symfony\Component\Filesystem\Filesystem;
32
use Throwable;
33
use function Humbug\PhpScoper\get_common_path;
34
35
final class AddPrefixCommand extends BaseCommand
36
{
37
    private const PATH_ARG = 'paths';
38
    private const PREFIX_OPT = 'prefix';
39
    private const OUTPUT_DIR_OPT = 'output-dir';
40
    private const FORCE_OPT = 'force';
41
    private const STOP_ON_FAILURE_OPT = 'stop-on-failure';
42
    private const CONFIG_FILE_OPT = 'config';
43
    private const CONFIG_FILE_DEFAULT = 'scoper.inc.php';
44
    private const NO_CONFIG_OPT = 'no-config';
45
46
    private $fileSystem;
47
    private $scoper;
48
    private $init = false;
49
50
    /**
51
     * @inheritdoc
52
     */
53 16
    public function __construct(Filesystem $fileSystem, Scoper $scoper)
54
    {
55 16
        parent::__construct();
56
57 16
        $this->fileSystem = $fileSystem;
58 16
        $this->scoper = $scoper;
59
    }
60
61
    /**
62
     * @inheritdoc
63
     */
64 16
    protected function configure(): void
65
    {
66 16
        parent::configure();
67
68
        $this
69 16
            ->setName('add-prefix')
70 16
            ->setDescription('Goes through all the PHP files found in the given paths to apply the given prefix to namespaces & FQNs.')
71 16
            ->addArgument(
72 16
                self::PATH_ARG,
73 16
                InputArgument::IS_ARRAY,
74 16
                'The path(s) to process.'
75
            )
76 16
            ->addOption(
77 16
                self::PREFIX_OPT,
78 16
                'p',
79 16
                InputOption::VALUE_REQUIRED,
80 16
                'The namespace prefix to add.'
81
            )
82 16
            ->addOption(
83 16
                self::OUTPUT_DIR_OPT,
84 16
                'o',
85 16
                InputOption::VALUE_REQUIRED,
86 16
                'The output directory in which the prefixed code will be dumped.',
87 16
                'build'
88
            )
89 16
            ->addOption(
90 16
                self::FORCE_OPT,
91 16
                'f',
92 16
                InputOption::VALUE_NONE,
93 16
                'Deletes any existing content in the output directory without any warning.'
94
            )
95 16
            ->addOption(
96 16
                self::STOP_ON_FAILURE_OPT,
97 16
                's',
98 16
                InputOption::VALUE_NONE,
99 16
                'Stops on failure.'
100
            )
101 16
            ->addOption(
102 16
                self::CONFIG_FILE_OPT,
103 16
                'c',
104 16
                InputOption::VALUE_REQUIRED,
105 16
                sprintf(
106 16
                    'Configuration file. Will use "%s" if found by default.',
107 16
                    self::CONFIG_FILE_DEFAULT
108
                )
109
            )
110 16
            ->addOption(
111 16
                self::NO_CONFIG_OPT,
112 16
                null,
113 16
                InputOption::VALUE_NONE,
114 16
            'Do not look for a configuration file.'
115
            )
116
        ;
117
    }
118
119
    /**
120
     * @inheritdoc
121
     */
122 14
    protected function execute(InputInterface $input, OutputInterface $output): int
123
    {
124 14
        $io = new SymfonyStyle($input, $output);
125 14
        $io->writeln('');
126
127 14
        $this->changeWorkingDirectory($input);
128
129 14
        $this->validatePrefix($input);
130 14
        $this->validatePaths($input);
131 14
        $this->validateOutputDir($input, $io);
132
133 14
        $config = $this->retrieveConfig($input, $output, $io);
134 12
        $output = $input->getOption(self::OUTPUT_DIR_OPT);
135
136 12
        $logger = new ConsoleLogger(
137 12
            $this->getApplication(),
138 12
            $io
139
        );
140
141 12
        $logger->outputScopingStart(
142 12
            $config->getPrefix(),
143 12
            $input->getArgument(self::PATH_ARG)
144
        );
145
146
        try {
147 12
            $this->scopeFiles(
148 12
                $config->getPrefix(),
149 12
                $config->getFilesWithContents(),
150 12
                $output,
151 12
                $config->getPatchers(),
152 12
                $config->getWhitelist(),
153 12
                $input->getOption(self::STOP_ON_FAILURE_OPT),
154 12
                $logger
155
            );
156
        } catch (Throwable $throwable) {
157
            $this->fileSystem->remove($output);
158
159
            $logger->outputScopingEndWithFailure();
160
161
            throw $throwable;
162
        }
163
164 12
        $logger->outputScopingEnd();
165
166 12
        return 0;
167
    }
168
169
    /**
170
     * @var callable[]
171
     */
172 12
    private function scopeFiles(
173
        string $prefix,
174
        array $filesWithContents,
175
        string $output,
176
        array $patchers,
177
        Whitelist $whitelist,
178
        bool $stopOnFailure,
179
        ConsoleLogger $logger
180
    ): void {
181
        // Creates output directory if does not already exist
182 12
        $this->fileSystem->mkdir($output);
183
184 12
        $logger->outputFileCount(count($filesWithContents));
185
186 12
        $vendorDirs = [];
187 12
        $commonPath = get_common_path(array_keys($filesWithContents));
188
189 12
        foreach ($filesWithContents as [$inputFilePath, $inputContents]) {
190 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...
191
192 12
            if (preg_match('~((?:.*)\/vendor)\/.*~', $outputFilePath, $matches)) {
193
                $vendorDirs[$matches[1]] = true;
194
            }
195
196 12
            $this->scopeFile(
197 12
                $inputFilePath,
198 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...
199 12
                $outputFilePath,
200 12
                $prefix,
201 12
                $patchers,
202 12
                $whitelist,
203 12
                $stopOnFailure,
204 12
                $logger
205
            );
206
        }
207
208 12
        $vendorDirs = array_keys($vendorDirs);
209
210 12
        usort(
211 12
            $vendorDirs,
212
            function ($a, $b) {
213
                return strlen($b) <=> strlen($a);
214 12
            }
215
        );
216
217 12
        $vendorDir = (0 === count($vendorDirs)) ? null : $vendorDirs[0];
218
219 12
        if (null !== $vendorDir) {
220
            $autoload = (new ScoperAutoloadGenerator($whitelist))->dump($prefix);
221
222
            $this->fileSystem->dumpFile($vendorDir.'/scoper-autoload.php', $autoload);
223
        }
224
    }
225
226
    /**
227
     * @param callable[] $patchers
228
     */
229 12
    private function scopeFile(
230
        string $inputFilePath,
231
        string $inputContents,
232
        string $outputFilePath,
233
        string $prefix,
234
        array $patchers,
235
        Whitelist $whitelist,
236
        bool $stopOnFailure,
237
        ConsoleLogger $logger
238
    ): void {
239
        try {
240 12
            $scoppedContent = $this->scoper->scope($inputFilePath, $inputContents, $prefix, $patchers, $whitelist);
241 2
        } catch (Throwable $throwable) {
242 2
            $exception = new ParsingException(
243 2
                sprintf(
244 2
                    'Could not parse the file "%s".',
245 2
                    $inputFilePath
246
                ),
247 2
                0,
248 2
                $throwable
249
            );
250
251 2
            if ($stopOnFailure) {
252
                throw $exception;
253
            }
254
255 2
            $logger->outputWarnOfFailure($inputFilePath, $exception);
256
257 2
            $scoppedContent = file_get_contents($inputFilePath);
258
        }
259
260 12
        $this->fileSystem->dumpFile($outputFilePath, $scoppedContent);
261
262 12
        if (false === isset($exception)) {
263 11
            $logger->outputSuccess($inputFilePath);
264
        }
265
    }
266
267 14
    private function validatePrefix(InputInterface $input): void
268
    {
269 14
        $prefix = $input->getOption(self::PREFIX_OPT);
270
271 14
        if (null !== $prefix && 1 === preg_match('/(?<prefix>.*?)\\\\*$/', $prefix, $matches)) {
272 13
            $prefix = $matches['prefix'];
273
        }
274
275 14
        $input->setOption(self::PREFIX_OPT, $prefix);
276
    }
277
278 14
    private function validatePaths(InputInterface $input): void
279
    {
280 14
        $cwd = getcwd();
281 14
        $fileSystem = $this->fileSystem;
282
283 14
        $paths = array_map(
284
            function (string $path) use ($cwd, $fileSystem) {
285 9
                if (false === $fileSystem->isAbsolutePath($path)) {
286
                    return $cwd.DIRECTORY_SEPARATOR.$path;
287
                }
288
289 9
                return $path;
290 14
            },
291 14
            $input->getArgument(self::PATH_ARG)
292
        );
293
294 14
        $input->setArgument(self::PATH_ARG, $paths);
295
    }
296
297 14
    private function validateOutputDir(InputInterface $input, OutputStyle $io): void
298
    {
299 14
        $outputDir = $input->getOption(self::OUTPUT_DIR_OPT);
300
301 14
        if (false === $this->fileSystem->isAbsolutePath($outputDir)) {
302 3
            $outputDir = getcwd().DIRECTORY_SEPARATOR.$outputDir;
303
        }
304
305 14
        $input->setOption(self::OUTPUT_DIR_OPT, $outputDir);
306
307 14
        if (false === $this->fileSystem->exists($outputDir)) {
308 14
            return;
309
        }
310
311
        if (false === is_writable($outputDir)) {
312
            throw new RuntimeException(
313
                sprintf(
314
                    'Expected "<comment>%s</comment>" to be writeable.',
315
                    $outputDir
316
                )
317
            );
318
        }
319
320
        if ($input->getOption(self::FORCE_OPT)) {
321
            $this->fileSystem->remove($outputDir);
322
323
            return;
324
        }
325
326
        if (false === is_dir($outputDir)) {
327
            $canDeleteFile = $io->confirm(
328
                sprintf(
329
                    'Expected "<comment>%s</comment>" to be a directory but found a file instead. It will be '
330
                    .'removed, do you wish to proceed?',
331
                    $outputDir
332
                ),
333
                false
334
            );
335
336
            if (false === $canDeleteFile) {
337
                return;
338
            }
339
340
            $this->fileSystem->remove($outputDir);
341
        } else {
342
            $canDeleteFile = $io->confirm(
343
                sprintf(
344
                    'The output directory "<comment>%s</comment>" already exists. Continuing will erase its'
345
                    .' content, 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
        }
357
    }
358
359 14
    private function retrieveConfig(InputInterface $input, OutputInterface $output, OutputStyle $io): Configuration
360
    {
361 14
        $prefix = $input->getOption(self::PREFIX_OPT);
362
363 14
        if ($input->getOption(self::NO_CONFIG_OPT)) {
364 10
            $io->writeln(
365 10
                'Loading without configuration file.',
366 10
                OutputInterface::VERBOSITY_DEBUG
367
            );
368
369 10
            $config = Configuration::load();
370
371 10
            if (null !== $prefix) {
372 9
                $config = $config->withPrefix($prefix);
373
            }
374
375 10
            if (null === $config->getPrefix()) {
376 1
                $config = $config->withPrefix($this->generateRandomPrefix());
377
            }
378
379 10
            return $this->retrievePaths($input, $config);
380
        }
381
382 4
        $configFile = $input->getOption(self::CONFIG_FILE_OPT);
383
384 4
        if (null === $configFile) {
385 3
            $configFile = $this->makeAbsolutePath(self::CONFIG_FILE_DEFAULT);
386
387 3
            if (false === file_exists($configFile) && false === $this->init) {
388
                $this->init = true;
389
390
                $initCommand = $this->getApplication()->find('init');
391
392
                $initInput = new StringInput('');
393
                $initInput->setInteractive($input->isInteractive());
394
395
                $initCommand->run($initInput, $output);
396
397
                $io->writeln(
398
                    sprintf(
399
                        'Config file "<comment>%s</comment>" not found. Skipping.',
400
                        $configFile
401
                    ),
402
                    OutputInterface::VERBOSITY_DEBUG
403
                );
404
405
                return self::retrieveConfig($input, $output, $io);
406
            }
407
408 3
            if ($this->init) {
409 3
                $configFile = null;
410
            }
411
        } else {
412 1
            $configFile = $this->makeAbsolutePath($configFile);
413
        }
414
415 4
        if (null === $configFile) {
416
            $io->writeln(
417
                'Loading without configuration file.',
418
                OutputInterface::VERBOSITY_DEBUG
419
            );
420 4
        } elseif (false === file_exists($configFile)) {
421 1
            throw new RuntimeException(
422 1
                sprintf(
423 1
                    'Could not find the configuration file "%s".',
424 1
                    $configFile
425
                )
426
            );
427
        } else {
428 3
            $io->writeln(
429 3
                sprintf(
430 3
                    'Using the configuration file "%s".',
431 3
                    $configFile
432
                ),
433 3
                OutputInterface::VERBOSITY_DEBUG
434
            );
435
        }
436
437 3
        $config = Configuration::load($configFile);
438 2
        $config = $this->retrievePaths($input, $config);
439
440 2
        if (null !== $prefix) {
441 2
            $config = $config->withPrefix($prefix);
442
        }
443
444 2
        if (null === $config->getPrefix()) {
445
            $config = $config->withPrefix($this->generateRandomPrefix());
446
        }
447
448 2
        return $config;
449
    }
450
451 12
    private function retrievePaths(InputInterface $input, Configuration $config): Configuration
452
    {
453
        // Checks if there is any path included and if note use the current working directory as the include path
454 12
        $paths = $input->getArgument(self::PATH_ARG);
455
456 12
        if (0 === count($paths) && 0 === count($config->getFilesWithContents())) {
457 3
            $paths = [getcwd()];
458
        }
459
460 12
        return $config->withPaths($paths);
461
    }
462
463 4
    private function makeAbsolutePath(string $path): string
464
    {
465 4
        if (false === $this->fileSystem->isAbsolutePath($path)) {
466 4
            $path = getcwd().DIRECTORY_SEPARATOR.$path;
467
        }
468
469 4
        return $path;
470
    }
471
472 1
    private function generateRandomPrefix(): string
473
    {
474 1
        return uniqid('_PhpScoper', false);
475
    }
476
}
477