Passed
Pull Request — master (#8)
by Jitendra
01:36
created

InitCommand::loadConfig()   C

Complexity

Conditions 8
Paths 15

Size

Total Lines 33
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 17
nc 15
nop 1
dl 0
loc 33
rs 5.3846
c 0
b 0
f 0
1
<?php
2
3
namespace Ahc\Phint\Console;
4
5
use Ahc\Phint\Generator\CollisionHandler;
6
use Ahc\Phint\Generator\TwigGenerator;
7
use Ahc\Phint\Util\Composer;
8
use Ahc\Phint\Util\Git;
9
use Ahc\Phint\Util\Inflector;
10
use Ahc\Phint\Util\Path;
11
use Symfony\Component\Console\Input\InputArgument;
12
use Symfony\Component\Console\Input\InputInterface;
13
use Symfony\Component\Console\Input\InputOption;
14
use Symfony\Component\Console\Output\OutputInterface;
15
16
class InitCommand extends BaseCommand
17
{
18
    /** @var Git */
19
    protected $git;
20
21
    /** @var Composer */
22
    protected $composer;
23
24
    /**
25
     * Configure the command options.
26
     *
27
     * @return void
28
     */
29
    protected function configure()
30
    {
31
        $this
32
            ->setName('init')
33
            ->setDescription('Scaffold a bare new PHP project')
34
            ->addArgument('project', InputArgument::REQUIRED, 'The project name without slashes')
35
            ->addOption('path', null, InputOption::VALUE_NONE, 'The project path (Auto resolved)')
36
            ->addOption('force', 'f', InputOption::VALUE_NONE, 'Run even if the project exists')
37
            ->addOption('description', 'i', InputOption::VALUE_OPTIONAL, 'Project description')
38
            ->addOption('name', 'm', InputOption::VALUE_OPTIONAL, 'Vendor full name, defaults to git name')
39
            ->addOption('username', 'u', InputOption::VALUE_OPTIONAL, 'Vendor handle/username')
40
            ->addOption('email', 'e', InputOption::VALUE_OPTIONAL, 'Vendor email, defaults to git email')
41
            ->addOption('namespace', 's', InputOption::VALUE_OPTIONAL, 'Root namespace')
42
            ->addOption('year', 'y', InputOption::VALUE_OPTIONAL, 'License Year', date('Y'))
43
            ->addOption('type', 't', InputOption::VALUE_OPTIONAL, 'Project type')
44
            ->addOption('using', 'z', InputOption::VALUE_OPTIONAL, 'Reference package name (eg: laravel/lumen)')
45
            ->addOption('keywords', 'l', InputOption::VALUE_OPTIONAL, 'Project Keywords')
46
            ->addOption('php', 'p', InputOption::VALUE_OPTIONAL, 'Minimum PHP version project needs')
47
            ->addOption('config', 'c', InputOption::VALUE_OPTIONAL, 'JSON filepath to read config from')
48
            ->addOption('req', 'r', InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Required packages', [])
49
            ->addOption('dev', 'd', InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Developer packages', [])
50
            ->setHelp(<<<'EOT'
51
The <info>init</info> command creates a new project with all basic files and
52
structures in the <project-name> directory. See some examples below:
53
54
<info>phint init</info> project-name <comment>--force --description "My awesome project" --name "Your Name" --email "[email protected]"</comment>
55
<info>phint init</info> project-name <comment>--using laravel/lumen --namespace Project/Api --type project</comment>
56
<info>phint init</info> project-name <comment>--php 5.6 --config /path/to/json --dev mockery/mockery --req doctrine/dbal --req symfony/console</comment>
57
EOT
58
            );
59
    }
60
61
    /**
62
     * Execute the command.
63
     *
64
     * @param InputInterface  $input
65
     * @param OutputInterface $output
66
     *
67
     * @return void
68
     */
69
    protected function execute(InputInterface $input, OutputInterface $output)
70
    {
71
        $parameters = $this->input->getOptions() + $this->input->getArguments();
72
73
        if (null !== $using = $parameters['using']) {
74
            $this->output->writeln('Using <comment>' . $using . '</comment> to create project');
75
76
            $this->composer->createProject($parameters['path'], $using);
77
        }
78
79
        $this->output->writeln('<comment>Generating files ...</comment>');
80
81
        $this->generate($parameters['path'], $parameters);
82
83
        $this->output->writeln('Setting up <info>git</info>');
84
85
        $this->git->init()->addRemote($parameters['username'], $parameters['project']);
86
87
        $this->output->writeln('Setting up <info>composer</info>');
88
89
        $this->composer->install();
90
91
        $output->writeln('<comment>Done</comment>');
92
    }
93
94
    protected function prepareProjectPath()
95
    {
96
        $path = $this->input->getArgument('project');
97
98
        if (!(new Path)->isAbsolute($path)) {
99
            $path = \getcwd() . '/' . $path;
100
        }
101
102
        if (\file_exists($path)) {
103
            if (!$this->input->getOption('force')) {
104
                throw new \InvalidArgumentException('Something with the same name already exists!');
105
            }
106
107
            if (!$this->input->getOption('using')) {
108
                $this->output->writeln('<error>You have set force flag, existing files will be overwritten</error>');
109
            }
110
        } else {
111
            \mkdir(\rtrim($path, '/') . '/src', 0777, true);
112
        }
113
114
        return $path;
115
    }
116
117
    protected function interact(InputInterface $input, OutputInterface $output)
118
    {
119
        $this->input  = $input;
120
        $this->output = $output;
1 ignored issue
show
Documentation Bug introduced by
It seems like $output of type Symfony\Component\Console\Output\OutputInterface is incompatible with the declared type Ahc\Phint\Console\OutputInterface of property $output.

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...
121
122
        $project = $input->getArgument('project');
123
124
        if (empty($project) || !preg_match('/[a-z0-9_-]/i', $project)) {
125
            throw new \InvalidArgumentException('Project argument is required and should only contain [a-z0-9_-]');
126
        }
127
128
        $this->loadConfig($this->input->getOption('config'));
129
130
        $this->input->setOption('path', $path = $this->prepareProjectPath());
131
132
        $this->git      = (new Git)->withOutput($this->output)->withWorkDir($path);
133
        $this->composer = (new Composer)->withOutput($this->output)->withWorkDir($path);
134
135
        $this->output->writeln('<info>Phint Setup</info>');
136
        $this->output->writeln('<comment>Just press ENTER if you want to use the [default] or skip<comment>');
137
        $this->output->writeln('');
138
139
        $this->input->setOption('type', $this->input->getOption('type') ?: $this->prompt(
140
            'Project type (project/library)',
141
            'library',
142
            ['project', 'library', 'composer-plugin']
143
        ));
144
145
        $this->input->setOption('name', $this->input->getOption('name') ?: $this->prompt(
146
            'Vendor full name',
147
            $this->git->getConfig('user.name')
148
        ));
149
150
        $this->input->setOption('email', $this->input->getOption('email') ?: $this->prompt(
151
            'Vendor email',
152
             $this->git->getConfig('user.email')
153
        ));
154
155
        $this->input->setOption('description', $this->input->getOption('description') ?: $this->prompt(
156
            'Brief project description'
157
        ));
158
159
        $this->input->setOption('username', $username = $this->input->getOption('username') ?: $this->prompt(
160
            'Vendor handle (often github username)',
161
            getenv('VENDOR_USERNAME') ?: null
162
        ));
163
164
        $inflector = new Inflector;
165
166
        $namespace = $this->input->getOption('namespace') ?: $this->prompt(
167
            'Project root namespace (forward slashes are auto fixed)',
168
            (getenv('VENDOR_NAMESPACE') ?: $inflector->stuldyCase($username))
169
                . '/' . $inflector->stuldyCase($project)
170
        );
171
172
        $this->input->setOption('namespace', \str_replace('/', '\\\\', $namespace));
173
174
        $keywords = $this->input->getOption('keywords') ?: $this->prompt(
175
            'Project keywords (CSV)',
176
            "php, $project"
177
        );
178
179
        $this->input->setOption('keywords', array_map('trim', explode(',', $keywords)));
1 ignored issue
show
Bug introduced by
array_map('trim', explode(',', $keywords)) of type array is incompatible with the type string|boolean expected by parameter $value of Symfony\Component\Consol...tInterface::setOption(). ( Ignorable by Annotation )

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

179
        $this->input->setOption('keywords', /** @scrutinizer ignore-type */ array_map('trim', explode(',', $keywords)));
Loading history...
180
181
        $this->input->setOption('php', floatval($this->input->getOption('php') ?: $this->prompt(
182
            'Minimum PHP version project needs',
183
            PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION,
184
            ['5.4', '5.5', '5.6', '7.0', '7.1']
185
        )));
186
187
        $this->collectPackages();
188
    }
189
190
    protected function generate($projectPath, array $parameters)
191
    {
192
        $templatePath = __DIR__ . '/../../resources';
193
        $cachePath    = __DIR__ . '/../../.cache';
194
195
        $generator = new TwigGenerator($templatePath, $cachePath);
196
197
        $generator->generate($projectPath, $parameters, new CollisionHandler);
198
    }
199
200
    protected function loadConfig($path = null)
201
    {
202
        if (empty($path)) {
203
            return;
204
        }
205
206
        $pathUtil = new Path;
207
208
        if (!$pathUtil->isAbsolute($path)) {
209
            $path = getcwd() . '/' . $path;
210
        }
211
212
        if (!is_file($path)) {
213
            $this->output->writeln('<error>Invalid path specified for config</error>');
214
215
            return;
216
        }
217
218
        $config = (new Path)->readAsJson($path);
219
220
        if (empty($config)) {
221
            return;
222
        }
223
224
        unset($config['path']);
225
226
        foreach ($config as $key => $value) {
227
            if ($this->input->hasOption($key)) {
228
                $this->input->setOption($key, $value);
229
            }
230
231
            if ($key === 'vendor_namespace') {
232
                putenv('VENDOR_NAMESPACE=' . $value);
233
            }
234
        }
235
    }
236
237
    protected function collectPackages()
238
    {
239
        $fn = function ($pkg) {
240
            if (!empty($pkg) && strpos($pkg, '/') === false) {
241
                throw new \InvalidArgumentException(
242
                    'Package name format should be vendor/package:version (version can be omitted)'
243
                );
244
            }
245
246
            return $pkg;
247
        };
248
249
        foreach (['req' => 'Required', 'dev' => 'Developer'] as $key => $label) {
250
            $pkgs = $this->input->getOption($key);
251
252
            if (!$pkgs) {
253
                do {
254
                    $pkgs[] = $this->prompt($label . ' package (press ENTER to skip)', null, $fn);
255
256
                    if (!end($pkgs)) {
257
                        array_pop($pkgs);
258
259
                        break;
260
                    }
261
                } while (true);
262
            }
263
264
            foreach ($pkgs as &$pkg) {
265
                if (strpos($pkg, ':') === false) {
266
                    $pkg .= ':@stable';
267
                }
268
269
                $pkg = array_combine(['name', 'version'], explode(':', $pkg, 2));
270
            }
271
272
            $this->input->setOption($key, $pkgs);
273
        }
274
    }
275
}
276