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\Config\ProvisionConfig; |
14
|
|
|
use Tworzenieweb\SqlProvisioner\Controller\ProvisionDispatcher; |
15
|
|
|
use Tworzenieweb\SqlProvisioner\Database\Connection; |
16
|
|
|
use Tworzenieweb\SqlProvisioner\Filesystem\Exception; |
17
|
|
|
use Tworzenieweb\SqlProvisioner\Filesystem\WorkingDirectory; |
18
|
|
|
use Tworzenieweb\SqlProvisioner\Model\Candidate; |
19
|
|
|
use Tworzenieweb\SqlProvisioner\Model\CandidateBuilder; |
20
|
|
|
use Tworzenieweb\SqlProvisioner\Table\DataRowsBuilder; |
21
|
|
|
|
22
|
|
|
/** |
23
|
|
|
* @author Luke Adamczewski |
24
|
|
|
* @package Tworzenieweb\SqlProvisioner\Command |
25
|
|
|
*/ |
26
|
|
|
class ProvisionCommand extends Command |
27
|
|
|
{ |
28
|
|
|
const HELP_MESSAGE = <<<'EOF' |
29
|
|
|
The <info>%command.name% [path-to-folder]</info> command will scan the content of [path-to-folder] directory. |
30
|
|
|
|
31
|
|
|
The script will look for <info>.env</info> file containing connection information in format: |
32
|
|
|
<comment> |
33
|
|
|
DATABASE_USER=[user] |
34
|
|
|
DATABASE_PASSWORD=[password] |
35
|
|
|
DATABASE_HOST=[host] |
36
|
|
|
DATABASE_PORT=[port] |
37
|
|
|
DATABASE_NAME=[database] |
38
|
|
|
PROVISIONING_TABLE=changelog_database_deployments |
39
|
|
|
PROVISIONING_TABLE_CANDIDATE_NUMBER_COLUMN=deploy_script_number |
40
|
|
|
</comment> |
41
|
|
|
|
42
|
|
|
If you want to create initial .env use <info>--init</info> |
43
|
|
|
|
44
|
|
|
<info>%command.name% --init [path-to-folder]</info> |
45
|
|
|
|
46
|
|
|
The next step is searching for sql files and trying to queue them in numerical order. |
47
|
|
|
First n-th digits of a filename will be treated as candidate number. |
48
|
|
|
This will be used then to check in database if a certain file was already deployed (PROVISIONING_TABLE_CANDIDATE_NUMBER_COLUMN). |
49
|
|
|
Before the insert, it will print the formatted output of a file and result of internal syntax check. |
50
|
|
|
Then you can either skip or execute each. |
51
|
|
|
|
52
|
|
|
If you would like to skip already provisioned candidates use <info>--skip-provisioned</info> |
53
|
|
|
If you would like to skip syntax checking (for speed purpose) of candidates use <info>--skip-syntax-check</info> |
54
|
|
|
|
55
|
|
|
EOF; |
56
|
|
|
|
57
|
|
|
/** @var Candidate[] */ |
58
|
|
|
private $workingDirectoryCandidates = []; |
59
|
|
|
|
60
|
|
|
/** @var WorkingDirectory */ |
61
|
|
|
private $workingDirectory; |
62
|
|
|
|
63
|
|
|
/** @var SymfonyStyle */ |
64
|
|
|
private $io; |
65
|
|
|
|
66
|
|
|
/** @var Connection */ |
67
|
|
|
private $connection; |
68
|
|
|
|
69
|
|
|
/** @var boolean */ |
70
|
|
|
private $skipProvisionedCandidates = false; |
71
|
|
|
|
72
|
|
|
/** @var CandidateBuilder */ |
73
|
|
|
private $candidateBuilder; |
74
|
|
|
|
75
|
|
|
/** @var DataRowsBuilder */ |
76
|
|
|
private $dataRowsBuilder; |
77
|
|
|
|
78
|
|
|
/** @var integer */ |
79
|
|
|
private $queuedCandidatesCount = 0; |
80
|
|
|
|
81
|
|
|
/** @var array */ |
82
|
|
|
private $errorMessages = []; |
83
|
|
|
|
84
|
|
|
/** @var ProvisionDispatcher */ |
85
|
|
|
private $dispatcher; |
86
|
|
|
|
87
|
|
|
/** @var ProvisionConfig */ |
88
|
|
|
private $config; |
89
|
|
|
|
90
|
|
|
|
91
|
|
|
/** |
92
|
|
|
* @param string $name |
93
|
|
|
* @param WorkingDirectory $workingDirectory |
94
|
|
|
* @param Connection $connection |
95
|
|
|
* @param CandidateBuilder $candidateBuilder |
96
|
1 |
|
* @param DataRowsBuilder $dataRowsBuilder |
97
|
|
|
* @param ProvisionDispatcher $dispatcher |
98
|
|
|
* @param ProvisionConfig $config |
99
|
|
|
*/ |
100
|
|
|
public function __construct( |
101
|
|
|
$name, |
102
|
|
|
WorkingDirectory $workingDirectory, |
103
|
|
|
Connection $connection, |
104
|
|
|
CandidateBuilder $candidateBuilder, |
105
|
1 |
|
DataRowsBuilder $dataRowsBuilder, |
106
|
1 |
|
ProvisionDispatcher $dispatcher, |
107
|
1 |
|
ProvisionConfig $config |
108
|
1 |
|
) { |
109
|
1 |
|
$this->workingDirectory = $workingDirectory; |
110
|
|
|
$this->connection = $connection; |
111
|
1 |
|
$this->candidateBuilder = $candidateBuilder; |
112
|
1 |
|
$this->dataRowsBuilder = $dataRowsBuilder; |
113
|
|
|
$this->dispatcher = $dispatcher; |
114
|
|
|
$this->config = $config; |
115
|
1 |
|
|
116
|
|
|
parent::__construct($name); |
117
|
1 |
|
} |
118
|
1 |
|
|
119
|
1 |
|
|
120
|
1 |
|
protected function configure() |
121
|
1 |
|
{ |
122
|
1 |
|
$this |
123
|
1 |
|
->setDescription('Execute the content of *.sql files from given') |
124
|
1 |
|
->setHelp(self::HELP_MESSAGE); |
125
|
|
|
$this->addOption('init', null, InputOption::VALUE_NONE, 'Initialize .env in given directory'); |
126
|
1 |
|
$this->addOption( |
127
|
1 |
|
'skip-provisioned', |
128
|
1 |
|
null, |
129
|
1 |
|
InputOption::VALUE_NONE, |
130
|
1 |
|
'Skip provisioned candidates from printing' |
131
|
|
|
); |
132
|
1 |
|
$this->addOption( |
133
|
1 |
|
'skip-syntax-check', |
134
|
1 |
|
null, |
135
|
|
|
InputOption::VALUE_NONE, |
136
|
|
|
'Skip executing of sql syntax check for each entry' |
137
|
|
|
); |
138
|
|
|
$this->addOption( |
139
|
|
|
'skip-email', |
140
|
|
|
null, |
141
|
|
|
InputOption::VALUE_NONE, |
142
|
|
|
'Skip email notification after provision is done' |
143
|
|
|
); |
144
|
|
|
$this->addOption( |
145
|
|
|
'force', |
146
|
|
|
'f', |
147
|
|
|
InputOption::VALUE_NONE, |
148
|
|
|
'Execute provision candidates without asking for confirmation' |
149
|
|
|
); |
150
|
|
|
$this->addOption( |
151
|
|
|
'env-file', |
152
|
|
|
null, |
153
|
|
|
InputOption::VALUE_OPTIONAL, |
154
|
|
|
'Environment variables file path. Use this env file to seed base environment variables.' |
155
|
|
|
); |
156
|
|
|
$this->addArgument('path', InputArgument::REQUIRED, 'Path to dbdeploys folder'); |
157
|
|
|
} |
158
|
|
|
|
159
|
|
|
protected function initialize(InputInterface $input, OutputInterface $output) |
160
|
|
|
{ |
161
|
|
|
if ($envFile = $input->getOption('env-file')) { |
162
|
|
|
$this->config->withEnvPath($envFile); |
163
|
|
|
} |
164
|
|
|
|
165
|
|
|
if ($input->getOption('force')) { |
166
|
|
|
$this->config->force(); |
167
|
|
|
} |
168
|
|
|
|
169
|
|
|
if ($input->getOption('skip-email')) { |
170
|
|
|
$this->config->skipEmail(); |
171
|
|
|
} |
172
|
|
|
|
173
|
|
|
if ($input->getOption('skip-syntax-check')) { |
174
|
|
|
$this->config->skipSyntaxCheck(); |
175
|
|
|
} |
176
|
|
|
|
177
|
|
|
if ($input->getOption('skip-provisioned')) { |
178
|
|
|
$this->config->skipProvisioned(); |
179
|
|
|
} |
180
|
|
|
|
181
|
|
|
$this->config->load(); |
182
|
|
|
} |
183
|
|
|
|
184
|
|
|
/** |
185
|
|
|
* @param InputInterface $input |
186
|
|
|
* @param OutputInterface $output |
187
|
|
|
* |
188
|
|
|
* @return int |
189
|
|
|
*/ |
190
|
|
|
protected function execute(InputInterface $input, OutputInterface $output) |
191
|
|
|
{ |
192
|
|
|
$this->start($input, $output); |
193
|
|
|
$this->io->section('Working directory processing'); |
194
|
|
|
$this->io->comment(sprintf('Using env file from [%s]', $this->config->getEnvPath())); |
195
|
|
|
|
196
|
|
|
if ($this->config->isSkipProvisioned()) { |
197
|
|
|
$this->skipProvisionedCandidates = true; |
198
|
|
|
$this->io->warning('Hiding of provisioned candidates ENABLED'); |
199
|
|
|
} |
200
|
|
|
|
201
|
|
|
if ($this->config->isSkipSyntaxCheck()) { |
202
|
|
|
$this->dispatcher->skipSyntaxCheck(); |
203
|
|
|
} |
204
|
|
|
|
205
|
|
|
$this->processWorkingDirectory($input); |
206
|
|
|
$this->processCandidates(); |
207
|
|
|
|
208
|
|
|
return 0; |
209
|
|
|
} |
210
|
|
|
|
211
|
|
|
|
212
|
|
|
/** |
213
|
|
|
* @param InputInterface $input |
214
|
|
|
* @param OutputInterface $output |
215
|
|
|
*/ |
216
|
|
|
protected function start(InputInterface $input, OutputInterface $output) |
217
|
|
|
{ |
218
|
|
|
$this->io = new SymfonyStyle($input, $output); |
219
|
|
|
$this->dispatcher->setInputOutput($this->io); |
220
|
|
|
|
221
|
|
|
$this->io->title('SQL Provisioner'); |
222
|
|
|
$this->io->block(sprintf('Provisioning started at %s', date('Y-m-d H:i:s'))); |
223
|
|
|
} |
224
|
|
|
|
225
|
|
|
|
226
|
|
|
protected function fetchCandidates() |
227
|
|
|
{ |
228
|
|
|
$this->iterateOverWorkingDirectory(); |
229
|
|
|
|
230
|
|
|
if (!empty($this->errorMessages)) { |
231
|
|
|
$this->showSyntaxErrors(); |
232
|
|
|
} |
233
|
|
|
|
234
|
|
|
if (!$this->queuedCandidatesCount) { |
235
|
|
|
$this->io->block('All candidates scripts were executed already.'); |
236
|
|
|
$this->dispatcher->finalizeAndExit(); |
237
|
|
|
} |
238
|
|
|
} |
239
|
|
|
|
240
|
|
|
|
241
|
|
|
/** |
242
|
|
|
* @param SplFileInfo $candidateFile |
243
|
|
|
*/ |
244
|
|
|
protected function processCandidateFile($candidateFile) |
245
|
|
|
{ |
246
|
|
|
$candidate = $this->candidateBuilder->build($candidateFile); |
247
|
|
|
array_push($this->workingDirectoryCandidates, $candidate); |
248
|
|
|
|
249
|
|
|
try { |
250
|
|
|
$this->dispatcher->validate($candidate); |
251
|
|
|
|
252
|
|
|
// can be also ignored but without error |
253
|
|
|
if ($candidate->isQueued()) { |
254
|
|
|
$this->queuedCandidatesCount++; |
255
|
|
|
} |
256
|
|
|
} catch (RuntimeException $validationError) { |
257
|
|
|
if ($validationError->getMessage()) { |
258
|
|
|
array_push($this->errorMessages, $validationError->getMessage()); |
259
|
|
|
} |
260
|
|
|
} |
261
|
|
|
} |
262
|
|
|
|
263
|
|
|
|
264
|
|
|
protected function iterateOverWorkingDirectory() |
265
|
|
|
{ |
266
|
|
|
foreach ($this->workingDirectory->getCandidates() as $candidateFile) { |
267
|
|
|
$this->processCandidateFile($candidateFile); |
268
|
|
|
} |
269
|
|
|
|
270
|
|
|
$this->io->text(sprintf('<info>%d</info> files found', count($this->workingDirectoryCandidates))); |
271
|
|
|
|
272
|
|
|
if (count($this->workingDirectoryCandidates) === 0) { |
273
|
|
|
throw Exception::noFilesInDirectory($this->workingDirectory); |
274
|
|
|
} |
275
|
|
|
} |
276
|
|
|
|
277
|
|
|
|
278
|
|
|
protected function showSyntaxErrors() |
279
|
|
|
{ |
280
|
|
|
$this->io->warning(sprintf('Detected %d syntax checking issues', count($this->errorMessages))); |
281
|
|
|
$this->printAllCandidates(); |
282
|
|
|
$this->io->warning(implode("\n", $this->errorMessages)); |
283
|
|
|
$this->dispatcher->finalizeAndExit(); |
284
|
|
|
} |
285
|
|
|
|
286
|
|
|
|
287
|
|
|
/** |
288
|
|
|
* @param InputInterface $input |
289
|
|
|
*/ |
290
|
|
|
protected function processWorkingDirectory(InputInterface $input) |
291
|
|
|
{ |
292
|
|
|
$this->workingDirectory = $this->workingDirectory->cd($input->getArgument('path')); |
|
|
|
|
293
|
|
|
$this->loadOrCreateEnvironment($input); |
294
|
|
|
$this->io->success('DONE'); |
295
|
|
|
} |
296
|
|
|
|
297
|
|
|
|
298
|
|
|
/** |
299
|
|
|
* @param InputInterface $input |
300
|
|
|
*/ |
301
|
|
|
private function loadOrCreateEnvironment(InputInterface $input) |
302
|
|
|
{ |
303
|
|
|
if ($input->getOption('init')) { |
304
|
|
|
$this->workingDirectory->createEnvironmentFile(); |
305
|
|
|
$this->io->success(sprintf('Initial .env file created in %s', $this->workingDirectory)); |
306
|
|
|
die(0); |
307
|
|
|
} |
308
|
|
|
|
309
|
|
|
$this->workingDirectory->loadEnvironment(); |
310
|
|
|
} |
311
|
|
|
|
312
|
|
|
|
313
|
|
|
private function setConnectionParameters() |
314
|
|
|
{ |
315
|
|
|
$this->connection->useMysql($_ENV['DATABASE_HOST'], $_ENV['DATABASE_PORT'], $_ENV['DATABASE_NAME'], |
316
|
|
|
$_ENV['DATABASE_USER'], $_ENV['DATABASE_PASSWORD']); |
317
|
|
|
$this->connection->setProvisioningTable($_ENV['PROVISIONING_TABLE']); |
318
|
|
|
$this->connection->setCriteriaColumn($_ENV['PROVISIONING_TABLE_CANDIDATE_NUMBER_COLUMN']); |
319
|
|
|
|
320
|
|
|
$this->io->success(sprintf('Connection with `%s` established', $_ENV['DATABASE_NAME'])); |
321
|
|
|
} |
322
|
|
|
|
323
|
|
|
|
324
|
|
|
private function processCandidates() |
325
|
|
|
{ |
326
|
|
|
$this->io->newLine(2); |
327
|
|
|
$this->io->section('Candidates processing'); |
328
|
|
|
|
329
|
|
|
$this->setConnectionParameters(); |
330
|
|
|
$this->fetchCandidates(); |
331
|
|
|
$this->printAllCandidates(); |
332
|
|
|
$this->dispatcher->deploy($this->workingDirectoryCandidates, $this->queuedCandidatesCount); |
333
|
|
|
} |
334
|
|
|
|
335
|
|
|
|
336
|
|
|
private function printAllCandidates() |
337
|
|
|
{ |
338
|
|
|
$this->io->table( |
339
|
|
|
DataRowsBuilder::TABLE_HEADERS, |
340
|
|
|
$this->dataRowsBuilder->build( |
341
|
|
|
$this->workingDirectoryCandidates, $this->skipProvisionedCandidates) |
342
|
|
|
); |
343
|
|
|
$this->io->newLine(3); |
344
|
|
|
} |
345
|
|
|
} |
346
|
|
|
|
This check looks at variables that are passed out again to other methods.
If the outgoing method call has stricter type requirements than the method itself, an issue is raised.
An additional type check may prevent trouble.