Passed
Pull Request — master (#9)
by Jitendra
08:36
created

InitCommand::prepareProjectPath()   A

Complexity

Conditions 5
Paths 8

Size

Total Lines 22
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 12
nc 8
nop 0
dl 0
loc 22
rs 9.5555
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
        $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...
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')
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')
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(<<<'EOT'
53
The <info>init</info> command creates a new project with all basic files and
54
structures in the <project-name> directory. See some examples below:
55
56
<info>phint init</info> project-name <comment>--force --description "My awesome project" --name "Your Name" --email "[email protected]"</comment>
57
<info>phint init</info> project-name <comment>--using laravel/lumen --namespace Project/Api --type project</comment>
58
<info>phint init</info> project-name <comment>--php 5.6 --config /path/to/json --dev mockery/mockery --req doctrine/dbal --req symfony/console</comment>
59
EOT
60
            );
61
    }
62
63
    /**
64
     * Execute the command action.
65
     *
66
     * @return void
67
     */
68
    public function execute()
69
    {
70
        $io = $this->app()->io();
71
72
        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...
73
            $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

73
            $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

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