InstallCommand   B
last analyzed

Complexity

Total Complexity 40

Size/Duplication

Total Lines 393
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 9

Test Coverage

Coverage 51%

Importance

Changes 2
Bugs 1 Features 0
Metric Value
wmc 40
c 2
b 1
f 0
lcom 1
cbo 9
dl 0
loc 393
ccs 102
cts 200
cp 0.51
rs 8.2608

13 Methods

Rating   Name   Duplication   Size   Complexity  
A configure() 0 20 1
B execute() 0 46 3
C checkRequirements() 0 47 8
B getAdminInformation() 0 36 5
B getPhpciConfigInformation() 0 29 3
A getQueueInformation() 0 18 4
B getDatabaseInformation() 0 33 5
B verifyDatabaseDetails() 0 25 2
A writeConfigFile() 0 7 1
A setupDatabase() 0 10 1
A createAdminUser() 0 15 2
A reloadConfig() 0 8 2
A verifyNotInstalled() 0 15 3

How to fix   Complexity   

Complex Class

Complex classes like InstallCommand often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use InstallCommand, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * PHPCI - Continuous Integration for PHP.
4
 *
5
 * @copyright    Copyright 2014, Block 8 Limited.
6
 * @license      https://github.com/Block8/PHPCI/blob/master/LICENSE.md
7
 *
8
 * @link         https://www.phptesting.org/
9
 */
10
11
namespace PHPCI\Command;
12
13
use Exception;
14
use PDO;
15
use b8\Config;
16
use b8\Store\Factory;
17
use PHPCI\Helper\Lang;
18
use Symfony\Component\Console\Command\Command;
19
use Symfony\Component\Console\Helper\DialogHelper;
20
use Symfony\Component\Console\Input\InputInterface;
21
use Symfony\Component\Console\Input\InputOption;
22
use Symfony\Component\Console\Output\OutputInterface;
23
use PHPCI\Service\UserService;
24
25
/**
26
 * Install console command - Installs PHPCI.
27
 *
28
 * @author       Dan Cryer <[email protected]>
29
 */
30
class InstallCommand extends Command
31
{
32
    protected $configFilePath;
33
34 9
    protected function configure()
35
    {
36 9
        $defaultPath = PHPCI_DIR.'PHPCI/config.yml';
37
38 9
        $this
39 9
            ->setName('phpci:install')
40 9
            ->addOption('url', null, InputOption::VALUE_OPTIONAL, Lang::get('installation_url'))
41 9
            ->addOption('db-host', null, InputOption::VALUE_OPTIONAL, Lang::get('db_host'))
42 9
            ->addOption('db-name', null, InputOption::VALUE_OPTIONAL, Lang::get('db_name'))
43 9
            ->addOption('db-user', null, InputOption::VALUE_OPTIONAL, Lang::get('db_user'))
44 9
            ->addOption('db-pass', null, InputOption::VALUE_OPTIONAL, Lang::get('db_pass'))
45 9
            ->addOption('admin-name', null, InputOption::VALUE_OPTIONAL, Lang::get('admin_name'))
46 9
            ->addOption('admin-pass', null, InputOption::VALUE_OPTIONAL, Lang::get('admin_pass'))
47 9
            ->addOption('admin-mail', null, InputOption::VALUE_OPTIONAL, Lang::get('admin_email'))
48 9
            ->addOption('config-path', null, InputOption::VALUE_OPTIONAL, Lang::get('config_path'), $defaultPath)
49 9
            ->addOption('queue-disabled', null, InputOption::VALUE_NONE, 'Don\'t ask for queue details')
50 9
            ->addOption('queue-server', null, InputOption::VALUE_OPTIONAL, 'Beanstalkd queue server hostname')
51 9
            ->addOption('queue-name', null, InputOption::VALUE_OPTIONAL, 'Beanstalkd queue name')
52 9
            ->setDescription(Lang::get('install_phpci'));
53 9
    }
54
55
    /**
56
     * Installs PHPCI - Can be run more than once as long as you ^C instead of entering an email address.
57
     */
58 9
    protected function execute(InputInterface $input, OutputInterface $output)
59
    {
60 9
        $this->configFilePath = $input->getOption('config-path');
61
62 9
        if (!$this->verifyNotInstalled($output)) {
63
            return;
64
        }
65
66 9
        $output->writeln('');
67 9
        $output->writeln('<info>******************</info>');
68 9
        $output->writeln('<info> '.Lang::get('welcome_to_phpci').'</info>');
69 9
        $output->writeln('<info>******************</info>');
70 9
        $output->writeln('');
71
72 9
        $this->checkRequirements($output);
73
74 9
        $output->writeln(Lang::get('please_answer'));
75 9
        $output->writeln('-------------------------------------');
76 9
        $output->writeln('');
77
78
        // ----
79
        // Get MySQL connection information and verify that it works:
80
        // ----
81 9
        $connectionVerified = false;
82
83 9
        while (!$connectionVerified) {
84 9
            $db = $this->getDatabaseInformation($input, $output);
85
86 9
            $connectionVerified = $this->verifyDatabaseDetails($db, $output);
87 9
        }
88
89 9
        $output->writeln('');
90
91 9
        $conf = array();
92 9
        $conf['b8']['database'] = $db;
0 ignored issues
show
Bug introduced by
The variable $db does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
93
94
        // ----
95
        // Get basic installation details (URL, etc)
96
        // ----
97 9
        $conf['phpci'] = $this->getPhpciConfigInformation($input, $output);
98
99 9
        $this->writeConfigFile($conf);
100 9
        $this->setupDatabase($output);
101 9
        $admin = $this->getAdminInformation($input, $output);
102 9
        $this->createAdminUser($admin, $output);
103 9
    }
104
105
    /**
106
     * Check PHP version, required modules and for disabled functions.
107
     *
108
     * @param OutputInterface $output
109
     *
110
     * @throws \Exception
111
     */
112
    protected function checkRequirements(OutputInterface $output)
113
    {
114
        $output->write('Checking requirements...');
115
        $errors = false;
116
117
        // Check PHP version:
118
        if (!(version_compare(PHP_VERSION, '5.3.8') >= 0)) {
119
            $output->writeln('');
120
            $output->writeln('<error>'.Lang::get('phpci_php_req').'</error>');
121
            $errors = true;
122
        }
123
124
        // Check required extensions are present:
125
        $requiredExtensions = array('PDO', 'pdo_mysql');
126
127
        foreach ($requiredExtensions as $extension) {
128
            if (!extension_loaded($extension)) {
129
                $output->writeln('');
130
                $output->writeln('<error>'.Lang::get('extension_required', $extension).'</error>');
131
                $errors = true;
132
            }
133
        }
134
135
        // Check required functions are callable:
136
        $requiredFunctions = array('exec', 'shell_exec');
137
138
        foreach ($requiredFunctions as $function) {
139
            if (!function_exists($function)) {
140
                $output->writeln('');
141
                $output->writeln('<error>'.Lang::get('function_required', $function).'</error>');
142
                $errors = true;
143
            }
144
        }
145
146
        if (!function_exists('password_hash')) {
147
            $output->writeln('');
148
            $output->writeln('<error>'.Lang::get('function_required', $function).'</error>');
0 ignored issues
show
Bug introduced by
The variable $function seems to be defined by a foreach iteration on line 138. Are you sure the iterator is never empty, otherwise this variable is not defined?

It seems like you are relying on a variable being defined by an iteration:

foreach ($a as $b) {
}

// $b is defined here only if $a has elements, for example if $a is array()
// then $b would not be defined here. To avoid that, we recommend to set a
// default value for $b.


// Better
$b = 0; // or whatever default makes sense in your context
foreach ($a as $b) {
}

// $b is now guaranteed to be defined here.
Loading history...
149
            $errors = true;
150
        }
151
152
        if ($errors) {
153
            throw new Exception(Lang::get('requirements_not_met'));
154
        }
155
156
        $output->writeln(' <info>'.Lang::get('ok').'</info>');
157
        $output->writeln('');
158
    }
159
160
    /**
161
     * Load information for admin user form CLI options or ask info to user.
162
     *
163
     * @param InputInterface  $input
164
     * @param OutputInterface $output
165
     *
166
     * @return array
167
     */
168 9
    protected function getAdminInformation(InputInterface $input, OutputInterface $output)
169
    {
170 9
        $admin = array();
171
172
        /*
173
         * @var \Symfony\Component\Console\Helper\DialogHelper
174
         */
175 9
        $dialog = $this->getHelperSet()->get('dialog');
176
177
        // Function to validate mail address.
178
        $mailValidator = function ($answer) {
179 8
            if (!filter_var($answer, FILTER_VALIDATE_EMAIL)) {
180
                throw new \InvalidArgumentException(Lang::get('must_be_valid_email'));
181
            }
182
183 8
            return $answer;
184 9
        };
185
186 9
        if ($adminEmail = $input->getOption('admin-mail')) {
187 8
            $adminEmail = $mailValidator($adminEmail);
188 8
        } else {
189 1
            $adminEmail = $dialog->askAndValidate($output, Lang::get('enter_email'), $mailValidator, false);
190
        }
191 9
        if (!$adminName = $input->getOption('admin-name')) {
192 1
            $adminName = $dialog->ask($output, Lang::get('enter_name'));
193 1
        }
194 9
        if (!$adminPass = $input->getOption('admin-pass')) {
195 1
            $adminPass = $dialog->askHiddenResponse($output, Lang::get('enter_password'));
196 1
        }
197
198 9
        $admin['mail'] = $adminEmail;
199 9
        $admin['name'] = $adminName;
200 9
        $admin['pass'] = $adminPass;
201
202 9
        return $admin;
203
    }
204
205
    /**
206
     * Load configuration for PHPCI form CLI options or ask info to user.
207
     *
208
     * @param InputInterface  $input
209
     * @param OutputInterface $output
210
     *
211
     * @return array
212
     */
213 9
    protected function getPhpciConfigInformation(InputInterface $input, OutputInterface $output)
214
    {
215 9
        $phpci = array();
216
217
        /*
218
         * @var \Symfony\Component\Console\Helper\DialogHelper
219
         */
220 9
        $dialog = $this->getHelperSet()->get('dialog');
221
222
        // FUnction do validate URL.
223 9
        $urlValidator = function ($answer) {
224 8
            if (!filter_var($answer, FILTER_VALIDATE_URL)) {
225
                throw new Exception(Lang::get('must_be_valid_url'));
226
            }
227
228 8
            return rtrim($answer, '/');
229 9
        };
230
231 9
        if ($url = $input->getOption('url')) {
232 8
            $url = $urlValidator($url);
233 8
        } else {
234 1
            $url = $dialog->askAndValidate($output, Lang::get('enter_phpci_url'), $urlValidator, false);
235
        }
236
237 9
        $phpci['url'] = $url;
238 9
        $phpci['worker'] = $this->getQueueInformation($input, $output, $dialog);
239
240 9
        return $phpci;
241
    }
242
243
    /**
244
     * If the user wants to use a queue, get the necessary details.
245
     *
246
     * @param InputInterface  $input
247
     * @param OutputInterface $output
248
     * @param DialogHelper    $dialog
249
     *
250
     * @return array
251
     */
252 9
    protected function getQueueInformation(InputInterface $input, OutputInterface $output, DialogHelper $dialog)
253
    {
254 9
        if ($input->getOption('queue-disabled')) {
255 9
            return;
256
        }
257
258
        $rtn = [];
259
260
        if (!$rtn['host'] = $input->getOption('queue-server')) {
261
            $rtn['host'] = $dialog->ask($output, 'Enter your beanstalkd hostname [localhost]: ', 'localhost');
262
        }
263
264
        if (!$rtn['queue'] = $input->getOption('queue-name')) {
265
            $rtn['queue'] = $dialog->ask($output, 'Enter the queue (tube) name to use [phpci]: ', 'phpci');
266
        }
267
268
        return $rtn;
269
    }
270
271
    /**
272
     * Load configuration for DB form CLI options or ask info to user.
273
     *
274
     * @param InputInterface  $input
275
     * @param OutputInterface $output
276
     *
277
     * @return array
278
     */
279 9
    protected function getDatabaseInformation(InputInterface $input, OutputInterface $output)
280
    {
281 9
        $db = array();
282
283
        /*
284
         * @var \Symfony\Component\Console\Helper\DialogHelper
285
         */
286 9
        $dialog = $this->getHelperSet()->get('dialog');
287
288 9
        if (!$dbHost = $input->getOption('db-host')) {
289 1
            $dbHost = $dialog->ask($output, Lang::get('enter_db_host'), 'localhost');
290 1
        }
291
292 9
        if (!$dbName = $input->getOption('db-name')) {
293 1
            $dbName = $dialog->ask($output, Lang::get('enter_db_name'), 'phpci');
294 1
        }
295
296 9
        if (!$dbUser = $input->getOption('db-user')) {
297 1
            $dbUser = $dialog->ask($output, Lang::get('enter_db_user'), 'phpci');
298 1
        }
299
300 9
        if (!$dbPass = $input->getOption('db-pass')) {
301 1
            $dbPass = $dialog->askHiddenResponse($output, Lang::get('enter_db_pass'));
302 1
        }
303
304 9
        $db['servers']['read'] = $dbHost;
305 9
        $db['servers']['write'] = $dbHost;
306 9
        $db['name'] = $dbName;
307 9
        $db['username'] = $dbUser;
308 9
        $db['password'] = $dbPass;
309
310 9
        return $db;
311
    }
312
313
    /**
314
     * Try and connect to MySQL using the details provided.
315
     *
316
     * @param array           $db
317
     * @param OutputInterface $output
318
     *
319
     * @return bool
320
     */
321
    protected function verifyDatabaseDetails(array $db, OutputInterface $output)
322
    {
323
        try {
324
            $pdo = new PDO(
325
                'mysql:host='.$db['servers']['write'].';dbname='.$db['name'],
326
                $db['username'],
327
                $db['password'],
328
                array(
329
                    \PDO::ATTR_PERSISTENT => false,
330
                    \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
331
                    \PDO::ATTR_TIMEOUT => 2,
332
                    \PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES \'UTF8\'',
333
                )
334
            );
335
336
            unset($pdo);
337
338
            return true;
339
        } catch (Exception $ex) {
340
            $output->writeln('<error>'.Lang::get('could_not_connect').'</error>');
341
            $output->writeln('<error>'.$ex->getMessage().'</error>');
342
        }
343
344
        return false;
345
    }
346
347
    /**
348
     * Write the PHPCI config.yml file.
349
     *
350
     * @param array $config
351
     */
352
    protected function writeConfigFile(array $config)
353
    {
354
        $dumper = new \Symfony\Component\Yaml\Dumper();
355
        $yaml = $dumper->dump($config, 4);
356
357
        file_put_contents($this->configFilePath, $yaml);
358
    }
359
360
    protected function setupDatabase(OutputInterface $output)
361
    {
362
        $output->write(Lang::get('setting_up_db'));
363
364
        $phinxBinary = escapeshellarg(PHPCI_DIR.'vendor/bin/phinx');
365
        $phinxScript = escapeshellarg(PHPCI_DIR.'phinx.php');
366
        shell_exec($phinxBinary.' migrate -c '.$phinxScript);
367
368
        $output->writeln('<info>'.Lang::get('ok').'</info>');
369
    }
370
371
    /**
372
     * Create admin user using information loaded before.
373
     *
374
     * @param array           $admin
375
     * @param OutputInterface $output
376
     */
377
    protected function createAdminUser($admin, $output)
378
    {
379
        try {
380
            $this->reloadConfig();
381
382
            $userStore = Factory::getStore('User');
383
            $userService = new UserService($userStore);
0 ignored issues
show
Compatibility introduced by
$userStore of type object<b8\Store> is not a sub-type of object<PHPCI\Store\UserStore>. It seems like you assume a child class of the class b8\Store to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
384
            $userService->createUser($admin['name'], $admin['mail'], $admin['pass'], 1);
0 ignored issues
show
Documentation introduced by
1 is of type integer, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
385
386
            $output->writeln('<info>'.Lang::get('user_created').'</info>');
387
        } catch (\Exception $ex) {
388
            $output->writeln('<error>'.Lang::get('failed_to_create').'</error>');
389
            $output->writeln('<error>'.$ex->getMessage().'</error>');
390
        }
391
    }
392
393
    protected function reloadConfig()
394
    {
395
        $config = Config::getInstance();
396
397
        if (file_exists($this->configFilePath)) {
398
            $config->loadYaml($this->configFilePath);
399
        }
400
    }
401
402
    /**
403
     * @param OutputInterface $output
404
     *
405
     * @return bool
406
     */
407
    protected function verifyNotInstalled(OutputInterface $output)
408
    {
409
        if (file_exists($this->configFilePath)) {
410
            $content = file_get_contents($this->configFilePath);
411
412
            if (!empty($content)) {
413
                $output->writeln('<error>'.Lang::get('config_exists').'</error>');
414
                $output->writeln('<error>'.Lang::get('update_instead').'</error>');
415
416
                return false;
417
            }
418
        }
419
420
        return true;
421
    }
422
}
423