Passed
Pull Request — master (#15)
by Łukasz
08:41
created

ProvisionCommand::executeCandidateScript()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 22
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

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