Completed
Push — master ( d29eb1...175026 )
by Jitendra
10s
created

InitCommand::generate()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 6
nc 1
nop 2
dl 0
loc 11
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 (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
            $this->_composer->withWorkDir($this->path)->update();
90
        } else {
91
            $this->_composer->withWorkDir($this->path)->install();
92
        }
93
94
        $success = $this->_composer->successful();
95
96
        $success ? $io->ok('Done', true) : $io->error('Composer setup failed', true);
97
    }
98
99
    public function interact(Interactor $io)
100
    {
101
        $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...
102
103
        if (!\preg_match('/[a-z0-9_-]/i', $project)) {
104
            throw new \InvalidArgumentException('Project argument should only contain [a-z0-9_-]');
105
        }
106
107
        $io->okBold('Phint Setup', true);
108
109
        $this->set('path', $path = $this->prepareProjectPath());
110
        $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...
111
112
        $this->collectMissing($io);
113
        $this->collectPackages($io);
114
    }
115
116
    protected function prepareProjectPath(): string
117
    {
118
        $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...
119
        $io   = $this->app()->io();
120
121
        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

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