Completed
Push — master ( 39c07f...aa04c1 )
by Jitendra
11s
created

InitCommand::prepareProjectPath()   A

Complexity

Conditions 5
Paths 8

Size

Total Lines 24
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 13
nc 8
nop 0
dl 0
loc 24
rs 9.5222
c 0
b 0
f 0
1
<?php
2
3
namespace Ahc\Phint\Console;
4
5
use Ahc\Cli\Exception\InvalidArgumentException;
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 BaseCommand
16
{
17
    /** @var string Command name */
18
    protected $_name = 'init';
19
20
    /** @var string Command description */
21
    protected $_desc = 'Create and Scaffold a bare new PHP project';
22
23
    /**
24
     * Configure the command options/arguments.
25
     *
26
     * @return void
27
     */
28
    protected function onConstruct()
29
    {
30
        $this
31
            ->argument('<project>', 'The project name without slashes')
32
            ->option('-T --type', 'Project type')
33
            ->option('-n --name', 'Vendor full name', null, $this->_git->getConfig('user.name'))
34
            ->option('-e --email', 'Vendor email', null, $this->_git->getConfig('user.email'))
35
            ->option('-u --username', 'Vendor handle/username')
36
            ->option('-N --namespace', 'Root namespace (use `/` separator)')
37
            ->option('-w --keywords [words...]', 'Project Keywords (`php`, `<project>` auto added)')
38
            ->option('-P --php', 'Minimum PHP version', 'floatval')
39
            ->option('-p --path', 'The project path (Auto resolved)')
40
            ->option('-f --force', 'Run even if the project exists', null, false)
41
            ->option('-d --descr', 'Project description')
42
            ->option('-y --year', 'License Year', null, date('Y'))
43
            ->option('-z --using', 'Reference package')
44
            ->option('-C --config', 'JSON filepath to read config from')
45
            ->option('-R --req [pkgs...]', 'Required packages')
46
            ->option('-D --dev [pkgs...]', 'Developer packages')
47
            ->option('-t --no-travis', 'Disable travis')
48
            ->option('-c --no-codecov', 'Disable codecov')
49
            ->option('-s --no-scrutinizer', 'Disable scrutinizer')
50
            ->option('-l --no-styleci', 'Disable StyleCI')
51
            ->option('-L --license', 'License')
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</end><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
        $success = $this->_composer->successful();
91
92
        $success ? $io->ok('Done', true) : $io->error('Composer setup failed', true);
93
    }
94
95
    public function interact(Interactor $io)
96
    {
97
        $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...
98
99
        if (!\preg_match('/[a-z0-9_-]/i', $project)) {
100
            throw new InvalidArgumentException('Project argument should only contain [a-z0-9_-]');
101
        }
102
103
        $io->okBold('Phint Setup', true);
104
105
        $this->set('path', $path = $this->prepareProjectPath());
106
        $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...
107
108
        $this->collectMissing($io);
109
        $this->collectPackages($io);
110
    }
111
112
    protected function prepareProjectPath(): string
113
    {
114
        $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...
115
        $io   = $this->app()->io();
116
117
        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

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