Completed
Pull Request — master (#18)
by Jitendra
01:59
created

InitCommand   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 280
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 142
dl 0
loc 280
rs 8.8798
c 0
b 0
f 0
wmc 44

13 Methods

Rating   Name   Duplication   Size   Complexity  
A makeKeywords() 0 13 3
A __construct() 0 36 1
A getCachePath() 0 17 5
A generate() 0 11 1
A promptPackages() 0 13 3
A prepareProjectPath() 0 24 5
A makeNamespace() 0 13 2
A execute() 0 24 4
A loadConfig() 0 20 5
A validatePackage() 0 11 3
A interact() 0 15 2
A collectPackages() 0 13 5
A collectMissing() 0 24 5

How to fix   Complexity   

Complex Class

Complex classes like InitCommand often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use InitCommand, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Ahc\Phint\Console;
4
5
use Ahc\Cli\Input\Command;
6
use Ahc\Cli\IO\Interactor;
7
use Ahc\Phint\Generator\CollisionHandler;
8
use Ahc\Phint\Generator\TwigGenerator;
9
use Ahc\Phint\Util\Composer;
10
use Ahc\Phint\Util\Git;
11
use Ahc\Phint\Util\Inflector;
12
use Ahc\Phint\Util\Path;
13
14
class InitCommand extends Command
15
{
16
    /** @var Git */
17
    protected $_git;
18
19
    /** @var Composer */
20
    protected $_composer;
21
22
    /**
23
     * Configure the command options/arguments.
24
     *
25
     * @return void
26
     */
27
    public function __construct()
28
    {
29
        parent::__construct('init', 'Create and Scaffold a bare new PHP project');
30
31
        $this->_git      = new Git;
32
        $this->_composer = new Composer;
33
34
        $this
35
            ->argument('<project>', 'The project name without slashes')
36
            ->option('-T --type', 'Project type', null, 'library')
37
            ->option('-n --name', 'Vendor full name', null, $this->_git->getConfig('user.name'))
38
            ->option('-e --email', 'Vendor email', null, $this->_git->getConfig('user.email'))
39
            ->option('-u --username', 'Vendor handle/username')
40
            ->option('-N --namespace', 'Root namespace (use `/` separator)')
41
            ->option('-w --keywords [words...]', 'Project Keywords (`php`, `<project>` auto added)')
42
            ->option('-P --php', 'Minimum PHP version', 'floatval')
43
            ->option('-p --path', 'The project path (Auto resolved)')
44
            ->option('-f --force', 'Run even if the project exists', null, false)
45
            ->option('-d --descr', 'Project description')
46
            ->option('-y --year', 'License Year', null, date('Y'))
47
            ->option('-z --using', 'Reference package')
48
            ->option('-C --config', 'JSON filepath to read config from')
49
            ->option('-R --req [pkgs...]', 'Required packages')
50
            ->option('-D --dev [pkgs...]', 'Developer packages')
51
            ->option('-t --no-travis', 'Disable travis')
52
            ->option('-c --no-codecov', 'Disable codecov')
53
            ->option('-s --no-scrutinizer', 'Disable scrutinizer')
54
            ->option('-l --no-styleci', 'Disable StyleCI')
55
            ->option('-L --license', 'License')
56
            ->usage($this->writer()->colorizer()->colors(''
57
                . '<bold>  phint init</end> <line><project></end> '
58
                . '<comment>--force --descr "Awesome project" --name "YourName" --email [email protected]</end><eol/>'
59
                . '<bold>  phint init</end> <line><project></end> '
60
                . '<comment>--using laravel/lumen --namespace Project/Api --type project</end><eol/>'
61
                . '<bold>  phint init</end> <line><project></end> '
62
                . '<comment>--php 7.0 --config /path/to/json --dev mockery/mockery --req adhocore/cli</end><eol/>'
63
            ));
64
    }
65
66
    /**
67
     * Execute the command action.
68
     *
69
     * @return void
70
     */
71
    public function execute()
72
    {
73
        $io = $this->app()->io();
74
75
        if ($using = $this->using) {
0 ignored issues
show
Bug Best Practice introduced by
The property using does not exist on Ahc\Phint\Console\InitCommand. Since you implemented __get, consider adding a @property annotation.
Loading history...
76
            $io->colors("Using <cyanBold>$using</end> to create project <comment>(takes some time)</end><eol/>");
77
78
            $this->_composer->createProject($this->path, $this->using);
0 ignored issues
show
Bug Best Practice introduced by
The property path does not exist on Ahc\Phint\Console\InitCommand. Since you implemented __get, consider adding a @property annotation.
Loading history...
79
        }
80
81
        $io->comment('Generating files ...', true);
82
        $this->generate($this->path, $this->values());
0 ignored issues
show
Bug introduced by
It seems like $this->path can also be of type null; however, parameter $projectPath of Ahc\Phint\Console\InitCommand::generate() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

82
        $this->generate(/** @scrutinizer ignore-type */ $this->path, $this->values());
Loading history...
83
84
        $io->colors('Setting up <cyanBold>git</end><eol/>');
85
        $this->_git->withWorkDir($this->path)->init()->addRemote($this->username, $this->project);
0 ignored issues
show
Bug Best Practice introduced by
The property project does not exist on Ahc\Phint\Console\InitCommand. Since you implemented __get, consider adding a @property annotation.
Loading history...
Bug Best Practice introduced by
The property username does not exist on Ahc\Phint\Console\InitCommand. Since you implemented __get, consider adding a @property annotation.
Loading history...
86
87
        $io->colors('Setting up <cyanBold>composer</end> <comment>(takes some time)</end><eol>');
88
        if ($using) {
89
            $status = $this->_composer->withWorkDir($this->path)->update();
90
        } else {
91
            $status = $this->_composer->withWorkDir($this->path)->install();
92
        }
93
94
        $status === false ? $io->error('Composer setup failed', true) : $io->ok('Done', true);
0 ignored issues
show
introduced by
The condition $status === false is always false.
Loading history...
95
    }
96
97
    public function interact(Interactor $io)
98
    {
99
        $project = $this->project;
0 ignored issues
show
Bug Best Practice introduced by
The property project does not exist on Ahc\Phint\Console\InitCommand. Since you implemented __get, consider adding a @property annotation.
Loading history...
100
101
        if (!\preg_match('/[a-z0-9_-]/i', $project)) {
102
            throw new \InvalidArgumentException('Project argument should only contain [a-z0-9_-]');
103
        }
104
105
        $io->okBold('Phint Setup', true);
106
107
        $this->set('path', $path = $this->prepareProjectPath());
108
        $this->loadConfig($this->config);
0 ignored issues
show
Bug Best Practice introduced by
The property config does not exist on Ahc\Phint\Console\InitCommand. Since you implemented __get, consider adding a @property annotation.
Loading history...
109
110
        $this->collectMissing($io);
111
        $this->collectPackages($io);
112
    }
113
114
    protected function prepareProjectPath(): string
115
    {
116
        $path = $this->project;
0 ignored issues
show
Bug Best Practice introduced by
The property project does not exist on Ahc\Phint\Console\InitCommand. Since you implemented __get, consider adding a @property annotation.
Loading history...
117
        $io   = $this->app()->io();
118
119
        if (!(new Path)->isAbsolute($path)) {
0 ignored issues
show
Bug introduced by
It seems like $path can also be of type null; however, parameter $path of Ahc\Phint\Util\Path::isAbsolute() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

119
        if (!(new Path)->isAbsolute(/** @scrutinizer ignore-type */ $path)) {
Loading history...
120
            $path = \getcwd() . '/' . $path;
121
        }
122
123
        if (\is_dir($path)) {
124
            if (!$this->force) {
0 ignored issues
show
Bug Best Practice introduced by
The property force does not exist on Ahc\Phint\Console\InitCommand. Since you implemented __get, consider adding a @property annotation.
Loading history...
125
                throw new \InvalidArgumentException(
126
                    \sprintf('Something with the name "%s" already exists!', \basename($path))
127
                );
128
            }
129
130
            if (!$this->using) {
0 ignored issues
show
Bug Best Practice introduced by
The property using does not exist on Ahc\Phint\Console\InitCommand. Since you implemented __get, consider adding a @property annotation.
Loading history...
131
                $io->error('You have set force flag, existing files will be overwritten', true);
132
            }
133
        } else {
134
            \mkdir($path, 0777, true);
135
        }
136
137
        return $path;
138
    }
139
140
    protected function loadConfig(string $path = null)
141
    {
142
        if (empty($path)) {
143
            return;
144
        }
145
146
        $pathUtil = new Path;
147
148
        if (!$pathUtil->isAbsolute($path)) {
149
            $path = \getcwd() . '/' . $path;
150
        }
151
152
        if (!\is_file($path)) {
153
            $this->app()->io()->error('Invalid path specified for config');
154
155
            return;
156
        }
157
158
        foreach ($pathUtil->readAsJson($path) as $key => $value) {
159
            $this->$key ?? $this->set($key, $value);
160
        }
161
    }
162
163
    protected function collectMissing(Interactor $io)
164
    {
165
        $setup = [
166
            'type'     => ['choices' => ['project', 'library', 'composer-plugin']],
167
            'license'  => ['choices' => ['m' => 'MIT', 'g' => 'GNU LGPL', 'a' => 'Apache 2', 'b' => 'BSD Simplified']],
168
            'php'      => ['choices' => ['5.4', '5.5', '5.6', '7.0', '7.1', '7.2']],
169
            'using'    => ['prompt' => 0, 'extra' => ' (ENTER to skip)'],
170
            'keywords' => ['prompt' => 0, 'extra' => ' (ENTER to skip)'],
171
        ];
172
173
        foreach ($this->userOptions() as $name => $option) {
174
            $default = $option->default();
175
            if ($this->$name !== null || \in_array($name, ['req', 'dev', 'config'])) {
176
                continue;
177
            }
178
179
            $set = $setup[$name] ?? [];
180
            if ($set['choices'] ?? null) {
181
                $value = $io->choice($option->desc(), $set['choices'], $default);
182
            } else {
183
                $value = $io->prompt($option->desc() . ($set['extra'] ?? ''), $default, null, $set['prompt'] ?? 1);
184
            }
185
186
            $this->set($name, $value);
187
        }
188
    }
189
190
    protected function collectPackages(Interactor $io)
0 ignored issues
show
Unused Code introduced by
The parameter $io is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

190
    protected function collectPackages(/** @scrutinizer ignore-unused */ Interactor $io)

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

Loading history...
191
    {
192
        $io = $this->app()->io();
193
194
        foreach (['req' => 'Required', 'dev' => 'Developer'] as $key => $label) {
195
            $pkgs = $this->$key ?: $this->promptPackages($label, $io);
196
197
            foreach ($pkgs as &$pkg) {
198
                $pkg = \strpos($pkg, ':') === false ? "{$pkg}:@stable" : $pkg;
199
                $pkg = \array_combine(['name', 'version'], \explode(':', $pkg, 2));
200
            }
201
202
            $this->set($key, $pkgs);
203
        }
204
    }
205
206
    public function promptPackages(string $label, Interactor $io): array
207
    {
208
        $pkgs = [];
209
210
        do {
211
            if (!$pkg = $io->prompt($label . ' package (ENTER to skip)', null, [$this, 'validatePackage'], 0)) {
212
                break;
213
            }
214
215
            $pkgs[] = $pkg;
216
        } while (true);
217
218
        return $pkgs;
219
    }
220
221
    public function validatePackage(string $pkg): string
222
    {
223
        $pkg = \trim($pkg);
224
225
        if ($pkg && \strpos($pkg, '/') === false) {
226
            throw new \InvalidArgumentException(
227
                'Package name format should be vendor/package:version (version can be omitted)'
228
            );
229
        }
230
231
        return $pkg;
232
    }
233
234
    protected function generate(string $projectPath, array $parameters)
235
    {
236
        $templatePath = __DIR__ . '/../../resources';
237
        $generator    = new TwigGenerator($templatePath, $this->getCachePath());
238
239
        // Normalize license (default MIT)
240
        $parameters['license']   = \strtolower($parameters['license'][0] ?? 'm');
241
        $parameters['namespace'] = $this->makeNamespace($parameters['namespace']);
242
        $parameters['keywords']  = $this->makeKeywords($parameters['keywords']);
243
244
        $generator->generate($projectPath, $parameters, new CollisionHandler);
245
    }
246
247
    protected function getCachePath(): string
248
    {
249
        if (!\Phar::running(false)) {
250
            return __DIR__ . '/../../.cache';
251
        }
252
253
        if (false !== $home = ($_SERVER['HOME'] ?? \getenv('HOME'))) {
254
            $path = $home . '/.phint';
255
256
            if (!\is_dir($path)) {
257
                return @\mkdir($path, 0777) ? $path : '';
258
            }
259
260
            return $path;
261
        }
262
263
        return '';
264
    }
265
266
    protected function makeNamespace(string $value): string
267
    {
268
        $in = new Inflector;
269
270
        $project = $this->project;
0 ignored issues
show
Bug Best Practice introduced by
The property project does not exist on Ahc\Phint\Console\InitCommand. Since you implemented __get, consider adding a @property annotation.
Loading history...
271
        $value   = $in->stuldyCase(\str_replace([' ', '/'], '\\', $value));
272
        $project = $in->stuldyCase(\str_replace([' ', '/', '\\'], '-', $project));
273
274
        if (\stripos($value, $project) === false) {
275
            $value .= '\\' . $project;
276
        }
277
278
        return $value;
279
    }
280
281
    protected function makeKeywords($value): array
282
    {
283
        $default = ['php', $this->project];
0 ignored issues
show
Bug Best Practice introduced by
The property project does not exist on Ahc\Phint\Console\InitCommand. Since you implemented __get, consider adding a @property annotation.
Loading history...
284
285
        if (empty($value)) {
286
            return $default;
287
        }
288
289
        if (\is_string($value)) {
290
            $value = \array_map('trim', \explode(',', $value));
291
        }
292
293
        return \array_values(\array_unique(\array_merge($default, $value)));
294
    }
295
}
296