Completed
Push — master ( c398b8...7a98a5 )
by Jitendra
10s
created

InitCommand::makeArray()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 5
nc 3
nop 2
dl 0
loc 11
rs 10
c 0
b 0
f 0
1
<?php
2
3
/*
4
 * This file is part of the PHINT package.
5
 *
6
 * (c) Jitendra Adhikari <[email protected]>
7
 *     <https://github.com/adhocore>
8
 *
9
 * Licensed under MIT license.
10
 */
11
12
namespace Ahc\Phint\Console;
13
14
use Ahc\Cli\Exception\InvalidArgumentException;
15
use Ahc\Cli\Input\Command;
16
use Ahc\Cli\IO\Interactor;
17
use Ahc\Phint\Generator\CollisionHandler;
18
use Ahc\Phint\Generator\TwigGenerator;
19
use Ahc\Phint\Util\Composer;
20
use Ahc\Phint\Util\Git;
21
use Ahc\Phint\Util\Inflector;
22
use Ahc\Phint\Util\Path;
23
24
class InitCommand extends BaseCommand
25
{
26
    /** @var string Command name */
27
    protected $_name = 'init';
28
29
    /** @var string Command description */
30
    protected $_desc = 'Create and Scaffold a bare new PHP project';
31
32
    /**
33
     * Configure the command options/arguments.
34
     *
35
     * @return void
36
     */
37
    protected function onConstruct()
38
    {
39
        $this
40
            ->argument('<project>', 'The project name without slashes')
41
            ->option('-T --type', 'Project type')
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('-P --php', 'Minimum PHP version', 'floatval')
47
            ->option('-p --path', 'The project path (Auto resolved)')
48
            ->option('-f --force', 'Run even if the project exists', null, false)
49
            ->option('-g --package', 'Packagist name (Without vendor handle)')
50
            ->option('-d --descr', 'Project description')
51
            ->option('-w --keywords [words...]', 'Project Keywords')
52
            ->option('-y --year', 'License Year', null, date('Y'))
53
            ->option('-b --bin [binaries...]', 'Executable binaries')
54
            ->option('-z --using', 'Reference package')
55
            ->option('-C --config', 'JSON filepath to read config from')
56
            ->option('-R --req [pkgs...]', 'Required packages')
57
            ->option('-D --dev [pkgs...]', 'Developer packages')
58
            ->option('-t --no-travis', 'Disable travis')
59
            ->option('-c --no-codecov', 'Disable codecov')
60
            ->option('-s --no-scrutinizer', 'Disable scrutinizer')
61
            ->option('-l --no-styleci', 'Disable StyleCI')
62
            ->option('-L --license', 'License')
63
            ->usage($this->writer()->colorizer()->colors(''
64
                . '<bold>  phint init</end> <line><project></end> '
65
                . '<comment>--force --descr "Awesome project" --name "YourName" --email [email protected]</end><eol/>'
66
                . '<bold>  phint init</end> <line><project></end> '
67
                . '<comment>--using laravel/lumen --namespace Project/Api --type project</end><eol/>'
68
                . '<bold>  phint init</end> <line><project></end> '
69
                . '<comment>--php 7.0 --config /path/to/json --dev mockery/mockery --req adhocore/cli</end><eol/>'
70
            ));
71
    }
72
73
    /**
74
     * Execute the command action.
75
     *
76
     * @return void
77
     */
78
    public function execute()
79
    {
80
        $io = $this->app()->io();
81
82
        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...
83
            $io->colors("Using <cyanBold>$using</end> to create project <comment>(takes some time)</end><eol/>");
84
85
            $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...
86
        }
87
88
        $io->comment('Generating files ...', true);
89
        $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

89
        $this->generate(/** @scrutinizer ignore-type */ $this->path, $this->values());
Loading history...
90
91
        $io->colors('Setting up <cyanBold>git</end><eol/>');
92
        $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...
93
94
        $io->colors('Setting up <cyanBold>composer</end> <comment>(takes some time)</end><eol>');
95
        if ($using) {
96
            $this->_composer->withWorkDir($this->path)->update();
97
        } else {
98
            $this->_composer->withWorkDir($this->path)->install();
99
        }
100
101
        $success = $this->_composer->successful();
102
103
        $success ? $io->ok('Done', true) : $io->error('Composer setup failed', true);
104
105
        $this->logging('end');
106
    }
107
108
    public function interact(Interactor $io)
109
    {
110
        $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...
111
112
        if (!\preg_match('/[a-z0-9_-]/i', $project)) {
113
            throw new InvalidArgumentException('Project argument should only contain [a-z0-9_-]');
114
        }
115
116
        $io->okBold('Phint Setup', true);
117
        $this->logging('start');
118
119
        $this->set('path', $path = $this->prepareProjectPath());
120
        $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...
121
122
        $this->collectMissing($io);
123
        $this->collectPackages($io);
124
    }
125
126
    protected function prepareProjectPath(): string
127
    {
128
        $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...
129
        $io   = $this->app()->io();
130
131
        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

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