|
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 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
|
|
|
public 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
|
|
|
public function __construct( |
|
71
|
|
|
WriterInterface $configWriter, |
|
72
|
|
|
Filesystem $filesystem, |
|
73
|
|
|
ConfigCustomizerManagerInterface $configCustomizers, |
|
74
|
|
|
bool $isUpdate = false, |
|
75
|
|
|
PhpExecutableFinder $phpFinder = null |
|
76
|
|
|
) { |
|
77
|
|
|
parent::__construct(); |
|
78
|
|
|
$this->configWriter = $configWriter; |
|
79
|
|
|
$this->isUpdate = $isUpdate; |
|
80
|
|
|
$this->filesystem = $filesystem; |
|
81
|
|
|
$this->configCustomizers = $configCustomizers; |
|
82
|
|
|
$this->phpFinder = $phpFinder ?: new PhpExecutableFinder(); |
|
83
|
|
|
} |
|
84
|
|
|
|
|
85
|
|
|
protected function configure(): void |
|
86
|
|
|
{ |
|
87
|
|
|
$this |
|
88
|
|
|
->setName('shlink:install') |
|
89
|
|
|
->setDescription('Installs or updates Shlink'); |
|
90
|
|
|
} |
|
91
|
|
|
|
|
92
|
|
|
/** |
|
93
|
|
|
* @param InputInterface $input |
|
94
|
|
|
* @param OutputInterface $output |
|
95
|
|
|
* @return void |
|
96
|
|
|
* @throws ContainerExceptionInterface |
|
97
|
|
|
* @throws NotFoundExceptionInterface |
|
98
|
|
|
*/ |
|
99
|
|
|
protected function execute(InputInterface $input, OutputInterface $output): void |
|
100
|
|
|
{ |
|
101
|
|
|
$this->io = new SymfonyStyle($input, $output); |
|
102
|
|
|
|
|
103
|
|
|
$this->io->writeln([ |
|
104
|
|
|
'<info>Welcome to Shlink!!</info>', |
|
105
|
|
|
'This tool will guide you through the installation process.', |
|
106
|
|
|
]); |
|
107
|
|
|
|
|
108
|
|
|
// Check if a cached config file exists and drop it if so |
|
109
|
|
|
if ($this->filesystem->exists('data/cache/app_config.php')) { |
|
110
|
|
|
$this->io->write('Deleting old cached config...'); |
|
111
|
|
|
try { |
|
112
|
|
|
$this->filesystem->remove('data/cache/app_config.php'); |
|
113
|
|
|
$this->io->writeln(' <info>Success</info>'); |
|
114
|
|
|
} catch (IOException $e) { |
|
|
|
|
|
|
115
|
|
|
$this->io->error( |
|
116
|
|
|
'Failed! You will have to manually delete the data/cache/app_config.php file to' |
|
117
|
|
|
. ' get new config applied.' |
|
118
|
|
|
); |
|
119
|
|
|
if ($this->io->isVerbose()) { |
|
120
|
|
|
$this->getApplication()->renderException($e, $output); |
|
121
|
|
|
} |
|
122
|
|
|
return; |
|
123
|
|
|
} |
|
124
|
|
|
} |
|
125
|
|
|
|
|
126
|
|
|
// If running update command, ask the user to import previous config |
|
127
|
|
|
$config = $this->isUpdate ? $this->importConfig() : new CustomizableAppConfig(); |
|
128
|
|
|
|
|
129
|
|
|
// Ask for custom config params |
|
130
|
|
|
foreach ([ |
|
131
|
|
|
Plugin\DatabaseConfigCustomizer::class, |
|
132
|
|
|
Plugin\UrlShortenerConfigCustomizer::class, |
|
133
|
|
|
Plugin\LanguageConfigCustomizer::class, |
|
134
|
|
|
Plugin\ApplicationConfigCustomizer::class, |
|
135
|
|
|
] as $pluginName) { |
|
136
|
|
|
/** @var Plugin\ConfigCustomizerInterface $configCustomizer */ |
|
137
|
|
|
$configCustomizer = $this->configCustomizers->get($pluginName); |
|
138
|
|
|
$configCustomizer->process($this->io, $config); |
|
139
|
|
|
} |
|
140
|
|
|
|
|
141
|
|
|
// Generate config params files |
|
142
|
|
|
$this->configWriter->toFile(self::GENERATED_CONFIG_PATH, $config->getArrayCopy(), false); |
|
143
|
|
|
$this->io->writeln(['<info>Custom configuration properly generated!</info>', '']); |
|
144
|
|
|
|
|
145
|
|
|
// If current command is not update, generate database |
|
146
|
|
|
if (! $this->isUpdate) { |
|
147
|
|
|
$this->io->write('Initializing database...'); |
|
148
|
|
|
if (! $this->runPhpCommand( |
|
149
|
|
|
'vendor/doctrine/orm/bin/doctrine.php orm:schema-tool:create', |
|
150
|
|
|
'Error generating database.', |
|
151
|
|
|
$output |
|
152
|
|
|
)) { |
|
153
|
|
|
return; |
|
154
|
|
|
} |
|
155
|
|
|
} |
|
156
|
|
|
|
|
157
|
|
|
// Run database migrations |
|
158
|
|
|
$this->io->write('Updating database...'); |
|
159
|
|
|
if (! $this->runPhpCommand( |
|
160
|
|
|
'vendor/doctrine/migrations/bin/doctrine-migrations.php migrations:migrate', |
|
161
|
|
|
'Error updating database.', |
|
162
|
|
|
$output |
|
163
|
|
|
)) { |
|
164
|
|
|
return; |
|
165
|
|
|
} |
|
166
|
|
|
|
|
167
|
|
|
// Generate proxies |
|
168
|
|
|
$this->io->write('Generating proxies...'); |
|
169
|
|
|
if (! $this->runPhpCommand( |
|
170
|
|
|
'vendor/doctrine/orm/bin/doctrine.php orm:generate-proxies', |
|
171
|
|
|
'Error generating proxies.', |
|
172
|
|
|
$output |
|
173
|
|
|
)) { |
|
174
|
|
|
return; |
|
175
|
|
|
} |
|
176
|
|
|
|
|
177
|
|
|
$this->io->success('Installation complete!'); |
|
178
|
|
|
} |
|
179
|
|
|
|
|
180
|
|
|
/** |
|
181
|
|
|
* @return CustomizableAppConfig |
|
182
|
|
|
* @throws RuntimeException |
|
183
|
|
|
*/ |
|
184
|
|
|
private function importConfig(): CustomizableAppConfig |
|
185
|
|
|
{ |
|
186
|
|
|
$config = new CustomizableAppConfig(); |
|
187
|
|
|
|
|
188
|
|
|
// Ask the user if he/she wants to import an older configuration |
|
189
|
|
|
$importConfig = $this->io->confirm('Do you want to import configuration from previous installation?'); |
|
190
|
|
|
if (! $importConfig) { |
|
191
|
|
|
return $config; |
|
192
|
|
|
} |
|
193
|
|
|
|
|
194
|
|
|
// Ask the user for the older shlink path |
|
195
|
|
|
$keepAsking = true; |
|
196
|
|
|
do { |
|
197
|
|
|
$config->setImportedInstallationPath($this->io->ask( |
|
198
|
|
|
'Previous shlink installation path from which to import config' |
|
199
|
|
|
)); |
|
200
|
|
|
$configFile = $config->getImportedInstallationPath() . '/' . self::GENERATED_CONFIG_PATH; |
|
201
|
|
|
$configExists = $this->filesystem->exists($configFile); |
|
202
|
|
|
|
|
203
|
|
|
if (! $configExists) { |
|
204
|
|
|
$keepAsking = $this->io->confirm( |
|
205
|
|
|
'Provided path does not seem to be a valid shlink root path. Do you want to try another path?' |
|
206
|
|
|
); |
|
207
|
|
|
} |
|
208
|
|
|
} while (! $configExists && $keepAsking); |
|
209
|
|
|
|
|
210
|
|
|
// If after some retries the user has chosen not to test another path, return |
|
211
|
|
|
if (! $configExists) { |
|
212
|
|
|
return $config; |
|
213
|
|
|
} |
|
214
|
|
|
|
|
215
|
|
|
// Read the config file |
|
216
|
|
|
$config->exchangeArray(include $configFile); |
|
217
|
|
|
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
|
|
|
private function runPhpCommand($command, $errorMessage, OutputInterface $output): bool |
|
229
|
|
|
{ |
|
230
|
|
|
if ($this->processHelper === null) { |
|
231
|
|
|
$this->processHelper = $this->getHelper('process'); |
|
232
|
|
|
} |
|
233
|
|
|
|
|
234
|
|
|
if ($this->phpBinary === null) { |
|
235
|
|
|
$this->phpBinary = $this->phpFinder->find(false) ?: 'php'; |
|
236
|
|
|
} |
|
237
|
|
|
|
|
238
|
|
|
$this->io->write( |
|
239
|
|
|
' <options=bold>[Running "' . sprintf('%s %s', $this->phpBinary, $command) . '"]</> ', |
|
240
|
|
|
false, |
|
241
|
|
|
OutputInterface::VERBOSITY_VERBOSE |
|
242
|
|
|
); |
|
243
|
|
|
$process = $this->processHelper->run($output, sprintf('%s %s', $this->phpBinary, $command)); |
|
244
|
|
|
if ($process->isSuccessful()) { |
|
245
|
|
|
$this->io->writeln(' <info>Success!</info>'); |
|
246
|
|
|
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
|
|
|
|
Scrutinizer analyzes your
composer.json/composer.lockfile 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.