Completed
Push — master ( 32a903...cb8294 )
by Théo
02:52 queued 22s
created

AddPrefixCommand::validatePaths()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 18
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 2.003

Importance

Changes 0
Metric Value
cc 2
eloc 10
nc 1
nop 1
dl 0
loc 18
ccs 10
cts 11
cp 0.9091
crap 2.003
rs 9.4285
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 Closure;
18
use Humbug\PhpScoper\Autoload\ScoperAutoloadGenerator;
19
use Humbug\PhpScoper\Console\Configuration;
20
use Humbug\PhpScoper\Logger\ConsoleLogger;
21
use Humbug\PhpScoper\Scoper;
22
use Humbug\PhpScoper\Throwable\Exception\ParsingException;
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
49
    /**
50
     * @inheritdoc
51
     */
52 21
    public function __construct(Filesystem $fileSystem, Scoper $scoper)
53
    {
54 21
        parent::__construct();
55
56 21
        $this->fileSystem = $fileSystem;
57 21
        $this->scoper = $scoper;
58
    }
59
60
    /**
61
     * @inheritdoc
62
     */
63 21
    protected function configure(): void
64
    {
65 21
        parent::configure();
66
67
        $this
68 21
            ->setName('add-prefix')
69 21
            ->setDescription('Goes through all the PHP files found in the given paths to apply the given prefix to namespaces & FQNs.')
70 21
            ->addArgument(
71 21
                self::PATH_ARG,
72 21
                InputArgument::IS_ARRAY,
73 21
                'The path(s) to process.'
74
            )
75 21
            ->addOption(
76 21
                self::PREFIX_OPT,
77 21
                'p',
78 21
                InputOption::VALUE_REQUIRED,
79 21
                'The namespace prefix to add.'
80
            )
81 21
            ->addOption(
82 21
                self::OUTPUT_DIR_OPT,
83 21
                'o',
84 21
                InputOption::VALUE_REQUIRED,
85 21
                'The output directory in which the prefixed code will be dumped.',
86 21
                'build'
87
            )
88 21
            ->addOption(
89 21
                self::FORCE_OPT,
90 21
                'f',
91 21
                InputOption::VALUE_NONE,
92 21
                'Deletes any existing content in the output directory without any warning.'
93
            )
94 21
            ->addOption(
95 21
                self::STOP_ON_FAILURE_OPT,
96 21
                's',
97 21
                InputOption::VALUE_NONE,
98 21
                'Stops on failure.'
99
            )
100 21
            ->addOption(
101 21
                self::CONFIG_FILE_OPT,
102 21
                'c',
103 21
                InputOption::VALUE_REQUIRED,
104 21
                sprintf(
105 21
                    'Configuration file. Will use "%s" if found by default.',
106 21
                    self::CONFIG_FILE_DEFAULT
107
                )
108
            )
109 21
            ->addOption(
110 21
                self::NO_CONFIG_OPT,
111 21
                null,
112 21
                InputOption::VALUE_NONE,
113 21
            'Do not look for a configuration file.'
114
            )
115
        ;
116
    }
117
118
    /**
119
     * @inheritdoc
120
     */
121 19
    protected function execute(InputInterface $input, OutputInterface $output): int
122
    {
123 19
        $io = new SymfonyStyle($input, $output);
124 19
        $io->writeln('');
125
126 19
        $this->changeWorkingDirectory($input);
127
128 19
        $this->validatePrefix($input);
129 14
        $this->validatePaths($input);
130 14
        $this->validateOutputDir($input, $io);
131
132 14
        $config = $this->retrieveConfig($input, $output, $io);
133 12
        $output = $input->getOption(self::OUTPUT_DIR_OPT);
134
135 12
        $logger = new ConsoleLogger(
136 12
            $this->getApplication(),
137 12
            $io
138
        );
139
140 12
        $logger->outputScopingStart(
141 12
            $input->getOption(self::PREFIX_OPT),
142 12
            $input->getArgument(self::PATH_ARG)
143
        );
144
145
        try {
146 12
            $this->scopeFiles(
147 12
                $input->getOption(self::PREFIX_OPT),
148 12
                $config->getFilesWithContents(),
149 12
                $output,
150 12
                $config->getPatchers(),
151 12
                $config->getWhitelist(),
152 12
                $config->getGlobalNamespaceWhitelister(),
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 12
    private function scopeFiles(
170
        string $prefix,
171
        array $filesWithContents,
172
        string $output,
173
        array $patchers,
174
        array $whitelist,
175
        Closure $globalNamespaceWhitelister,
176
        bool $stopOnFailure,
177
        ConsoleLogger $logger
178
    ): void {
179
        // Creates output directory if does not already exist
180 12
        $this->fileSystem->mkdir($output);
181
182 12
        $logger->outputFileCount(count($filesWithContents));
183
184 12
        $vendorDirs = [];
185 12
        $commonPath = get_common_path(array_keys($filesWithContents));
186
187 12
        foreach ($filesWithContents as $fileWithContents) {
188 12
            [$inputFilePath, $inputContents] = $fileWithContents;
0 ignored issues
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...
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...
189
190 12
            $outputFilePath = $output.str_replace($commonPath, '', $inputFilePath);
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,
199 12
                $outputFilePath,
200 12
                $prefix,
201 12
                $patchers,
202 12
                $whitelist,
203 12
                $globalNamespaceWhitelister,
204 12
                $stopOnFailure,
205 12
                $logger
206
            );
207
        }
208
209 12
        $vendorDirs = array_keys($vendorDirs);
210
211 12
        usort(
212 12
            $vendorDirs,
213 12
            function ($a, $b) {
214
                return strlen($b) <=> strlen($a);
215 12
            }
216
        );
217
218 12
        $vendorDir = (0 === count($vendorDirs)) ? null : $vendorDirs[0];
219
220 12
        if (null !== $vendorDir) {
221
            $autoload = (new ScoperAutoloadGenerator($whitelist))->dump($prefix);
222
223
            $this->fileSystem->dumpFile($vendorDir.'/scoper-autoload.php', $autoload);
224
        }
225
    }
226
227
    /**
228
     * @param string        $inputFilePath
229
     * @param string        $outputFilePath
230
     * @param string        $inputContents
231
     * @param string        $prefix
232
     * @param callable[]    $patchers
233
     * @param string[]      $whitelist
234
     * @param callable      $globalWhitelister
235
     * @param bool          $stopOnFailure
236
     * @param ConsoleLogger $logger
237
     */
238 12
    private function scopeFile(
239
        string $inputFilePath,
240
        string $inputContents,
0 ignored issues
show
Unused Code introduced by
The parameter $inputContents is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

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