Completed
Pull Request — master (#246)
by Alejandro
05:59
created

InstallCommand::importConfig()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 39
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 5.0214

Importance

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