Completed
Push — master ( c32e20...5b9784 )
by Alejandro
15s
created

InstallCommand::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 5
dl 0
loc 14
rs 9.7998
c 0
b 0
f 0
ccs 8
cts 8
cp 1
crap 2
1
<?php
2
declare(strict_types=1);
3
4
namespace Shlinkio\Shlink\CLI\Command\Install;
5
6
use Psr\Container\ContainerExceptionInterface;
7
use Psr\Container\NotFoundExceptionInterface;
8
use Shlinkio\Shlink\CLI\Install\ConfigCustomizerManagerInterface;
9
use Shlinkio\Shlink\CLI\Install\Plugin;
10
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
11
use Symfony\Component\Console\Command\Command;
12
use Symfony\Component\Console\Exception\InvalidArgumentException;
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
24
class InstallCommand extends Command
25
{
26
    const GENERATED_CONFIG_PATH = 'config/params/generated_config.php';
27
28
    /**
29
     * @var SymfonyStyle
30
     */
31
    private $io;
32
    /**
33
     * @var ProcessHelper
34
     */
35
    private $processHelper;
36
    /**
37
     * @var WriterInterface
38
     */
39
    private $configWriter;
40
    /**
41
     * @var Filesystem
42
     */
43
    private $filesystem;
44
    /**
45
     * @var ConfigCustomizerManagerInterface
46
     */
47
    private $configCustomizers;
48
    /**
49
     * @var bool
50
     */
51
    private $isUpdate;
52
    /**
53
     * @var PhpExecutableFinder
54
     */
55
    private $phpFinder;
56
    /**
57
     * @var string|bool
58
     */
59
    private $phpBinary;
60
61
    /**
62
     * InstallCommand constructor.
63
     * @param WriterInterface $configWriter
64
     * @param Filesystem $filesystem
65
     * @param ConfigCustomizerManagerInterface $configCustomizers
66
     * @param bool $isUpdate
67
     * @param PhpExecutableFinder|null $phpFinder
68
     * @throws LogicException
69
     */
70 5
    public function __construct(
71
        WriterInterface $configWriter,
72
        Filesystem $filesystem,
73
        ConfigCustomizerManagerInterface $configCustomizers,
74
        bool $isUpdate = false,
75
        PhpExecutableFinder $phpFinder = null
76
    ) {
77 5
        parent::__construct();
78 5
        $this->configWriter = $configWriter;
79 5
        $this->isUpdate = $isUpdate;
80 5
        $this->filesystem = $filesystem;
81 5
        $this->configCustomizers = $configCustomizers;
82 5
        $this->phpFinder = $phpFinder ?: new PhpExecutableFinder();
83 5
    }
84
85 5
    protected function configure(): void
86
    {
87
        $this
88 5
            ->setName('shlink:install')
89 5
            ->setDescription('Installs or updates Shlink');
90 5
    }
91
92
    /**
93
     * @param InputInterface $input
94
     * @param OutputInterface $output
95
     * @return void
96
     * @throws ContainerExceptionInterface
97
     * @throws NotFoundExceptionInterface
98
     */
99 4
    protected function execute(InputInterface $input, OutputInterface $output): void
100
    {
101 4
        $this->io = new SymfonyStyle($input, $output);
102
103 4
        $this->io->writeln([
104 4
            '<info>Welcome to Shlink!!</info>',
105
            'This will guide you through the installation process.',
106
        ]);
107
108
        // Check if a cached config file exists and drop it if so
109 4
        if ($this->filesystem->exists('data/cache/app_config.php')) {
110 2
            $this->io->write('Deleting old cached config...');
111
            try {
112 2
                $this->filesystem->remove('data/cache/app_config.php');
113 1
                $this->io->writeln(' <info>Success</info>');
114 1
            } catch (IOException $e) {
0 ignored issues
show
Bug introduced by
The class Symfony\Component\Filesystem\Exception\IOException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
115 1
                $this->io->error(
116
                    'Failed! You will have to manually delete the data/cache/app_config.php file to'
117 1
                    . ' get new config applied.'
118
                );
119 1
                if ($this->io->isVerbose()) {
120
                    $this->getApplication()->renderException($e, $output);
121
                }
122 1
                return;
123
            }
124
        }
125
126
        // If running update command, ask the user to import previous config
127 3
        $config = $this->isUpdate ? $this->importConfig() : new CustomizableAppConfig();
128
129
        // Ask for custom config params
130
        foreach ([
131 3
            Plugin\DatabaseConfigCustomizer::class,
132
            Plugin\UrlShortenerConfigCustomizer::class,
133
            Plugin\LanguageConfigCustomizer::class,
134
            Plugin\ApplicationConfigCustomizer::class,
135
        ] as $pluginName) {
136
            /** @var Plugin\ConfigCustomizerInterface $configCustomizer */
137 3
            $configCustomizer = $this->configCustomizers->get($pluginName);
138 3
            $configCustomizer->process($this->io, $config);
139
        }
140
141
        // Generate config params files
142 3
        $this->configWriter->toFile(self::GENERATED_CONFIG_PATH, $config->getArrayCopy(), false);
143 3
        $this->io->writeln(['<info>Custom configuration properly generated!</info>', '']);
144
145
        // If current command is not update, generate database
146 3
        if (!  $this->isUpdate) {
147 2
            $this->io->write('Initializing database...');
148 2
            if (! $this->runPhpCommand(
149 2
                'vendor/doctrine/orm/bin/doctrine.php orm:schema-tool:create',
150 2
                'Error generating database.',
151 2
                $output
152
            )) {
153
                return;
154
            }
155
        }
156
157
        // Run database migrations
158 3
        $this->io->write('Updating database...');
159 3
        if (! $this->runPhpCommand(
160 3
            'vendor/doctrine/migrations/bin/doctrine-migrations.php migrations:migrate',
161 3
            'Error updating database.',
162 3
            $output
163
        )) {
164
            return;
165
        }
166
167
        // Generate proxies
168 3
        $this->io->write('Generating proxies...');
169 3
        if (! $this->runPhpCommand(
170 3
            'vendor/doctrine/orm/bin/doctrine.php orm:generate-proxies',
171 3
            'Error generating proxies.',
172 3
            $output
173
        )) {
174
            return;
175
        }
176
177 3
        $this->io->success('Installation complete!');
178 3
    }
179
180
    /**
181
     * @return CustomizableAppConfig
182
     * @throws RuntimeException
183
     */
184 1
    private function importConfig(): CustomizableAppConfig
185
    {
186 1
        $config = new CustomizableAppConfig();
187
188
        // Ask the user if he/she wants to import an older configuration
189 1
        $importConfig = $this->io->confirm('Do you want to import configuration from previous installation?');
190 1
        if (! $importConfig) {
191
            return $config;
192
        }
193
194
        // Ask the user for the older shlink path
195 1
        $keepAsking = true;
196
        do {
197 1
            $config->setImportedInstallationPath($this->io->ask(
198 1
                'Previous shlink installation path from which to import config'
199
            ));
200 1
            $configFile = $config->getImportedInstallationPath() . '/' . self::GENERATED_CONFIG_PATH;
201 1
            $configExists = $this->filesystem->exists($configFile);
202
203 1
            if (! $configExists) {
204 1
                $keepAsking = $this->io->confirm(
205 1
                    'Provided path does not seem to be a valid shlink root path. Do you want to try another path?'
206
                );
207
            }
208 1
        } while (! $configExists && $keepAsking);
209
210
        // If after some retries the user has chosen not to test another path, return
211 1
        if (! $configExists) {
212
            return $config;
213
        }
214
215
        // Read the config file
216 1
        $config->exchangeArray(include $configFile);
217 1
        return $config;
218
    }
219
220
    /**
221
     * @param string $command
222
     * @param string $errorMessage
223
     * @param OutputInterface $output
224
     * @return bool
225
     * @throws LogicException
226
     * @throws InvalidArgumentException
227
     */
228 3
    private function runPhpCommand($command, $errorMessage, OutputInterface $output): bool
229
    {
230 3
        if ($this->processHelper === null) {
231 3
            $this->processHelper = $this->getHelper('process');
232
        }
233
234 3
        if ($this->phpBinary === null) {
235 3
            $this->phpBinary = $this->phpFinder->find(false) ?: 'php';
236
        }
237
238 3
        $this->io->write(
239 3
            ' <options=bold>[Running "' . sprintf('%s %s', $this->phpBinary, $command) . '"]</> ',
240 3
            false,
241 3
            OutputInterface::VERBOSITY_VERBOSE
242
        );
243 3
        $process = $this->processHelper->run($output, sprintf('%s %s', $this->phpBinary, $command));
244 3
        if ($process->isSuccessful()) {
245 3
            $this->io->writeln(' <info>Success!</info>');
246 3
            return true;
247
        }
248
249
        if (! $this->io->isVerbose()) {
250
            $this->io->error($errorMessage . '  Run this command with -vvv to see specific error info.');
251
        }
252
253
        return false;
254
    }
255
}
256