Test Failed
Push — master ( 47f3fa...944618 )
by Théo
05:45 queued 03:14
created

AddPrefixCommand::validatePrefix()   B

Complexity

Conditions 4
Paths 8

Size

Total Lines 25
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
eloc 14
nc 8
nop 1
dl 0
loc 25
ccs 13
cts 13
cp 1
crap 4
rs 8.5806
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
    /** @internal */
34
    const PATH_ARG = 'paths';
35
    /** @internal */
36
    const PREFIX_OPT = 'prefix';
37
    /** @internal */
38
    const OUTPUT_DIR_OPT = 'output-dir';
39
    /** @internal */
40
    const FORCE_OPT = 'force';
41
    /** @internal */
42
    const STOP_ON_FAILURE_OPT = 'stop-on-failure';
43
    /** @internal */
44
    const CONFIG_FILE_OPT = 'config';
45
    /** @internal */
46
    const CONFIG_FILE_DEFAULT = 'scoper.inc.php';
47
    /** @internal */
48
    const WORKING_DIR_OPT = 'working-dir';
49
50
    private $fileSystem;
51
    private $handle;
52
53
    /**
54
     * @inheritdoc
55
     */
56 22
    public function __construct(Filesystem $fileSystem, HandleAddPrefix $handle)
57
    {
58 22
        parent::__construct();
59
60 22
        $this->fileSystem = $fileSystem;
61 22
        $this->handle = $handle;
62
    }
63
64
    /**
65
     * @inheritdoc
66
     */
67 22
    protected function configure()
68
    {
69
        $this
70 22
            ->setName('add-prefix')
71 22
            ->setDescription('Goes through all the PHP files found in the given paths to apply the given prefix to namespaces & FQNs.')
72 22
            ->addArgument(
73 22
                self::PATH_ARG,
74 22
                InputArgument::IS_ARRAY,
75 22
                'The path(s) to process.'
76
            )
77 22
            ->addOption(
78 22
                self::PREFIX_OPT,
79 22
                'p',
80 22
                InputOption::VALUE_REQUIRED,
81 22
                'The namespace prefix to add.'
82
            )
83 22
            ->addOption(
84 22
                self::OUTPUT_DIR_OPT,
85 22
                'o',
86 22
                InputOption::VALUE_REQUIRED,
87 22
                'The output directory in which the prefixed code will be dumped.',
88 22
                'build'
89
            )
90 22
            ->addOption(
91 22
                self::FORCE_OPT,
92 22
                'f',
93 22
                InputOption::VALUE_NONE,
94 22
                'Deletes any existing content in the output directory without any warning.'
95
            )
96 22
            ->addOption(
97 22
                self::STOP_ON_FAILURE_OPT,
98 22
                's',
99 22
                InputOption::VALUE_NONE,
100 22
                'Stops on failure.'
101
            )
102 22
            ->addOption(
103 22
                self::CONFIG_FILE_OPT,
104 22
                'c',
105 22
                InputOption::VALUE_REQUIRED,
106 22
                sprintf(
107 22
                    'Configuration file. Will use "%s" if found by default',
108 22
                    self::CONFIG_FILE_DEFAULT
109
                ),
110 22
                null
111
            )
112 22
            ->addOption(
113 22
                self::WORKING_DIR_OPT,
114 22
                'd',
115 22
                InputOption::VALUE_REQUIRED,
116 22
                'If specified, use the given directory as working directory.',
117 22
                null
118
            )
119
        ;
120
    }
121
122
    /**
123
     * @inheritdoc
124
     */
125 20
    protected function execute(InputInterface $input, OutputInterface $output)
126
    {
127 20
        $io = new SymfonyStyle($input, $output);
128
129 20
        $workingDir = $input->getOption(self::WORKING_DIR_OPT);
130
131 20
        if (null !== $workingDir) {
132
            chdir($workingDir);
133
        }
134
135 20
        $this->validatePrefix($input);
136 15
        $this->validatePaths($input);
137 15
        $this->validateOutputDir($input, $io);
138
139 15
        $config = $this->retrieveConfig($input, $io);
140
141 13
        $logger = new ConsoleLogger(
142 13
            $this->getApplication(),
143 13
            $io
144
        );
145
146 13
        $logger->outputScopingStart(
147 13
            $input->getOption(self::PREFIX_OPT),
148 13
            $input->getArgument(self::PATH_ARG)
149
        );
150
151 13
        $paths = $this->retrievePaths($input, $config);
152
153
        try {
154 13
            $this->handle->__invoke(
155 13
                $input->getOption(self::PREFIX_OPT),
156 13
                $paths,
157 13
                $input->getOption(self::OUTPUT_DIR_OPT),
158 13
                $config->getPatchers(),
159 13
                $config->getWhitelist(),
160 13
                $config->getGlobalNamespaceWhitelisters(),
161 13
                $input->getOption(self::STOP_ON_FAILURE_OPT),
162 13
                $logger
163
            );
164 1
        } catch (Throwable $throwable) {
165 1
            $logger->outputScopingEndWithFailure();
166
167 1
            throw $throwable;
168
        }
169
170 12
        $logger->outputScopingEnd();
171
    }
172
173 20
    private function validatePrefix(InputInterface $input)
174
    {
175 20
        $prefix = $input->getOption(self::PREFIX_OPT);
176
177 20
        if (null === $prefix) {
178 1
            $prefix = uniqid('PhpScoper');
179
        } else {
180 19
            $prefix = trim($prefix);
181
        }
182
183 20
        if (1 === preg_match('/(?<prefix>.*?)\\\\*$/', $prefix, $matches)) {
184 20
            $prefix = $matches['prefix'];
185
        }
186
187 20
        if ('' === $prefix) {
188 5
            throw new RuntimeException(
189 5
                sprintf(
190 5
                    'Expected "%s" argument to be a non empty string.',
191 5
                    self::PREFIX_OPT
192
                )
193
            );
194
        }
195
196 15
        $input->setOption(self::PREFIX_OPT, $prefix);
197
    }
198
199 15
    private function validatePaths(InputInterface $input)
200
    {
201 15
        $cwd = getcwd();
202 15
        $fileSystem = $this->fileSystem;
203
204 15
        $paths = array_map(
205 15
            function (string $path) use ($cwd, $fileSystem) {
206 13
                if (false === $fileSystem->isAbsolutePath($path)) {
207 1
                    return $cwd.DIRECTORY_SEPARATOR.$path;
208
                }
209
210 13
                return $path;
211 15
            },
212 15
            $input->getArgument(self::PATH_ARG)
213
        );
214
215 15
        $input->setArgument(self::PATH_ARG, $paths);
216
    }
217
218 15
    private function validateOutputDir(InputInterface $input, OutputStyle $io)
219
    {
220 15
        $outputDir = $input->getOption(self::OUTPUT_DIR_OPT);
221
222 15 View Code Duplication
        if (false === $this->fileSystem->isAbsolutePath($outputDir)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
223 1
            $outputDir = getcwd().DIRECTORY_SEPARATOR.$outputDir;
224
        }
225
226 15
        $input->setOption(self::OUTPUT_DIR_OPT, $outputDir);
227
228 15
        if (false === $this->fileSystem->exists($outputDir)) {
229 15
            return;
230
        }
231
232
        if (false === is_writable($outputDir)) {
233
            throw new RuntimeException(
234
                sprintf(
235
                    'Expected "<comment>%s</comment>" to be writeable.',
236
                    $outputDir
237
                )
238
            );
239
        }
240
241
        if ($input->getOption(self::FORCE_OPT)) {
242
            $this->fileSystem->remove($outputDir);
243
244
            return;
245
        }
246
247
        if (false === is_dir($outputDir)) {
248
            $canDeleteFile = $io->confirm(
249
                sprintf(
250
                    'Expected "<comment>%s</comment>" to be a directory but found a file instead. It will be '
251
                    .'removed, do you wish to proceed?',
252
                    $outputDir
253
                ),
254
                false
255
            );
256
257
            if (false === $canDeleteFile) {
258
                return;
259
            }
260
261
            $this->fileSystem->remove($outputDir);
262
        } else {
263
            $canDeleteFile = $io->confirm(
264
                sprintf(
265
                    'The output directory "<comment>%s</comment>" already exists. Continuing will erase its'
266
                    .' content, do you wish to proceed?',
267
                    $outputDir
268
                ),
269
                false
270
            );
271
272
            if (false === $canDeleteFile) {
273
                return;
274
            }
275
276
            $this->fileSystem->remove($outputDir);
277
        }
278
    }
279
280 15
    private function retrieveConfig(InputInterface $input, OutputStyle $io): Configuration
281
    {
282 15
        $configFile = $input->getOption(self::CONFIG_FILE_OPT);
283
284 15
        if (null === $configFile) {
285 14
            $configFile = $this->makeAbsolutePath(self::CONFIG_FILE_DEFAULT);
286
287 14
            if (false === file_exists($configFile)) {
288 12
                $io->writeln(
289 12
                    sprintf(
290 12
                        'Config file "%s" not found. Skipping.',
291 12
                        $configFile
292
                    ),
293 12
                    OutputStyle::VERBOSITY_DEBUG
294
                );
295
296 12
                return Configuration::load(null);
297
            }
298
        } else {
299 1
            $configFile = $this->makeAbsolutePath($configFile);
300
        }
301
302 3
        if (false === file_exists($configFile)) {
303 1
            throw new RuntimeException(
304 1
                sprintf(
305 1
                    'Could not find the file "%s".',
306 1
                    $configFile
307
                )
308
            );
309
        }
310
311 2
        $io->writeln(
312 2
            sprintf(
313 2
                'Using the configuration file "%s".',
314 2
                $configFile
315
            ),
316 2
            OutputStyle::VERBOSITY_DEBUG
317
        );
318
319 2
        return Configuration::load($configFile);
320
    }
321
322
    /**
323
     * @param InputInterface $input
324
     * @param Configuration  $configuration
325
     *
326
     * @return string[] List of absolute paths
327
     */
328 13
    private function retrievePaths(InputInterface $input, Configuration $configuration): array
329
    {
330 13
        $paths = $input->getArgument(self::PATH_ARG);
331
332 13
        $finders = $configuration->getFinders();
333
334 13
        foreach ($finders as $finder) {
335
            foreach ($finder as $file) {
336
                $paths[] = $file->getRealPath();
337
            }
338
        }
339
340 13
        if (0 === count($paths)) {
341 2
            return [getcwd()];
342
        }
343
344 11
        return array_unique($paths);
345
    }
346
347 15
    private function makeAbsolutePath(string $path): string
348
    {
349 15 View Code Duplication
        if (false === $this->fileSystem->isAbsolutePath($path)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
350 12
            $path = getcwd().DIRECTORY_SEPARATOR.$path;
351
        }
352
353 15
        return $path;
354
    }
355
}
356