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