Completed
Pull Request — master (#105)
by Mikołaj
04:26 queued 01:11
created

InstallCommand::importConfig()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 38
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 6.042

Importance

Changes 0
Metric Value
cc 6
eloc 21
nc 5
nop 0
dl 0
loc 38
ccs 17
cts 19
cp 0.8947
crap 6.042
rs 8.439
c 0
b 0
f 0
1
<?php
2
namespace Shlinkio\Shlink\CLI\Command\Install;
3
4
use Shlinkio\Shlink\CLI\Install\ConfigCustomizerPluginManagerInterface;
5
use Shlinkio\Shlink\CLI\Install\Plugin;
6
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
7
use Symfony\Component\Console\Command\Command;
8
use Symfony\Component\Console\Exception\LogicException;
9
use Symfony\Component\Console\Exception\RuntimeException;
10
use Symfony\Component\Console\Helper\ProcessHelper;
11
use Symfony\Component\Console\Helper\QuestionHelper;
12
use Symfony\Component\Console\Input\InputInterface;
13
use Symfony\Component\Console\Output\OutputInterface;
14
use Symfony\Component\Console\Question\ConfirmationQuestion;
15
use Symfony\Component\Console\Question\Question;
16
use Symfony\Component\Filesystem\Exception\IOException;
17
use Symfony\Component\Filesystem\Filesystem;
18
use Zend\Config\Writer\WriterInterface;
19
20
class InstallCommand extends Command
21
{
22
    const GENERATED_CONFIG_PATH = 'config/params/generated_config.php';
23
24
    /**
25
     * @var InputInterface
26
     */
27
    private $input;
28
    /**
29
     * @var OutputInterface
30
     */
31
    private $output;
32
    /**
33
     * @var QuestionHelper
34
     */
35
    private $questionHelper;
36
    /**
37
     * @var ProcessHelper
38
     */
39
    private $processHelper;
40
    /**
41
     * @var WriterInterface
42
     */
43
    private $configWriter;
44
    /**
45
     * @var Filesystem
46
     */
47
    private $filesystem;
48
    /**
49
     * @var ConfigCustomizerPluginManagerInterface
50
     */
51
    private $configCustomizers;
52
    /**
53
     * @var bool
54
     */
55
    private $isUpdate;
56
57
    /**
58
     * InstallCommand constructor.
59
     * @param WriterInterface $configWriter
60
     * @param Filesystem $filesystem
61
     * @param bool $isUpdate
62
     * @throws LogicException
63
     */
64 5
    public function __construct(
65
        WriterInterface $configWriter,
66
        Filesystem $filesystem,
67
        ConfigCustomizerPluginManagerInterface $configCustomizers,
68
        $isUpdate = false
69
    ) {
70 5
        parent::__construct();
71 5
        $this->configWriter = $configWriter;
72 5
        $this->isUpdate = $isUpdate;
73 5
        $this->filesystem = $filesystem;
74 5
        $this->configCustomizers = $configCustomizers;
75 5
    }
76
77 5
    public function configure()
78
    {
79
        $this
80 5
            ->setName('shlink:install')
81 5
            ->setDescription('Installs or updates Shlink');
82 5
    }
83
84 4
    public function execute(InputInterface $input, OutputInterface $output)
85
    {
86 4
        $this->input = $input;
87 4
        $this->output = $output;
88 4
        $this->questionHelper = $this->getHelper('question');
89 4
        $this->processHelper = $this->getHelper('process');
90
91 4
        $output->writeln([
92 4
            '<info>Welcome to Shlink!!</info>',
93
            'This will guide you through the installation process.',
94
        ]);
95
96
        // Check if a cached config file exists and drop it if so
97 4
        if ($this->filesystem->exists('data/cache/app_config.php')) {
98 2
            $output->write('Deleting old cached config...');
99
            try {
100 2
                $this->filesystem->remove('data/cache/app_config.php');
101 1
                $output->writeln(' <info>Success</info>');
102 1
            } catch (IOException $e) {
103 1
                $output->writeln(
104
                    ' <error>Failed!</error> You will have to manually delete the data/cache/app_config.php file to get'
105 1
                    . ' new config applied.'
106
                );
107 1
                if ($output->isVerbose()) {
108
                    $this->getApplication()->renderException($e, $output);
109
                }
110 1
                return;
111
            }
112
        }
113
114
        // If running update command, ask the user to import previous config
115 3
        $config = $this->isUpdate ? $this->importConfig() : new CustomizableAppConfig();
116
117
        // Ask for custom config params
118
        foreach ([
119 3
            Plugin\DatabaseConfigCustomizerPlugin::class,
120
            Plugin\UrlShortenerConfigCustomizerPlugin::class,
121
            Plugin\LanguageConfigCustomizerPlugin::class,
122
            Plugin\ApplicationConfigCustomizerPlugin::class,
123
        ] as $pluginName) {
124
            /** @var Plugin\ConfigCustomizerPluginInterface $configCustomizer */
125 3
            $configCustomizer = $this->configCustomizers->get($pluginName);
126 3
            $configCustomizer->process($input, $output, $config);
127
        }
128
129
        // Generate config params files
130 3
        $this->configWriter->toFile(self::GENERATED_CONFIG_PATH, $config->getArrayCopy(), false);
131 3
        $output->writeln(['<info>Custom configuration properly generated!</info>', '']);
132
133
        // If current command is not update, generate database
134 3
        if (!  $this->isUpdate) {
135 2
            $this->output->writeln('Initializing database...');
136 2
            if (! $this->runCommand(
137 2
                'php vendor/bin/doctrine.php orm:schema-tool:create',
138 2
                'Error generating database.'
139
            )) {
140
                return;
141
            }
142
        }
143
144
        // Run database migrations
145 3
        $output->writeln('Updating database...');
146 3
        if (! $this->runCommand('php vendor/bin/doctrine-migrations migrations:migrate', 'Error updating database.')) {
147
            return;
148
        }
149
150
        // Generate proxies
151 3
        $output->writeln('Generating proxies...');
152 3
        if (! $this->runCommand('php vendor/bin/doctrine.php orm:generate-proxies', 'Error generating proxies.')) {
153
            return;
154
        }
155 3
    }
156
157
    /**
158
     * @return CustomizableAppConfig
159
     * @throws RuntimeException
160
     */
161 1
    private function importConfig()
162
    {
163 1
        $config = new CustomizableAppConfig();
164
165
        // Ask the user if he/she wants to import an older configuration
166 1
        $importConfig = $this->questionHelper->ask($this->input, $this->output, new ConfirmationQuestion(
167 1
            '<question>Do you want to import previous configuration? (Y/n):</question> '
168
        ));
169 1
        if (! $importConfig) {
170
            return $config;
171
        }
172
173
        // Ask the user for the older shlink path
174 1
        $keepAsking = true;
175
        do {
176 1
            $config->setImportedInstallationPath($this->ask(
177 1
                'Previous shlink installation path from which to import config'
178
            ));
179 1
            $configFile = $config->getImportedInstallationPath() . '/' . self::GENERATED_CONFIG_PATH;
180 1
            $configExists = $this->filesystem->exists($configFile);
181
182 1
            if (! $configExists) {
183 1
                $keepAsking = $this->questionHelper->ask($this->input, $this->output, new ConfirmationQuestion(
184
                    'Provided path does not seem to be a valid shlink root path. '
185 1
                    . '<question>Do you want to try another path? (Y/n):</question> '
186
                ));
187
            }
188 1
        } while (! $configExists && $keepAsking);
189
190
        // If after some retries the user has chosen not to test another path, return
191 1
        if (! $configExists) {
192
            return $config;
193
        }
194
195
        // Read the config file
196 1
        $config->exchangeArray(include $configFile);
197 1
        return $config;
198
    }
199
200
    /**
201
     * @param string $text
202
     * @param string|null $default
203
     * @param bool $allowEmpty
204
     * @return string
205
     * @throws RuntimeException
206
     */
207 1
    private function ask($text, $default = null, $allowEmpty = false)
208
    {
209 1
        if ($default !== null) {
210
            $text .= ' (defaults to ' . $default . ')';
211
        }
212
        do {
213 1
            $value = $this->questionHelper->ask($this->input, $this->output, new Question(
214 1
                '<question>' . $text . ':</question> ',
215 1
                $default
216
            ));
217 1
            if (empty($value) && ! $allowEmpty) {
218
                $this->output->writeln('<error>Value can\'t be empty</error>');
219
            }
220 1
        } while (empty($value) && $default === null && ! $allowEmpty);
221
222 1
        return $value;
223
    }
224
225
    /**
226
     * @param string $command
227
     * @param string $errorMessage
228
     * @return bool
229
     */
230 3
    private function runCommand($command, $errorMessage)
231
    {
232 3
        $process = $this->processHelper->run($this->output, $command);
233 3
        if ($process->isSuccessful()) {
234 3
            $this->output->writeln('    <info>Success!</info>');
235 3
            return true;
236
        }
237
238
        if ($this->output->isVerbose()) {
239
            return false;
240
        }
241
242
        $this->output->writeln(
243
            '    <error>' . $errorMessage . '</error>  Run this command with -vvv to see specific error info.'
244
        );
245
        return false;
246
    }
247
}
248