Completed
Pull Request — master (#340)
by Alejandro
05:57
created

InstallCommand   A

Complexity

Total Complexity 25

Size/Duplication

Total Lines 220
Duplicated Lines 0 %

Test Coverage

Coverage 89.01%

Importance

Changes 0
Metric Value
eloc 107
dl 0
loc 220
rs 10
c 0
b 0
f 0
ccs 81
cts 91
cp 0.8901
wmc 25

5 Methods

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