Completed
Push — master ( 065cdd...96faaf )
by Alejandro
09:28
created

InstallCommand::buildAppConfig()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 38
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 23
c 0
b 0
f 0
nc 2
nop 1
dl 0
loc 38
rs 8.8571
1
<?php
2
namespace Shlinkio\Shlink\CLI\Command\Install;
3
4
use Shlinkio\Shlink\Common\Util\StringUtilsTrait;
5
use Shlinkio\Shlink\Core\Service\UrlShortener;
6
use Symfony\Component\Console\Command\Command;
7
use Symfony\Component\Console\Helper\ProcessHelper;
8
use Symfony\Component\Console\Helper\QuestionHelper;
9
use Symfony\Component\Console\Input\InputInterface;
10
use Symfony\Component\Console\Output\OutputInterface;
11
use Symfony\Component\Console\Question\ChoiceQuestion;
12
use Symfony\Component\Console\Question\Question;
13
use Zend\Config\Writer\WriterInterface;
14
15
class InstallCommand extends Command
16
{
17
    use StringUtilsTrait;
18
19
    const DATABASE_DRIVERS = [
20
        'MySQL' => 'pdo_mysql',
21
        'PostgreSQL' => 'pdo_pgsql',
22
        'SQLite' => 'pdo_sqlite',
23
    ];
24
    const SUPPORTED_LANGUAGES = ['en', 'es'];
25
26
    /**
27
     * @var InputInterface
28
     */
29
    private $input;
30
    /**
31
     * @var OutputInterface
32
     */
33
    private $output;
34
    /**
35
     * @var QuestionHelper
36
     */
37
    private $questionHelper;
38
    /**
39
     * @var ProcessHelper
40
     */
41
    private $processHelper;
42
    /**
43
     * @var WriterInterface
44
     */
45
    private $configWriter;
46
47
    /**
48
     * InstallCommand constructor.
49
     * @param WriterInterface $configWriter
50
     * @param callable|null $databaseCreationLogic
0 ignored issues
show
Bug introduced by
There is no parameter named $databaseCreationLogic. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
51
     */
52
    public function __construct(WriterInterface $configWriter)
53
    {
54
        parent::__construct(null);
55
        $this->configWriter = $configWriter;
56
    }
57
58
    public function configure()
59
    {
60
        $this->setName('shlink:install')
61
             ->setDescription('Installs Shlink');
62
    }
63
64
    public function execute(InputInterface $input, OutputInterface $output)
65
    {
66
        $this->input = $input;
67
        $this->output = $output;
68
        $this->questionHelper = $this->getHelper('question');
69
        $this->processHelper = $this->getHelper('process');
70
        $params = [];
71
72
        $output->writeln([
73
            '<info>Welcome to Shlink!!</info>',
74
            'This process will guide you through the installation.',
75
        ]);
76
77
        // Check if a cached config file exists and drop it if so
78
        if (file_exists('data/cache/app_config.php')) {
79
            $output->write('Deleting old cached config...');
80
            if (unlink('data/cache/app_config.php')) {
81
                $output->writeln(' <info>Success</info>');
82
            } else {
83
                $output->writeln(
84
                    ' <error>Failed!</error> You will have to manually delete the data/cache/app_config.php file to get'
85
                    . ' new config applied.'
86
                );
87
            }
88
        }
89
90
        // Ask for custom config params
91
        $params['DATABASE'] = $this->askDatabase();
92
        $params['URL_SHORTENER'] = $this->askUrlShortener();
93
        $params['LANGUAGE'] = $this->askLanguage();
94
        $params['APP'] = $this->askApplication();
95
96
        // Generate config params files
97
        $config = $this->buildAppConfig($params);
98
        $this->configWriter->toFile('config/params/generated_config.php', $config, false);
99
        $output->writeln(['<info>Custom configuration properly generated!</info>', '']);
100
101
        // Generate database
102
        if (! $this->createDatabase()) {
103
            return;
104
        }
105
106
        // Run database migrations
107
        $output->writeln('Updating database...');
108
        if (! $this->runCommand('php vendor/bin/doctrine-migrations migrations:migrate', 'Error updating database.')) {
109
            return;
110
        }
111
112
        // Generate proxies
113
        $output->writeln('Generating proxies...');
114
        if (! $this->runCommand('php vendor/bin/doctrine.php orm:generate-proxies', 'Error generating proxies.')) {
115
            return;
116
        }
117
    }
118
119
    protected function askDatabase()
120
    {
121
        $params = [];
122
        $this->printTitle('DATABASE');
123
124
        // Select database type
125
        $databases = array_keys(self::DATABASE_DRIVERS);
126
        $dbType = $this->questionHelper->ask($this->input, $this->output, new ChoiceQuestion(
127
            '<question>Select database type (defaults to ' . $databases[0] . '):</question>',
128
            $databases,
129
            0
130
        ));
131
        $params['DRIVER'] = self::DATABASE_DRIVERS[$dbType];
132
133
        // Ask for connection params if database is not SQLite
134
        if ($params['DRIVER'] !== self::DATABASE_DRIVERS['SQLite']) {
135
            $params['NAME'] = $this->ask('Database name', 'shlink');
136
            $params['USER'] = $this->ask('Database username');
137
            $params['PASSWORD'] = $this->ask('Database password');
138
        }
139
140
        return $params;
141
    }
142
143
    protected function askUrlShortener()
144
    {
145
        $this->printTitle('URL SHORTENER');
146
147
        // Ask for URL shortener params
148
        return [
149
            'SCHEMA' => $this->questionHelper->ask($this->input, $this->output, new ChoiceQuestion(
150
                '<question>Select schema for generated short URLs (defaults to http):</question>',
151
                ['http', 'https'],
152
                0
153
            )),
154
            'HOSTNAME' => $this->ask('Hostname for generated URLs'),
155
            'CHARS' => $this->ask(
156
                'Character set for generated short codes (leave empty to autogenerate one)',
157
                null,
158
                true
159
            ) ?: str_shuffle(UrlShortener::DEFAULT_CHARS)
160
        ];
161
    }
162
163
    protected function askLanguage()
164
    {
165
        $this->printTitle('LANGUAGE');
166
167
        return [
168
            'DEFAULT' => $this->questionHelper->ask($this->input, $this->output, new ChoiceQuestion(
169
                '<question>Select default language for the application in general (defaults to '
170
                . self::SUPPORTED_LANGUAGES[0] . '):</question>',
171
                self::SUPPORTED_LANGUAGES,
172
                0
173
            )),
174
            'CLI' => $this->questionHelper->ask($this->input, $this->output, new ChoiceQuestion(
175
                '<question>Select default language for CLI executions (defaults to '
176
                . self::SUPPORTED_LANGUAGES[0] . '):</question>',
177
                self::SUPPORTED_LANGUAGES,
178
                0
179
            )),
180
        ];
181
    }
182
183
    protected function askApplication()
184
    {
185
        $this->printTitle('APPLICATION');
186
187
        return [
188
            'SECRET' => $this->ask(
189
                'Define a secret string that will be used to sign API tokens (leave empty to autogenerate one)',
190
                null,
191
                true
192
            ) ?: $this->generateRandomString(32),
193
        ];
194
    }
195
196
    /**
197
     * @param string $text
198
     */
199
    protected function printTitle($text)
200
    {
201
        $text = trim($text);
202
        $length = strlen($text) + 4;
203
        $header = str_repeat('*', $length);
204
205
        $this->output->writeln([
206
            '',
207
            '<info>' . $header . '</info>',
208
            '<info>* ' . strtoupper($text) . ' *</info>',
209
            '<info>' . $header . '</info>',
210
        ]);
211
    }
212
213
    /**
214
     * @param string $text
215
     * @param string|null $default
216
     * @param bool $allowEmpty
217
     * @return string
218
     */
219
    protected function ask($text, $default = null, $allowEmpty = false)
220
    {
221
        if (isset($default)) {
222
            $text .= ' (defaults to ' . $default . ')';
223
        }
224
        do {
225
            $value = $this->questionHelper->ask($this->input, $this->output, new Question(
226
                '<question>' . $text . ':</question> ',
227
                $default
228
            ));
229
            if (empty($value) && ! $allowEmpty) {
230
                $this->output->writeln('<error>Value can\'t be empty</error>');
231
            }
232
        } while (empty($value) && empty($default) && ! $allowEmpty);
233
234
        return $value;
235
    }
236
237
    /**
238
     * @param array $params
239
     * @return array
240
     */
241
    protected function buildAppConfig(array $params)
242
    {
243
        // Build simple config
244
        $config = [
245
            'app_options' => [
246
                'secret_key' => $params['APP']['SECRET'],
247
            ],
248
            'entity_manager' => [
249
                'connection' => [
250
                    'driver' => $params['DATABASE']['DRIVER'],
251
                ],
252
            ],
253
            'translator' => [
254
                'locale' => $params['LANGUAGE']['DEFAULT'],
255
            ],
256
            'cli' => [
257
                'locale' => $params['LANGUAGE']['CLI'],
258
            ],
259
            'url_shortener' => [
260
                'domain' => [
261
                    'schema' => $params['URL_SHORTENER']['SCHEMA'],
262
                    'hostname' => $params['URL_SHORTENER']['HOSTNAME'],
263
                ],
264
                'shortcode_chars' => $params['URL_SHORTENER']['CHARS'],
265
            ],
266
        ];
267
268
        // Build dynamic database config
269
        if ($params['DATABASE']['DRIVER'] === 'pdo_sqlite') {
270
            $config['entity_manager']['connection']['path'] = 'data/database.sqlite';
271
        } else {
272
            $config['entity_manager']['connection']['user'] = $params['DATABASE']['USER'];
273
            $config['entity_manager']['connection']['password'] = $params['DATABASE']['PASSWORD'];
274
            $config['entity_manager']['connection']['dbname'] = $params['DATABASE']['NAME'];
275
        }
276
277
        return $config;
278
    }
279
280
    protected function createDatabase()
281
    {
282
        $this->output->writeln('Initializing database...');
283
        return $this->runCommand('php vendor/bin/doctrine.php orm:schema-tool:create', 'Error generating database.');
284
    }
285
286
    /**
287
     * @param string $command
288
     * @param string $errorMessage
289
     * @return bool
290
     */
291
    protected function runCommand($command, $errorMessage)
292
    {
293
        $process = $this->processHelper->run($this->output, $command);
294
        if ($process->isSuccessful()) {
295
            $this->output->writeln('    <info>Success!</info>');
296
            return true;
297
        } else {
298
            if ($this->output->isVerbose()) {
299
                return false;
300
            }
301
            $this->output->writeln(
302
                '    <error>' . $errorMessage . '</error>  Run this command with -vvv to see specific error info.'
303
            );
304
            return false;
305
        }
306
    }
307
}
308