Install::envRequirementsRow()   B
last analyzed

Complexity

Conditions 5
Paths 16

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 11
ccs 0
cts 7
cp 0
rs 8.8571
cc 5
eloc 7
nc 16
nop 4
crap 30
1
<?php
2
3
/*
4
 * This file is part of the Tinyissue package.
5
 *
6
 * (c) Mohamed Alsharaf <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Tinyissue\Console\Commands;
13
14
use Illuminate\Console\Command;
15
use Illuminate\Support\Facades\Artisan;
16
use League\Flysystem\Adapter\Local as Adapter;
17
use League\Flysystem\Filesystem;
18
use Tinyissue\Model;
19
20
/**
21
 * Install is console command to install the Tiny Issue application.
22
 *
23
 * @author Mohamed Alsharaf <[email protected]>
24
 */
25
class Install extends Command
26
{
27
    const COLOR_GOOD  = 'green';
28
    const COLOR_BAD   = 'red';
29
    const COLOR_INFO  = 'blue';
30
    const EMPTY_VALUE = 'empty value';
31
32
    /**
33
     * The console command name.
34
     *
35
     * @var string
36
     */
37
    protected $name = 'tinyissue:install';
38
39
    /**
40
     * The console command description.
41
     *
42
     * @var string
43
     */
44
    protected $description = 'Install Tinyissue.';
45
46
    /**
47
     * Required PHP modules.
48
     *
49
     * @var array
50
     */
51
    protected $modules = [
52
        'pdo'      => 0,
53
        'mcrypt'   => 0,
54
        'openssl'  => 0,
55
        'curl'     => 0,
56
        'json'     => 0,
57
        'mbstring' => 0,
58
        'xml'      => 0,
59
    ];
60
61
    /**
62
     * Minimum PHP version.
63
     *
64
     * @var string
65
     */
66
    protected $phpVersion = '5.5.0';
67
68
    /**
69
     * Supported drivers.
70
     *
71
     * @var array
72
     */
73
    protected $dbDrivers = [
74
        'pdo_sqlite' => 0,
75
        'pdo_mysql'  => 0,
76
        'pdo_pgsql'  => 0,
77
        'pdo_sqlsrv' => 0,
78
    ];
79
80
    /**
81
     * Current user entered data & default values.
82
     *
83
     * @var array
84
     */
85
    protected $data = [
86
        'key'            => '',
87
        'timezone'       => 'Pacific/Auckland',
88
        'dbHost'         => 'localhost',
89
        'dbName'         => 'tinyissue',
90
        'dbUser'         => 'root',
91
        'dbPass'         => self::EMPTY_VALUE,
92
        'dbDriver'       => 'mysql',
93
        'dbPrefix'       => '',
94
        'sysEmail'       => '',
95
        'sysName'        => '',
96
        'adminEmail'     => '',
97
        'adminFirstName' => '',
98
        'adminLastName'  => '',
99
        'adminPass'      => '',
100
    ];
101
102
    /**
103
     * The status of the environment check.
104
     *
105
     * @var bool
106
     */
107
    protected $envStatus = true;
108
109
    /**
110
     * Environment requirements for display status table.
111
     *
112
     * @var array
113
     */
114
    protected $envRequirements = [];
115
116
    /**
117
     * @var Filesystem
118
     */
119
    protected $filesystem;
120
121
    /**
122
     * Execute the console command.
123
     *
124
     * @return bool
125
     */
126
    public function fire()
127
    {
128
        if (!$this->checkEnvironment()) {
129
            return false;
130
        }
131
132
        Artisan::call('down');
133
        $this->loop('stageOne');
134
        $this->loop('stageTwo');
135
        Artisan::call('up');
136
137
        $this->line('<fg=green>Instalation complete.</fg=green>');
138
139
        return true;
140
    }
141
142
    /**
143
     * Check the current environment and display the result in table.
144
     *
145
     * @return bool
146
     */
147
    protected function checkEnvironment()
148
    {
149
        // Check PHP modules
150
        $this->checkPhpExtension($this->modules, '{module} extension', true, 'No');
151
152
        // Check db drivers
153
        $this->checkPhpExtension($this->dbDrivers, '{module} driver for pdo', false, 'Not Found');
154
155
        // Whether or not one or more valid drivers were found
156
        $validDrivers   = $this->getValidDbDrivers();
157
        $dbDriverStatus = !empty($validDrivers);
158
        if (!$dbDriverStatus) {
159
            $dbDriverTitle = 'Install one of the following pdo drivers ('
160
                . implode(', ', array_keys($this->dbDrivers)) . ')';
161
        } else {
162
            $dbDriverTitle = 'You can choose one of the following pdo drivers ('
163
                . implode(', ', $validDrivers) . ')';
164
        }
165
        $this->envRequirementsRow($dbDriverTitle, $dbDriverStatus, true, 'No Found');
166
167
        // Check PHP version
168
        $phpVersionStatus = version_compare(PHP_VERSION, $this->phpVersion, '>=');
169
        $this->envRequirementsRow('PHP version ' . $this->phpVersion . ' or above is needed.', $phpVersionStatus, true);
170
171
        // Check application upload directory
172
        $this->envRequirementsRow('Upload directory is writable.', is_writable(base_path('storage/app/uploads')));
173
174
        // Check if upload directory is accessible to the public
175
        $url = $this->isUploadDirectoryPublic();
176
        if (!empty($url)) {
177
            $this->envRequirementsRow('Upload directory maybe accessible from (' . $url . ').');
178
        }
179
180
        // Display the result table
181
        $this->table(['Requirement', 'Status'], $this->envRequirements);
182
183
        return $this->envStatus;
184
    }
185
186
    /**
187
     * Check the availability of list of php extensions.
188
     *
189
     * @param array  $modules
190
     * @param string $labelFormat
191
     * @param bool   $required
192
     * @param string $failedLabel
193
     *
194
     * @return $this
195
     */
196
    protected function checkPhpExtension(array &$modules, $labelFormat, $required = true, $failedLabel = 'No')
197
    {
198
        foreach ($modules as $module => $status) {
199
            $title            = str_replace(['{module}'], [$module], $labelFormat);
200
            $status           = extension_loaded($module);
201
            $modules[$module] = $status;
202
            $this->envRequirementsRow($title, $status, $required, $failedLabel);
203
        }
204
205
        return $this;
206
    }
207
208
    /**
209
     * Render environment requirement row.
210
     *
211
     * @param string $label
212
     * @param bool   $status
213
     * @param bool   $required
214
     * @param string $failedLabel
215
     *
216
     * @return $this
217
     */
218
    protected function envRequirementsRow($label, $status = false, $required = false, $failedLabel = 'No')
219
    {
220
        $statusColor             = $status ? static::COLOR_GOOD : ($required ? static::COLOR_BAD : static::COLOR_INFO);
221
        $statusTitle             = $status ? 'OK' : $failedLabel;
222
        $this->envRequirements[] = $this->formatTableCells([$label, $statusTitle], $statusColor);
223
        if ($required) {
224
            $this->envStatus = $status;
225
        }
226
227
        return $this;
228
    }
229
230
    /**
231
     * Format cell text color.
232
     *
233
     * @param string[] $cells
234
     * @param string   $color
235
     *
236
     * @return array
237
     */
238
    protected function formatTableCells(array $cells, $color)
239
    {
240
        return array_map(function ($cell) use ($color) {
241
            return '<fg=' . $color . '>' . $cell . '</fg=' . $color . '>';
242
        }, $cells);
243
    }
244
245
    /**
246
     * Returns list of valid db drivers.
247
     *
248
     * @return array
249
     */
250
    protected function getValidDbDrivers()
251
    {
252
        return array_keys(array_filter($this->dbDrivers, function ($item) {
253
            return $item === true;
254
        }));
255
    }
256
257
    /**
258
     * Check if upload directory is accessible to the public.
259
     *
260
     * @return string
261
     */
262
    protected function isUploadDirectoryPublic()
263
    {
264
        $pathSegments = explode('/', base_path());
265
        $count        = count($pathSegments);
266
        $indexes      = [];
267
        for ($i = 0; $i < $count; ++$i) {
268
            $indexes[] = $i;
269
            $path      = implode('/', array_except($pathSegments, $indexes));
270
            $guessUrl  = url($path . '/storage/app/uploads');
271
            $curl      = curl_init($guessUrl);
272
            curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
273
            curl_setopt($curl, CURLOPT_HEADER, true);
274
            curl_exec($curl);
275
            $code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
276
            curl_close($curl);
277
            if ($code != '404') {
278
                return $guessUrl;
279
            }
280
        }
281
282
        return '';
283
    }
284
285
    /**
286
     * Start a stage loop.
287
     *
288
     * @param string $method The method name to execute in a loop
289
     *
290
     * @return void
291
     */
292
    protected function loop($method)
293
    {
294
        while (true) {
295
            try {
296
                $this->$method();
297
                break;
298
            } catch (\Exception $e) {
299
                $this->error($e->getMessage());
300
                $this->comment($e->getTraceAsString());
301
                $this->line('... Start again');
302
                $this->line('');
303
            }
304
        }
305
    }
306
307
    /**
308
     * Stage one:
309
     * - Collect data for the configuration file
310
     * - Create .env file
311
     * - Install the database.
312
     *
313
     * @return void
314
     *
315
     * @throws \Exception
316
     */
317
    protected function stageOne()
318
    {
319
        $this->section('Local configurations:');
320
321
        $validDbDrivers = $this->getValidDbDrivers();
322
        $this->askQuestions([
323
            'dbDriver' => ['choice', ['Select a database driver', $validDbDrivers, 0]],
324
            'dbHost'   => 'Enter the database host',
325
            'dbName'   => 'Enter the database name',
326
            'dbUser'   => 'Enter the database username',
327
            'dbPass'   => 'Enter the database password',
328
            'dbPrefix' => 'Enter the tables prefix',
329
            'sysEmail' => 'Email address used by the Tiny Issue to send emails from',
330
            'sysName'  => 'Email name used by the Tiny Issue for the email address above',
331
            'timezone' => 'The application timezone. Find your timezone from: http://php.net/manual/en/timezones.php)',
332
        ]);
333
        $this->data['key']      = md5(str_random(40));
334
        $this->data['dbDriver'] = substr($this->data['dbDriver'], 4);
335
336
        // Prefix application path for sqlite driver && database name not absolute path
337
        if ($this->data['dbDriver'] === 'sqlite'
338
            && strpos($this->data['dbName'], ':\\') === false
339
            && strpos($this->data['dbName'], '/') === false) {
340
            $this->data['dbName'] = base_path($this->data['dbName']);
341
        }
342
343
        // Create .env from .env.example and populate with user data
344
        $filesystem = $this->getFilesystem();
345
        $content    = $filesystem->read('.env.example');
346
        if (empty($content)) {
347
            throw new \Exception('Unable to read .env.example to create .env file.');
348
        }
349
350
        $dbPass = $this->getInputValue('dbPass');
351
        foreach ($this->data as $key => $value) {
352
            $value   = $key == 'dbPass' ? $dbPass : $value;
353
            $content = str_replace('{' . $key . '}', $value, $content);
354
        }
355
        if ($filesystem->has('.env')) {
356
            $filesystem->delete('.env');
357
        }
358
        $filesystem->put('.env', $content);
359
360
        // Update the current database connection
361
        $config             = \Config::get('database.connections.' . $this->data['dbDriver']);
362
        $config['driver']   = $this->data['dbDriver'];
363
        $config['host']     = $this->data['dbHost'];
364
        $config['database'] = $this->data['dbName'];
365
        $config['username'] = $this->data['dbUser'];
366
        $config['password'] = $dbPass;
367
        $config['prefix']   = $this->data['dbPrefix'];
368
        \Config::set('database.connections.' . $this->data['dbDriver'], $config);
369
        \Config::set('database.default', $this->data['dbDriver']);
370
371
        // Install the new database
372
        $this->section('Setting up the database:');
373
        Artisan::call('migrate:install');
374
        Artisan::call('migrate', ['--force' => true]);
375
        $this->info('Database created successfully.');
376
    }
377
378
    /**
379
     * Prints out a section title.
380
     *
381
     * @param string $title
382
     *
383
     * @return void
384
     */
385
    protected function section($title)
386
    {
387
        $this->line('');
388
        $this->info($title);
389
        $this->line('------------------------');
390
    }
391
392
    /**
393
     * Ask user questions.
394
     *
395
     * @param array $questions
396
     *
397
     * @return $this
398
     */
399
    protected function askQuestions(array $questions)
400
    {
401
        $labelFormat = function ($label, $value) {
402
            return sprintf('%s: (%s)', $label, $value);
403
        };
404
405
        foreach ($questions as $name => $question) {
406
            if (is_array($question)) {
407
                $question[1][0]    = $labelFormat($question[1][0], $this->data[$name]);
408
                $this->data[$name] = call_user_func_array([$this, $question[0]], $question[1]);
409
            } else {
410
                $question          = $labelFormat($question, $this->data[$name]);
411
                $this->data[$name] = $this->ask($question, $this->data[$name]);
412
            }
413
        }
414
415
        return $this;
416
    }
417
418
    /**
419
     * Returns an object for application file system.
420
     *
421
     * @return Filesystem
422
     */
423
    protected function getFilesystem()
424
    {
425
        if (null === $this->filesystem) {
426
            $this->filesystem = new Filesystem(new Adapter(base_path()));
427
        }
428
429
        return $this->filesystem;
430
    }
431
432
    /**
433
     * Stage two:
434
     * - Collect details for admin user
435
     * - Create the admin user.
436
     *
437
     * @return void
438
     */
439
    protected function stageTwo()
440
    {
441
        $this->section('Setting up the admin account:');
442
443
        $this->askQuestions([
444
            'adminEmail'     => 'Email address',
445
            'adminFirstName' => 'First Name',
446
            'adminLastName'  => 'Last Name',
447
            'adminPass'      => 'Password',
448
        ]);
449
450
        Model\User::updateOrCreate(['email' => $this->data['adminEmail']], [
451
            'email'     => $this->data['adminEmail'],
452
            'firstname' => $this->data['adminFirstName'],
453
            'lastname'  => $this->data['adminLastName'],
454
            'password'  => \Hash::make($this->data['adminPass']),
455
            'deleted'   => Model\User::NOT_DELETED_USERS,
456
            'role_id'   => 4,
457
            'language'  => 'en',
458
        ]);
459
460
        $this->info('Admin account created successfully.');
461
    }
462
463
    /**
464
     * Returns the actual value of user input.
465
     *
466
     * @param $name
467
     *
468
     * @return string
469
     */
470
    protected function getInputValue($name)
471
    {
472
        return $this->data[$name] === self::EMPTY_VALUE ? '' : $this->data[$name];
473
    }
474
}
475