Completed
Push — master ( 920b19...26cf4b )
by Jitendra
01:47
created

InitCommand   A

Complexity

Total Complexity 41

Size/Duplication

Total Lines 272
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 138
dl 0
loc 272
rs 9.1199
c 0
b 0
f 0
wmc 41

13 Methods

Rating   Name   Duplication   Size   Complexity  
A makeKeywords() 0 13 3
A __construct() 0 37 1
A getCachePath() 0 7 2
A generate() 0 11 1
A promptPackages() 0 13 3
A prepareProjectPath() 0 24 5
A makeNamespace() 0 13 2
A execute() 0 26 4
A loadConfig() 0 18 5
A validatePackage() 0 11 3
A interact() 0 15 2
A collectPackages() 0 11 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\ExceptionInvalidArgumentException;
0 ignored issues
show
Bug introduced by
The type Ahc\Cli\ExceptionInvalidArgumentException was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

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

87
        $this->generate(/** @scrutinizer ignore-type */ $this->path, $this->values());
Loading history...
88
89
        $io->colors('Setting up <cyanBold>git</end><eol/>');
90
        $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...
91
92
        $io->colors('Setting up <cyanBold>composer</end> <comment>(takes some time)</end><eol>');
93
        if ($using) {
94
            $this->_composer->withWorkDir($this->path)->update();
95
        } else {
96
            $this->_composer->withWorkDir($this->path)->install();
97
        }
98
99
        $success = $this->_composer->successful();
100
101
        $success ? $io->ok('Done', true) : $io->error('Composer setup failed', true);
102
    }
103
104
    public function interact(Interactor $io)
105
    {
106
        $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...
107
108
        if (!\preg_match('/[a-z0-9_-]/i', $project)) {
109
            throw new InvalidArgumentException('Project argument should only contain [a-z0-9_-]');
0 ignored issues
show
Bug introduced by
The type Ahc\Phint\Console\InvalidArgumentException was not found. Did you mean InvalidArgumentException? If so, make sure to prefix the type with \.
Loading history...
110
        }
111
112
        $io->okBold('Phint Setup', true);
113
114
        $this->set('path', $path = $this->prepareProjectPath());
115
        $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...
116
117
        $this->collectMissing($io);
118
        $this->collectPackages($io);
119
    }
120
121
    protected function prepareProjectPath(): string
122
    {
123
        $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...
124
        $io   = $this->app()->io();
125
126
        if (!$this->_pathUtil->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

126
        if (!$this->_pathUtil->isAbsolute(/** @scrutinizer ignore-type */ $path)) {
Loading history...
127
            $path = \getcwd() . '/' . $path;
128
        }
129
130
        if (\is_dir($path)) {
131
            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...
132
                throw new InvalidArgumentException(
133
                    \sprintf('Something with the name "%s" already exists!', \basename($path))
134
                );
135
            }
136
137
            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...
138
                $io->error('You have set force flag, existing files will be overwritten', true);
139
            }
140
        } else {
141
            \mkdir($path, 0777, true);
142
        }
143
144
        return $path;
145
    }
146
147
    protected function loadConfig(string $path = null)
148
    {
149
        if (empty($path)) {
150
            return;
151
        }
152
153
        if (!$this->_pathUtil->isAbsolute($path)) {
154
            $path = \getcwd() . '/' . $path;
155
        }
156
157
        if (!\is_file($path)) {
158
            $this->app()->io()->error('Invalid path specified for config');
159
160
            return;
161
        }
162
163
        foreach ($this->_pathUtil->readAsJson($path) as $key => $value) {
164
            $this->$key ?? $this->set($key, $value);
165
        }
166
    }
167
168
    protected function collectMissing(Interactor $io)
169
    {
170
        $setup = [
171
            'type'     => ['choices' => ['project', 'library', 'composer-plugin']],
172
            'license'  => ['choices' => ['m' => 'MIT', 'g' => 'GNU LGPL', 'a' => 'Apache 2', 'b' => 'BSD Simplified']],
173
            'php'      => ['choices' => ['5.4', '5.5', '5.6', '7.0', '7.1', '7.2']],
174
            'using'    => ['prompt' => 0, 'extra' => ' (ENTER to skip)'],
175
            'keywords' => ['prompt' => 0, 'extra' => ' (ENTER to skip)'],
176
        ];
177
178
        foreach ($this->userOptions() as $name => $option) {
179
            $default = $option->default();
180
            if ($this->$name !== null || \in_array($name, ['req', 'dev', 'config'])) {
181
                continue;
182
            }
183
184
            $set = $setup[$name] ?? [];
185
            if ($set['choices'] ?? null) {
186
                $value = $io->choice($option->desc(), $set['choices'], $default);
187
            } else {
188
                $value = $io->prompt($option->desc() . ($set['extra'] ?? ''), $default, null, $set['prompt'] ?? 1);
189
            }
190
191
            $this->set($name, $value);
192
        }
193
    }
194
195
    protected function collectPackages(Interactor $io)
196
    {
197
        foreach (['req' => 'Required', 'dev' => 'Developer'] as $key => $label) {
198
            $pkgs = $this->$key ?: $this->promptPackages($label, $io);
199
200
            foreach ($pkgs as &$pkg) {
201
                $pkg = \strpos($pkg, ':') === false ? "{$pkg}:@stable" : $pkg;
202
                $pkg = \array_combine(['name', 'version'], \explode(':', $pkg, 2));
203
            }
204
205
            $this->set($key, $pkgs);
206
        }
207
    }
208
209
    public function promptPackages(string $label, Interactor $io): array
210
    {
211
        $pkgs = [];
212
213
        do {
214
            if (!$pkg = $io->prompt($label . ' package (ENTER to skip)', null, [$this, 'validatePackage'], 0)) {
215
                break;
216
            }
217
218
            $pkgs[] = $pkg;
219
        } while (true);
220
221
        return $pkgs;
222
    }
223
224
    public function validatePackage(string $pkg): string
225
    {
226
        $pkg = \trim($pkg);
227
228
        if ($pkg && \strpos($pkg, '/') === false) {
229
            throw new InvalidArgumentException(
230
                'Package name format should be vendor/package:version (version can be omitted)'
231
            );
232
        }
233
234
        return $pkg;
235
    }
236
237
    protected function generate(string $projectPath, array $parameters)
238
    {
239
        $templatePath = __DIR__ . '/../../resources';
240
        $generator    = new TwigGenerator($templatePath, $this->getCachePath());
241
242
        // Normalize license (default MIT)
243
        $parameters['license']   = \strtolower($parameters['license'][0] ?? 'm');
244
        $parameters['namespace'] = $this->makeNamespace($parameters['namespace']);
245
        $parameters['keywords']  = $this->makeKeywords($parameters['keywords']);
246
247
        $generator->generate($projectPath, $parameters, new CollisionHandler);
248
    }
249
250
    protected function getCachePath(): string
251
    {
252
        if (!\Phar::running(false)) {
253
            return __DIR__ . '/../../.cache';
254
        }
255
256
        return $this->_pathUtil->getPhintPath('.cache');
257
    }
258
259
    protected function makeNamespace(string $value): string
260
    {
261
        $in = new Inflector;
262
263
        $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...
264
        $value   = $in->stuldyCase(\str_replace([' ', '/'], '\\', $value));
265
        $project = $in->stuldyCase(\str_replace([' ', '/', '\\'], '-', $project));
266
267
        if (\stripos($value, $project) === false) {
268
            $value .= '\\' . $project;
269
        }
270
271
        return $value;
272
    }
273
274
    protected function makeKeywords($value): array
275
    {
276
        $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...
277
278
        if (empty($value)) {
279
            return $default;
280
        }
281
282
        if (\is_string($value)) {
283
            $value = \array_map('trim', \explode(',', $value));
284
        }
285
286
        return \array_values(\array_unique(\array_merge($default, $value)));
287
    }
288
}
289