Completed
Push — master ( 70f2f0...ee672a )
by Théo
09:52
created

AddPrefixCommand::execute()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 49
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 29
CRAP Score 3.0003

Importance

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