Completed
Push — master ( 235830...d4758b )
by Alejandro
15s queued 11s
created

InstallCommand::configure()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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