Completed
Pull Request — master (#9)
by Jitendra
01:58
created

InitCommand::collectPackages()   B

Complexity

Conditions 9
Paths 10

Size

Total Lines 38
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 20
nc 10
nop 0
dl 0
loc 38
rs 8.0555
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\Path;
12
13
class InitCommand extends Command
14
{
15
    /** @var Git */
16
    protected $_git;
17
18
    /** @var Composer */
19
    protected $_composer;
20
21
    /**
22
     * Configure the command options/arguments.
23
     *
24
     * @return void
25
     */
26
    public function __construct()
27
    {
28
        parent::__construct('init', 'Create and Scaffold a bare new PHP project');
29
30
        $this->_git      = new Git;
31
        $this->_composer = new Composer;
32
        $this->_action   = [$this, 'execute'];
0 ignored issues
show
Documentation Bug introduced by
It seems like array($this, 'execute') of type array<integer,Ahc\Phint\...ole\InitCommand|string> is incompatible with the declared type callable of property $_action.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
33
        $colorizer       = $this->writer()->colorizer();
34
35
        $this
36
            ->argument('<project>', 'The project name without slashes')
37
            ->option('-t --type', 'Project type', null, 'library')
38
            ->option('-n --name', 'Vendor full name', null, $this->_git->getConfig('user.name'))
39
            ->option('-e --email', 'Vendor email', null, $this->_git->getConfig('user.email'))
40
            ->option('-u --username', 'Vendor handle/username')
41
            ->option('-N --namespace', 'Root namespace')
42
            ->option('-k --keywords [words...]', 'Project Keywords (`php`, `<project>` auto added)')
43
            ->option('-P --php', 'Minimum PHP version', null, PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION)
44
            ->option('-p --path', 'The project path (Auto resolved)')
45
            ->option('-f --force', 'Run even if the project exists', null, false)
46
            ->option('-d --descr', 'Project description')
47
            ->option('-y --year', 'License Year', null, date('Y'))
48
            ->option('-z --using', 'Reference package name')
49
            ->option('-c --config', 'JSON filepath to read config from')
50
            ->option('-r --req [pkgs...]', 'Required packages')
51
            ->option('-D --dev [pkgs...]', 'Developer packages')
52
            ->usage(
53
$colorizer->bold('  phint init') . ' <project> '
54
. $colorizer->comment('--force --description "My awesome project" --name "Your Name" --email "[email protected]"') . PHP_EOL
55
. $colorizer->bold('  phint init') . ' <project> '
56
. $colorizer->comment('--using laravel/lumen --namespace Project/Api --type project') . PHP_EOL
57
. $colorizer->bold('  phint init') . ' <project> '
58
. $colorizer->comment('--php 7.0 --config /path/to/json --dev mockery/mockery --req adhocore/jwt --req adhocore/cli')
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 ($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->write('Using ') . $io->comment($this->using) . $io->write(' to create project', true);
0 ignored issues
show
Bug introduced by
Are you sure $io->write(' to create project', true) of type mixed|Ahc\Cli\Output\Writer can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

72
            $io->write('Using ') . $io->comment($this->using) . /** @scrutinizer ignore-type */ $io->write(' to create project', true);
Loading history...
Bug introduced by
Are you sure $io->write('Using ') of type mixed|Ahc\Cli\Output\Writer can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

72
            /** @scrutinizer ignore-type */ $io->write('Using ') . $io->comment($this->using) . $io->write(' to create project', true);
Loading history...
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());
79
80
        $io->write('Setting up ')->cyanBold('git', true);
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->write('Setting up ')->cyanBold('composer')->comment(' (takes some time)', true);
84
        $this->_composer->withWorkDir($this->path)->install();
85
86
        $io->ok('Done', true);
87
    }
88
89
    protected function prepareProjectPath()
90
    {
91
        $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...
92
        $io   = $this->app()->io();
93
94
        if (!(new Path)->isAbsolute($path)) {
95
            $path = \getcwd() . '/' . $path;
96
        }
97
98
        if (\is_dir($path)) {
99
            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...
100
                throw new \InvalidArgumentException('Something with the same name already exists!');
101
            }
102
103
            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...
104
                $io->error('You have set force flag, existing files will be overwritten', true);
105
            }
106
        } else {
107
            \mkdir(\rtrim($path, '/') . '/src', 0777, true);
108
        }
109
110
        return $path;
111
    }
112
113
    public function interact(Interactor $io)
114
    {
115
        $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...
116
117
        if (!\preg_match('/[a-z0-9_-]/i', $project)) {
118
            throw new \InvalidArgumentException('Project argument should only contain [a-z0-9_-]');
119
        }
120
121
        $io->okBold('Phint Setup', true);
122
123
        $this->set('path', $path = $this->prepareProjectPath());
124
        $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...
125
126
        $setup = [
127
            'type'   => ['choices' => ['project', 'library', 'composer-plugin']],
128
            'php'    => ['choices' => ['5.4', '5.5', '5.6', '7.0', '7.1', '7.2']],
129
            'using'  => ['prompt' => 0],
130
        ];
131
132
        $options = $this->userOptions();
133
        foreach ($options as $name => $option) {
134
            $default = $option->default();
135
            if ($this->$name !== null || \in_array($name, ['req', 'dev', 'config'])) {
136
                continue;
137
            }
138
139
            $set = $setup[$name] ?? [];
140
            if ($set['choices'] ?? null) {
141
                $value = $io->choice($option->desc(), $set['choices'], $default);
142
            } else {
143
                $value = $io->prompt($option->desc(), $default, null, $set['prompt'] ?? 1);
144
            }
145
146
            if ($name === 'namespace' && \stripos($value, $project) === false) {
147
                $value .= '\\' . ucfirst($project);
148
            }
149
            if ($name === 'keywords') {
150
                $value = \array_merge(['php', $project], \array_map('trim', \explode(',', $value)));
151
            }
152
153
            $this->set($name, $value);
154
        }
155
156
        $this->collectPackages();
157
    }
158
159
    protected function generate($projectPath, array $parameters)
160
    {
161
        $templatePath = __DIR__ . '/../../resources';
162
        $cachePath    = __DIR__ . '/../../.cache';
163
164
        $generator = new TwigGenerator($templatePath, $cachePath);
165
166
        $generator->generate($projectPath, $parameters, new CollisionHandler);
167
    }
168
169
    protected function loadConfig($path = null)
170
    {
171
        if (empty($path)) {
172
            return;
173
        }
174
175
        $pathUtil = new Path;
176
177
        if (!$pathUtil->isAbsolute($path)) {
178
            $path = \getcwd() . '/' . $path;
179
        }
180
181
        if (!\is_file($path)) {
182
            $this->app()->io()->error('Invalid path specified for config');
183
184
            return;
185
        }
186
187
        $config = $pathUtil->readAsJson($path);
188
        unset($config['path']);
189
190
        foreach ($config as $key => $value) {
191
            $this->set($key, $value);
192
        }
193
    }
194
195
    protected function collectPackages()
196
    {
197
        $fn = function ($pkg) {
198
            if (!empty($pkg) && strpos($pkg, '/') === false) {
199
                throw new \InvalidArgumentException(
200
                    'Package name format should be vendor/package:version (version can be omitted)'
201
                );
202
            }
203
204
            return $pkg;
205
        };
206
207
        $io = $this->app()->io();
208
209
        foreach (['req' => 'Required', 'dev' => 'Developer'] as $key => $label) {
210
            $pkgs = $this->$key;
211
212
            if (!$pkgs) {
213
                do {
214
                    $pkgs[] = $io->prompt($label . ' package (press ENTER to skip)', null, $fn, 0);
215
216
                    if (!end($pkgs)) {
217
                        array_pop($pkgs);
218
219
                        break;
220
                    }
221
                } while (true);
222
            }
223
224
            foreach ($pkgs as &$pkg) {
225
                if (strpos($pkg, ':') === false) {
226
                    $pkg .= ':@stable';
227
                }
228
229
                $pkg = array_combine(['name', 'version'], explode(':', $pkg, 2));
230
            }
231
232
            $this->set($key, $pkgs);
233
        }
234
    }
235
}
236