|
1
|
|
|
<?php |
|
2
|
|
|
|
|
3
|
|
|
namespace Tworzenieweb\SqlProvisioner\Command; |
|
4
|
|
|
|
|
5
|
|
|
use RuntimeException; |
|
6
|
|
|
use Symfony\Component\Console\Command\Command; |
|
7
|
|
|
use Symfony\Component\Console\Input\InputArgument; |
|
8
|
|
|
use Symfony\Component\Console\Input\InputInterface; |
|
9
|
|
|
use Symfony\Component\Console\Input\InputOption; |
|
10
|
|
|
use Symfony\Component\Console\Output\OutputInterface; |
|
11
|
|
|
use Symfony\Component\Console\Style\SymfonyStyle; |
|
12
|
|
|
use Symfony\Component\Finder\SplFileInfo; |
|
13
|
|
|
use Tworzenieweb\SqlProvisioner\Database\Connection; |
|
14
|
|
|
use Tworzenieweb\SqlProvisioner\Database\Exception as DatabaseException; |
|
15
|
|
|
use Tworzenieweb\SqlProvisioner\Database\Executor; |
|
16
|
|
|
use Tworzenieweb\SqlProvisioner\Filesystem\Exception; |
|
17
|
|
|
use Tworzenieweb\SqlProvisioner\Filesystem\WorkingDirectory; |
|
18
|
|
|
use Tworzenieweb\SqlProvisioner\Formatter\Sql; |
|
19
|
|
|
use Tworzenieweb\SqlProvisioner\Model\Candidate; |
|
20
|
|
|
use Tworzenieweb\SqlProvisioner\Model\CandidateBuilder; |
|
21
|
|
|
use Tworzenieweb\SqlProvisioner\Processor\CandidateProcessor; |
|
22
|
|
|
use Tworzenieweb\SqlProvisioner\Table\DataRowsBuilder; |
|
23
|
|
|
|
|
24
|
|
|
/** |
|
25
|
|
|
* @author Luke Adamczewski |
|
26
|
|
|
* @package Tworzenieweb\SqlProvisioner\Command |
|
27
|
|
|
*/ |
|
28
|
|
|
class ProvisionCommand extends Command |
|
29
|
|
|
{ |
|
30
|
|
|
const HELP_MESSAGE = <<<'EOF' |
|
31
|
|
|
The <info>%command.name% [path-to-folder]</info> command will scan the content of [path-to-folder] directory. |
|
32
|
|
|
|
|
33
|
|
|
The script will look for <info>.env</info> file containing connection information in format: |
|
34
|
|
|
<comment> |
|
35
|
|
|
DATABASE_USER=[user] |
|
36
|
|
|
DATABASE_PASSWORD=[password] |
|
37
|
|
|
DATABASE_HOST=[host] |
|
38
|
|
|
DATABASE_PORT=[port] |
|
39
|
|
|
DATABASE_NAME=[database] |
|
40
|
|
|
PROVISIONING_TABLE=changelog_database_deployments |
|
41
|
|
|
PROVISIONING_TABLE_CANDIDATE_NUMBER_COLUMN=deploy_script_number |
|
42
|
|
|
</comment> |
|
43
|
|
|
|
|
44
|
|
|
If you want to create initial .env use <info>--init</info> |
|
45
|
|
|
|
|
46
|
|
|
<info>%command.name% --init [path-to-folder]</info> |
|
47
|
|
|
|
|
48
|
|
|
The next step is searching for sql files and trying to queue them in numerical order. |
|
49
|
|
|
First n-th digits of a filename will be treated as candidate number. |
|
50
|
|
|
This will be used then to check in database if a certain file was already deployed (PROVISIONING_TABLE_CANDIDATE_NUMBER_COLUMN). |
|
51
|
|
|
Before the insert, it will print the formatted output of a file and result of internal syntax check. |
|
52
|
|
|
Then you can either skip or execute each. |
|
53
|
|
|
|
|
54
|
|
|
If you would like to skip already provisioned candidates use <info>--skip-provisioned</info> |
|
55
|
|
|
EOF; |
|
56
|
|
|
|
|
57
|
|
|
/** @var int */ |
|
58
|
|
|
private $candidateIndexValue = 1; |
|
59
|
|
|
|
|
60
|
|
|
/** @var Candidate[] */ |
|
61
|
|
|
private $workingDirectoryCandidates = []; |
|
62
|
|
|
|
|
63
|
|
|
/** @var Sql */ |
|
64
|
|
|
private $sqlFormatter; |
|
65
|
|
|
|
|
66
|
|
|
/** @var WorkingDirectory */ |
|
67
|
|
|
private $workingDirectory; |
|
68
|
|
|
|
|
69
|
|
|
/** @var SymfonyStyle */ |
|
70
|
|
|
private $io; |
|
71
|
|
|
|
|
72
|
|
|
/** @var Connection */ |
|
73
|
|
|
private $connection; |
|
74
|
|
|
|
|
75
|
|
|
/** @var CandidateProcessor */ |
|
76
|
|
|
private $processor; |
|
77
|
|
|
|
|
78
|
|
|
/** @var Executor */ |
|
79
|
|
|
private $executor; |
|
80
|
|
|
|
|
81
|
|
|
/** @var boolean */ |
|
82
|
|
|
private $skipProvisionedCandidates = false; |
|
83
|
|
|
|
|
84
|
|
|
/** @var CandidateBuilder */ |
|
85
|
|
|
private $candidateBuilder; |
|
86
|
|
|
|
|
87
|
|
|
/** @var DataRowsBuilder */ |
|
88
|
|
|
private $dataRowsBuilder; |
|
89
|
|
|
|
|
90
|
|
|
/** @var bool */ |
|
91
|
|
|
private $hasQueuedCandidates = false; |
|
92
|
|
|
|
|
93
|
|
|
/** @var integer */ |
|
94
|
|
|
private $queuedCandidatesCount = 0; |
|
95
|
|
|
|
|
96
|
|
|
/** @var array */ |
|
97
|
|
|
private $errorMessages = []; |
|
98
|
|
|
|
|
99
|
|
|
/** @var integer */ |
|
100
|
|
|
private $startTimestamp; |
|
101
|
|
|
|
|
102
|
|
|
|
|
103
|
|
|
/** |
|
104
|
|
|
* @param string $name |
|
105
|
|
|
* @param WorkingDirectory $workingDirectory |
|
106
|
|
|
* @param Connection $connection |
|
107
|
|
|
* @param Sql $sqlFormatter |
|
108
|
|
|
* @param CandidateProcessor $processor |
|
109
|
|
|
* @param CandidateBuilder $candidateBuilder |
|
110
|
|
|
* @param DataRowsBuilder $dataRowsBuilder |
|
111
|
|
|
* @param Executor $executor |
|
112
|
|
|
*/ |
|
113
|
1 |
|
public function __construct( |
|
114
|
|
|
$name, |
|
115
|
|
|
WorkingDirectory $workingDirectory, |
|
116
|
|
|
Connection $connection, |
|
117
|
|
|
Sql $sqlFormatter, |
|
118
|
|
|
CandidateProcessor $processor, |
|
119
|
|
|
CandidateBuilder $candidateBuilder, |
|
120
|
|
|
DataRowsBuilder $dataRowsBuilder, |
|
121
|
|
|
Executor $executor |
|
122
|
|
|
) |
|
123
|
|
|
{ |
|
124
|
1 |
|
$this->workingDirectory = $workingDirectory; |
|
125
|
1 |
|
$this->connection = $connection; |
|
126
|
1 |
|
$this->sqlFormatter = $sqlFormatter; |
|
127
|
1 |
|
$this->processor = $processor; |
|
128
|
1 |
|
$this->candidateBuilder = $candidateBuilder; |
|
129
|
1 |
|
$this->dataRowsBuilder = $dataRowsBuilder; |
|
130
|
1 |
|
$this->executor = $executor; |
|
131
|
|
|
|
|
132
|
1 |
|
parent::__construct($name); |
|
133
|
1 |
|
} |
|
134
|
|
|
|
|
135
|
|
|
|
|
136
|
1 |
|
protected function configure() |
|
137
|
|
|
{ |
|
138
|
1 |
|
$this |
|
139
|
1 |
|
->setDescription('Execute the content of *.sql files from given') |
|
140
|
1 |
|
->setHelp(self::HELP_MESSAGE); |
|
141
|
1 |
|
$this->addOption('init', null, InputOption::VALUE_NONE, 'Initialize .env in given directory'); |
|
142
|
1 |
|
$this->addOption( |
|
143
|
1 |
|
'skip-provisioned', |
|
144
|
1 |
|
null, |
|
145
|
1 |
|
InputOption::VALUE_NONE, |
|
146
|
|
|
'Skip provisioned candidates from printing' |
|
147
|
1 |
|
); |
|
148
|
1 |
|
$this->addArgument('path', InputArgument::REQUIRED, 'Path to dbdeploys folder'); |
|
149
|
1 |
|
} |
|
150
|
|
|
|
|
151
|
|
|
|
|
152
|
|
|
/** |
|
153
|
|
|
* @param InputInterface $input |
|
154
|
|
|
* @param OutputInterface $output |
|
155
|
|
|
* @return int |
|
156
|
|
|
*/ |
|
157
|
|
|
protected function execute(InputInterface $input, OutputInterface $output) |
|
158
|
|
|
{ |
|
159
|
|
|
$this->start($input, $output); |
|
160
|
|
|
$this->io->section('Working directory processing'); |
|
161
|
|
|
|
|
162
|
|
|
if ($input->getOption('skip-provisioned')) { |
|
163
|
|
|
$this->skipProvisionedCandidates = true; |
|
164
|
|
|
$this->io->warning('Hiding of provisioned candidates ENABLED'); |
|
165
|
|
|
} |
|
166
|
|
|
|
|
167
|
|
|
$this->processWorkingDirectory($input); |
|
168
|
|
|
$this->processCandidates(); |
|
169
|
|
|
$this->finish(); |
|
170
|
|
|
|
|
171
|
|
|
return 0; |
|
172
|
|
|
} |
|
173
|
|
|
|
|
174
|
|
|
|
|
175
|
|
|
/** |
|
176
|
|
|
* @param InputInterface $input |
|
177
|
|
|
* @param OutputInterface $output |
|
178
|
|
|
*/ |
|
179
|
|
|
protected function start(InputInterface $input, OutputInterface $output) |
|
180
|
|
|
{ |
|
181
|
|
|
$this->startTimestamp = time(); |
|
182
|
|
|
$this->io = new SymfonyStyle($input, $output); |
|
183
|
|
|
$this->io->title('SQL Provisioner'); |
|
184
|
|
|
$this->io->block(sprintf('Provisioning started at %s', date('Y-m-d H:i:s'))); |
|
185
|
|
|
} |
|
186
|
|
|
|
|
187
|
|
|
|
|
188
|
|
|
protected function fetchCandidates() |
|
189
|
|
|
{ |
|
190
|
|
|
$this->iterateOverWorkingDirectory(); |
|
191
|
|
|
|
|
192
|
|
|
if (!empty($this->errorMessages)) { |
|
193
|
|
|
$this->showSyntaxErrors(); |
|
194
|
|
|
} |
|
195
|
|
|
|
|
196
|
|
|
if (false === $this->hasQueuedCandidates) { |
|
197
|
|
|
$this->io->block('All candidates scripts were executed already.'); |
|
198
|
|
|
$this->finish(); |
|
199
|
|
|
} |
|
200
|
|
|
} |
|
201
|
|
|
|
|
202
|
|
|
|
|
203
|
|
|
/** |
|
204
|
|
|
* @param SplFileInfo $candidateFile |
|
205
|
|
|
*/ |
|
206
|
|
|
protected function processCandidateFile($candidateFile) |
|
207
|
|
|
{ |
|
208
|
|
|
$candidate = $this->candidateBuilder->build($candidateFile); |
|
209
|
|
|
array_push($this->workingDirectoryCandidates, $candidate); |
|
210
|
|
|
|
|
211
|
|
|
if ($this->processor->isValid($candidate)) { |
|
212
|
|
|
$candidate->markAsQueued(); |
|
213
|
|
|
$candidate->setIndex($this->candidateIndexValue++); |
|
214
|
|
|
$this->hasQueuedCandidates = true; |
|
215
|
|
|
$this->queuedCandidatesCount++; |
|
216
|
|
|
} else { |
|
217
|
|
|
$candidate->markAsIgnored($this->processor->getLastError()); |
|
218
|
|
|
$lastErrorMessage = $this->processor->getLastErrorMessage(); |
|
219
|
|
|
|
|
220
|
|
|
if (!empty($lastErrorMessage)) { |
|
221
|
|
|
array_push($this->errorMessages, $lastErrorMessage); |
|
222
|
|
|
} |
|
223
|
|
|
} |
|
224
|
|
|
} |
|
225
|
|
|
|
|
226
|
|
|
|
|
227
|
|
|
protected function iterateOverWorkingDirectory() |
|
228
|
|
|
{ |
|
229
|
|
|
foreach ($this->workingDirectory->getCandidates() as $candidateFile) { |
|
230
|
|
|
$this->processCandidateFile($candidateFile); |
|
231
|
|
|
} |
|
232
|
|
|
|
|
233
|
|
|
$this->io->text(sprintf('<info>%d</info> files found', count($this->workingDirectoryCandidates))); |
|
234
|
|
|
|
|
235
|
|
|
if (count($this->workingDirectoryCandidates) === 0) { |
|
236
|
|
|
throw Exception::noFilesInDirectory($this->workingDirectory); |
|
237
|
|
|
} |
|
238
|
|
|
} |
|
239
|
|
|
|
|
240
|
|
|
|
|
241
|
|
|
protected function showSyntaxErrors() |
|
242
|
|
|
{ |
|
243
|
|
|
$this->io->warning(sprintf('Detected %d syntax checking issues', count($this->errorMessages))); |
|
244
|
|
|
$this->printAllCandidates(); |
|
245
|
|
|
$this->io->writeln(sprintf('<error>%s</error>', implode("\n", $this->errorMessages))); |
|
246
|
|
|
$this->finish(); |
|
247
|
|
|
} |
|
248
|
|
|
|
|
249
|
|
|
|
|
250
|
|
|
/** |
|
251
|
|
|
* @param InputInterface $input |
|
252
|
|
|
*/ |
|
253
|
|
|
protected function processWorkingDirectory(InputInterface $input) |
|
254
|
|
|
{ |
|
255
|
|
|
$this->workingDirectory = $this->workingDirectory->cd($input->getArgument('path')); |
|
256
|
|
|
$this->loadOrCreateEnvironment($input); |
|
257
|
|
|
$this->io->success('DONE'); |
|
258
|
|
|
} |
|
259
|
|
|
|
|
260
|
|
|
|
|
261
|
|
|
/** |
|
262
|
|
|
* @param InputInterface $input |
|
263
|
|
|
*/ |
|
264
|
|
|
private function loadOrCreateEnvironment(InputInterface $input) |
|
265
|
|
|
{ |
|
266
|
|
|
if ($input->getOption('init')) { |
|
267
|
|
|
$this->workingDirectory->createEnvironmentFile(); |
|
268
|
|
|
$this->io->success(sprintf('Initial .env file created in %s', $this->workingDirectory)); |
|
269
|
|
|
die(0); |
|
|
|
|
|
|
270
|
|
|
} |
|
271
|
|
|
|
|
272
|
|
|
$this->workingDirectory->loadEnvironment(); |
|
273
|
|
|
} |
|
274
|
|
|
|
|
275
|
|
|
|
|
276
|
|
|
private function setConnectionParameters() |
|
|
|
|
|
|
277
|
|
|
{ |
|
278
|
|
|
$this->connection->useMysql($_ENV['DATABASE_HOST'], $_ENV['DATABASE_PORT'], $_ENV['DATABASE_NAME'], $_ENV['DATABASE_USER'], $_ENV['DATABASE_PASSWORD']); |
|
279
|
|
|
$this->connection->setProvisioningTable($_ENV['PROVISIONING_TABLE']); |
|
280
|
|
|
$this->connection->setCriteriaColumn($_ENV['PROVISIONING_TABLE_CANDIDATE_NUMBER_COLUMN']); |
|
281
|
|
|
|
|
282
|
|
|
$this->io->success(sprintf('Connection with `%s` established', $_ENV['DATABASE_NAME'])); |
|
283
|
|
|
} |
|
284
|
|
|
|
|
285
|
|
|
|
|
286
|
|
|
private function processCandidates() |
|
287
|
|
|
{ |
|
288
|
|
|
$this->io->newLine(2); |
|
289
|
|
|
$this->io->section('Candidates processing'); |
|
290
|
|
|
|
|
291
|
|
|
$this->setConnectionParameters(); |
|
292
|
|
|
$this->fetchCandidates(); |
|
293
|
|
|
$this->printAllCandidates(); |
|
294
|
|
|
$this->processQueuedCandidates(); |
|
295
|
|
|
} |
|
296
|
|
|
|
|
297
|
|
|
|
|
298
|
|
|
/** |
|
299
|
|
|
* @param Candidate $candidate |
|
300
|
|
|
*/ |
|
301
|
|
|
private function executeCandidateScript(Candidate $candidate) |
|
302
|
|
|
{ |
|
303
|
|
|
$this->io->warning( |
|
304
|
|
|
sprintf( |
|
305
|
|
|
'PROCESSING [%d/%d] %s', |
|
306
|
|
|
$candidate->getIndex(), |
|
307
|
|
|
$this->queuedCandidatesCount, |
|
308
|
|
|
$candidate->getName() |
|
309
|
|
|
) |
|
310
|
|
|
); |
|
311
|
|
|
$this->io->text($this->sqlFormatter->format($candidate->getContent())); |
|
312
|
|
|
$action = $this->io->choice('What action to perform', ['DEPLOY', 'SKIP', 'QUIT']); |
|
313
|
|
|
|
|
314
|
|
|
switch ($action) { |
|
315
|
|
|
case 'DEPLOY': |
|
316
|
|
|
$this->deployCandidate($candidate); |
|
317
|
|
|
break; |
|
318
|
|
|
case 'QUIT': |
|
319
|
|
|
$this->finish(); |
|
320
|
|
|
break; |
|
321
|
|
|
} |
|
322
|
|
|
} |
|
323
|
|
|
|
|
324
|
|
|
|
|
325
|
|
|
private function printAllCandidates() |
|
326
|
|
|
{ |
|
327
|
|
|
$this->io->table( |
|
328
|
|
|
DataRowsBuilder::TABLE_HEADERS, |
|
329
|
|
|
$this->dataRowsBuilder->build( |
|
330
|
|
|
$this->workingDirectoryCandidates, $this->skipProvisionedCandidates) |
|
331
|
|
|
); |
|
332
|
|
|
$this->io->newLine(3); |
|
333
|
|
|
} |
|
334
|
|
|
|
|
335
|
|
|
|
|
336
|
|
|
private function processQueuedCandidates() |
|
337
|
|
|
{ |
|
338
|
|
|
while (!empty($this->workingDirectoryCandidates)) { |
|
339
|
|
|
$candidate = array_shift($this->workingDirectoryCandidates); |
|
340
|
|
|
|
|
341
|
|
|
if ($candidate->isQueued()) { |
|
342
|
|
|
$this->executeCandidateScript($candidate); |
|
343
|
|
|
} |
|
344
|
|
|
} |
|
345
|
|
|
$this->io->writeln('<info>All candidates scripts were executed</info>'); |
|
346
|
|
|
} |
|
347
|
|
|
|
|
348
|
|
|
|
|
349
|
|
|
/** |
|
350
|
|
|
* @param Candidate $candidate |
|
351
|
|
|
*/ |
|
352
|
|
|
private function deployCandidate(Candidate $candidate) |
|
353
|
|
|
{ |
|
354
|
|
|
try { |
|
355
|
|
|
$this->executor->execute($candidate); |
|
356
|
|
|
$this->processor->postValidate($candidate); |
|
357
|
|
|
} catch (DatabaseException $databaseException) { |
|
358
|
|
|
$this->io->error($databaseException->getMessage()); |
|
359
|
|
|
$this->io->writeln( |
|
360
|
|
|
sprintf( |
|
361
|
|
|
"<bg=yellow>%s\n\r%s</>", |
|
362
|
|
|
$databaseException->getPrevious()->getMessage(), |
|
363
|
|
|
$candidate->getContent() |
|
364
|
|
|
) |
|
365
|
|
|
); |
|
366
|
|
|
$this->terminate(); |
|
367
|
|
|
} catch (RuntimeException $runtimeException) { |
|
368
|
|
|
$this->io->error($runtimeException->getMessage()); |
|
369
|
|
|
$this->terminate(); |
|
370
|
|
|
} |
|
371
|
|
|
} |
|
372
|
|
|
|
|
373
|
|
|
|
|
374
|
|
|
private function finish() |
|
375
|
|
|
{ |
|
376
|
|
|
$this->io->text(sprintf('Provisioning ended at %s', date('Y-m-d H:i:s'))); |
|
377
|
|
|
$this->io->writeln(sprintf('<info>Memory used: %s MB. Total Time of provisioning: %s seconds</info>', |
|
378
|
|
|
memory_get_peak_usage(true) / (pow(1024, 2)), |
|
379
|
|
|
time() - $this->startTimestamp |
|
380
|
|
|
)); |
|
381
|
|
|
die(0); |
|
|
|
|
|
|
382
|
|
|
} |
|
383
|
|
|
|
|
384
|
|
|
|
|
385
|
|
|
private function terminate() |
|
386
|
|
|
{ |
|
387
|
|
|
$this->io->text(sprintf('Provisioning ended with error at %s', date('Y-m-d H:i:s'))); |
|
388
|
|
|
die(1); |
|
|
|
|
|
|
389
|
|
|
} |
|
390
|
|
|
} |
|
391
|
|
|
|
An exit expression should only be used in rare cases. For example, if you write a short command line script.
In most cases however, using an
exitexpression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.