Completed
Push — master ( f0391e...085236 )
by Jitendra
9s
created

InitCommand::collectPackages()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 6
nc 3
nop 1
dl 0
loc 12
rs 10
c 0
b 0
f 0
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')
41
            ->option('-k --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
            ->action([$this, 'execute'])
52
            ->usage($this->writer()->colorizer()->colors(''
53
                . '<bold>  phint init</end> <line><project></end> '
54
                . '<comment>--force --descr "Awesome project" --name "YourName" --email [email protected]</end><eol/>'
55
                . '<bold>  phint init</end> <line><project></end> '
56
                . '<comment>--using laravel/lumen --namespace Project/Api --type project</comment><eol/>'
57
                . '<bold>  phint init</end> <line><project></end> '
58
                . '<comment>--php 7.0 --config /path/to/json --dev mockery/mockery --req adhocore/cli</end><eol/>'
59
            ));
60
    }
61
62
    /**
63
     * Execute the command action.
64
     *
65
     * @return void
66
     */
67
    public function execute()
68
    {
69
        $io = $this->app()->io();
70
71
        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...
72
            $io->colors("Using <cyanBold>$using</end> to create project <comment>(takes some time)</end><eol/>");
73
74
            $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...
75
        }
76
77
        $io->comment('Generating files ...', true);
78
        $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

78
        $this->generate(/** @scrutinizer ignore-type */ $this->path, $this->values());
Loading history...
79
80
        $io->colors('Setting up <cyanBold>git</end><eol/>');
81
        $this->_git->withWorkDir($this->path)->init()->addRemote($this->username, $this->project);
0 ignored issues
show
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...
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...
82
83
        $io->colors('Setting up <cyanBold>composer</end> <comment>(takes some time)</end><eol>');
84
        if ($using) {
85
            $this->_composer->withWorkDir($this->path)->update();
86
        } else {
87
            $this->_composer->withWorkDir($this->path)->install();
88
        }
89
90
        $io->ok('Done', true);
91
    }
92
93
    public function interact(Interactor $io)
94
    {
95
        $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...
96
97
        if (!\preg_match('/[a-z0-9_-]/i', $project)) {
98
            throw new \InvalidArgumentException('Project argument should only contain [a-z0-9_-]');
99
        }
100
101
        $io->okBold('Phint Setup', true);
102
103
        $this->set('path', $path = $this->prepareProjectPath());
104
        $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...
105
106
        $this->collectMissing($io);
107
        $this->collectPackages($io);
108
    }
109
110
    protected function prepareProjectPath(): string
111
    {
112
        $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...
113
        $io   = $this->app()->io();
114
115
        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

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