Completed
Push — master ( 31f994...fb8b80 )
by Théo
02:19
created

AddPrefixCommand::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 4
nc 1
nop 2
dl 0
loc 7
ccs 4
cts 4
cp 1
crap 1
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 Humbug\PhpScoper\Console\Configuration;
18
use Humbug\PhpScoper\Handler\HandleAddPrefix;
19
use Humbug\PhpScoper\Logger\ConsoleLogger;
20
use InvalidArgumentException;
21
use Symfony\Component\Console\Command\Command;
22
use Symfony\Component\Console\Exception\RuntimeException;
23
use Symfony\Component\Console\Input\InputArgument;
24
use Symfony\Component\Console\Input\InputInterface;
25
use Symfony\Component\Console\Input\InputOption;
26
use Symfony\Component\Console\Output\OutputInterface;
27
use Symfony\Component\Console\Style\OutputStyle;
28
use Symfony\Component\Console\Style\SymfonyStyle;
29
use Symfony\Component\Filesystem\Filesystem;
30
use Throwable;
31
32
final class AddPrefixCommand extends Command
33
{
34
    private const PATH_ARG = 'paths';
35
    private const PREFIX_OPT = 'prefix';
36
    private const OUTPUT_DIR_OPT = 'output-dir';
37
    private const FORCE_OPT = 'force';
38
    private const STOP_ON_FAILURE_OPT = 'stop-on-failure';
39
    private const CONFIG_FILE_OPT = 'config';
40
    private const CONFIG_FILE_DEFAULT = 'scoper.inc.php';
41
    private const NO_CONFIG_OPT = 'no-config';
42
    private const WORKING_DIR_OPT = 'working-dir';
43
44
    private $fileSystem;
45
    private $handle;
46
47
    /**
48
     * @inheritdoc
49
     */
50 23
    public function __construct(Filesystem $fileSystem, HandleAddPrefix $handle)
51
    {
52 23
        parent::__construct();
53
54 23
        $this->fileSystem = $fileSystem;
55 23
        $this->handle = $handle;
56
    }
57
58
    /**
59
     * @inheritdoc
60
     */
61 23
    protected function configure(): void
62
    {
63
        $this
64 23
            ->setName('add-prefix')
65 23
            ->setDescription('Goes through all the PHP files found in the given paths to apply the given prefix to namespaces & FQNs.')
66 23
            ->addArgument(
67 23
                self::PATH_ARG,
68 23
                InputArgument::IS_ARRAY,
69 23
                'The path(s) to process.'
70
            )
71 23
            ->addOption(
72 23
                self::PREFIX_OPT,
73 23
                'p',
74 23
                InputOption::VALUE_REQUIRED,
75 23
                'The namespace prefix to add.'
76
            )
77 23
            ->addOption(
78 23
                self::OUTPUT_DIR_OPT,
79 23
                'o',
80 23
                InputOption::VALUE_REQUIRED,
81 23
                'The output directory in which the prefixed code will be dumped.',
82 23
                'build'
83
            )
84 23
            ->addOption(
85 23
                self::FORCE_OPT,
86 23
                'f',
87 23
                InputOption::VALUE_NONE,
88 23
                'Deletes any existing content in the output directory without any warning.'
89
            )
90 23
            ->addOption(
91 23
                self::STOP_ON_FAILURE_OPT,
92 23
                's',
93 23
                InputOption::VALUE_NONE,
94 23
                'Stops on failure.'
95
            )
96 23
            ->addOption(
97 23
                self::CONFIG_FILE_OPT,
98 23
                'c',
99 23
                InputOption::VALUE_REQUIRED,
100 23
                sprintf(
101 23
                    'Configuration file. Will use "%s" if found by default.',
102 23
                    self::CONFIG_FILE_DEFAULT
103
                ),
104 23
                null
105
            )
106 23
            ->addOption(
107 23
                self::NO_CONFIG_OPT,
108 23
                null,
109 23
                InputOption::VALUE_NONE,
110 23
                sprintf(
111 23
                    'Do not look for a configuration file.',
112 23
                    self::CONFIG_FILE_DEFAULT
113
                ),
114 23
                null
115
            )
116 23
            ->addOption(
117 23
                self::WORKING_DIR_OPT,
118 23
                'd',
119 23
                InputOption::VALUE_REQUIRED,
120 23
                'If specified, use the given directory as working directory.',
121 23
                null
122
            )
123
        ;
124
    }
125
126
    /**
127
     * @inheritdoc
128
     */
129 21
    protected function execute(InputInterface $input, OutputInterface $output): int
130
    {
131 21
        $io = new SymfonyStyle($input, $output);
132 21
        $io->writeln('');
133
134 21
        $this->changeWorkingDirectory($input);
135
136 21
        $this->validatePrefix($input);
137 16
        $this->validatePaths($input);
138 16
        $this->validateOutputDir($input, $io);
139
140 16
        $config = $this->retrieveConfig($input, $io);
141
142 14
        $logger = new ConsoleLogger(
143 14
            $this->getApplication(),
144 14
            $io
145
        );
146
147 14
        $logger->outputScopingStart(
148 14
            $input->getOption(self::PREFIX_OPT),
149 14
            $input->getArgument(self::PATH_ARG)
150
        );
151
152 14
        $paths = $this->retrievePaths($input, $config);
153
154
        try {
155 14
            $this->handle->__invoke(
156 14
                $input->getOption(self::PREFIX_OPT),
157 14
                $paths,
158 14
                $input->getOption(self::OUTPUT_DIR_OPT),
159 14
                $config->getPatchers(),
160 14
                $config->getWhitelist(),
161 14
                $config->getGlobalNamespaceWhitelisters(),
162 14
                $input->getOption(self::STOP_ON_FAILURE_OPT),
163 14
                $logger
164
            );
165 1
        } catch (Throwable $throwable) {
166 1
            $logger->outputScopingEndWithFailure();
167
168 1
            throw $throwable;
169
        }
170
171 13
        $logger->outputScopingEnd();
172
173 13
        return 0;
174
    }
175
176 21
    private function changeWorkingDirectory(InputInterface $input): void
177
    {
178 21
        $workingDir = $input->getOption(self::WORKING_DIR_OPT);
179
180 21
        if (null !== $workingDir) {
181
            if (false === file_exists($workingDir)) {
182
                throw new InvalidArgumentException(
183
                    sprintf(
184
                        'Could not change the working directory to "%s": directory does not exists.',
185
                        $workingDir
186
                    )
187
                );
188
            }
189
190
            if (false === chdir($workingDir)) {
191
                throw new RuntimeException(
192
                    sprintf(
193
                        'Failed to change the working directory to "%s" from "%s".',
194
                        $workingDir,
195
                        getcwd()
196
                    )
197
                );
198
            }
199
        }
200
    }
201
202 21
    private function validatePrefix(InputInterface $input): void
203
    {
204 21
        $prefix = $input->getOption(self::PREFIX_OPT);
205
206 21
        if (null === $prefix) {
207 1
            $prefix = uniqid('PhpScoper');
208
        } else {
209 20
            $prefix = trim($prefix);
210
        }
211
212 21
        if (1 === preg_match('/(?<prefix>.*?)\\\\*$/', $prefix, $matches)) {
213 21
            $prefix = $matches['prefix'];
214
        }
215
216 21
        if ('' === $prefix) {
217 5
            throw new RuntimeException(
218 5
                sprintf(
219 5
                    'Expected "%s" argument to be a non empty string.',
220 5
                    self::PREFIX_OPT
221
                )
222
            );
223
        }
224
225 16
        $input->setOption(self::PREFIX_OPT, $prefix);
226
    }
227
228 16
    private function validatePaths(InputInterface $input): void
229
    {
230 16
        $cwd = getcwd();
231 16
        $fileSystem = $this->fileSystem;
232
233 16
        $paths = array_map(
234 16
            function (string $path) use ($cwd, $fileSystem) {
235 14
                if (false === $fileSystem->isAbsolutePath($path)) {
236 1
                    return $cwd.DIRECTORY_SEPARATOR.$path;
237
                }
238
239 14
                return $path;
240 16
            },
241 16
            $input->getArgument(self::PATH_ARG)
242
        );
243
244 16
        $input->setArgument(self::PATH_ARG, $paths);
245
    }
246
247 16
    private function validateOutputDir(InputInterface $input, OutputStyle $io): void
248
    {
249 16
        $outputDir = $input->getOption(self::OUTPUT_DIR_OPT);
250
251 16
        if (false === $this->fileSystem->isAbsolutePath($outputDir)) {
252 1
            $outputDir = getcwd().DIRECTORY_SEPARATOR.$outputDir;
253
        }
254
255 16
        $input->setOption(self::OUTPUT_DIR_OPT, $outputDir);
256
257 16
        if (false === $this->fileSystem->exists($outputDir)) {
258 16
            return;
259
        }
260
261
        if (false === is_writable($outputDir)) {
262
            throw new RuntimeException(
263
                sprintf(
264
                    'Expected "<comment>%s</comment>" to be writeable.',
265
                    $outputDir
266
                )
267
            );
268
        }
269
270
        if ($input->getOption(self::FORCE_OPT)) {
271
            $this->fileSystem->remove($outputDir);
272
273
            return;
274
        }
275
276
        if (false === is_dir($outputDir)) {
277
            $canDeleteFile = $io->confirm(
278
                sprintf(
279
                    'Expected "<comment>%s</comment>" to be a directory but found a file instead. It will be '
280
                    .'removed, do you wish to proceed?',
281
                    $outputDir
282
                ),
283
                false
284
            );
285
286
            if (false === $canDeleteFile) {
287
                return;
288
            }
289
290
            $this->fileSystem->remove($outputDir);
291
        } else {
292
            $canDeleteFile = $io->confirm(
293
                sprintf(
294
                    'The output directory "<comment>%s</comment>" already exists. Continuing will erase its'
295
                    .' content, do you wish to proceed?',
296
                    $outputDir
297
                ),
298
                false
299
            );
300
301
            if (false === $canDeleteFile) {
302
                return;
303
            }
304
305
            $this->fileSystem->remove($outputDir);
306
        }
307
    }
308
309 16
    private function retrieveConfig(InputInterface $input, OutputStyle $io): Configuration
310
    {
311 16
        if ($input->getOption(self::NO_CONFIG_OPT)) {
312
            $io->writeln(
313
                'Loading without configuration file.',
314
                OutputStyle::VERBOSITY_DEBUG
315
            );
316
317
            return Configuration::load(null);
318
        }
319
320 16
        $configFile = $input->getOption(self::CONFIG_FILE_OPT);
321
322 16
        if (null === $configFile) {
323 15
            $configFile = $this->makeAbsolutePath(self::CONFIG_FILE_DEFAULT);
324
325 15
            if (false === file_exists($configFile)) {
326 12
                $io->writeln(
327 12
                    sprintf(
328 12
                        'Config file "<comment>%s</comment>" not found. Skipping.',
329 12
                        $configFile
330
                    ),
331 12
                    OutputStyle::VERBOSITY_DEBUG
332
                );
333
334 12
                return Configuration::load(null);
335
            }
336
        } else {
337 1
            $configFile = $this->makeAbsolutePath($configFile);
338
        }
339
340 4
        if (false === file_exists($configFile)) {
341 1
            throw new RuntimeException(
342 1
                sprintf(
343 1
                    'Could not find the file "<comment>%s</comment>".',
344 1
                    $configFile
345
                )
346
            );
347
        }
348
349 3
        $io->writeln(
350 3
            sprintf(
351 3
                'Using the configuration file "<comment>%s</comment>".',
352 3
                $configFile
353
            ),
354 3
            OutputStyle::VERBOSITY_DEBUG
355
        );
356
357 3
        return Configuration::load($configFile);
358
    }
359
360
    /**
361
     * @param InputInterface $input
362
     * @param Configuration  $configuration
363
     *
364
     * @return string[] List of absolute paths
365
     */
366 14
    private function retrievePaths(InputInterface $input, Configuration $configuration): array
367
    {
368 14
        $paths = $input->getArgument(self::PATH_ARG);
369
370 14
        $finders = $configuration->getFinders();
371
372 14
        foreach ($finders as $finder) {
373 1
            foreach ($finder as $file) {
374 1
                $paths[] = $file->getRealPath();
375
            }
376
        }
377
378 14
        if (0 === count($paths)) {
379 2
            return [getcwd()];
380
        }
381
382 12
        return array_unique($paths);
383
    }
384
385 16
    private function makeAbsolutePath(string $path): string
386
    {
387 16
        if (false === $this->fileSystem->isAbsolutePath($path)) {
388 13
            $path = getcwd().DIRECTORY_SEPARATOR.$path;
389
        }
390
391 16
        return $path;
392
    }
393
}
394