Completed
Push — master ( f7fec7...2cdfdd )
by Łukasz
02:07
created

ProvisionCommand::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 18
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 18
rs 9.4285
cc 1
eloc 15
nc 1
nop 7
1
<?php
2
3
namespace Tworzenieweb\SqlProvisioner\Command;
4
5
use Symfony\Component\Console\Command\Command;
6
use Symfony\Component\Console\Input\InputArgument;
7
use Symfony\Component\Console\Input\InputInterface;
8
use Symfony\Component\Console\Input\InputOption;
9
use Symfony\Component\Console\Output\OutputInterface;
10
use Symfony\Component\Console\Style\SymfonyStyle;
11
use Symfony\Component\Finder\SplFileInfo;
12
use Tworzenieweb\SqlProvisioner\Check\HasDbDeployCheck;
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
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
    const TABLE_HEADERS = ['FILENAME', 'STATUS'];
57
58
    /** @var int */
59
    private $candidateIndexValue = 1;
60
61
    /** @var Candidate[] */
62
    private $workingDirectoryCandidates = [];
63
64
    /** @var Sql */
65
    private $sqlFormatter;
66
67
    /** @var WorkingDirectory */
68
    private $workingDirectory;
69
70
    /** @var SymfonyStyle */
71
    private $io;
72
73
    /** @var Connection */
74
    private $connection;
75
76
    /** @var CandidateProcessor */
77
    private $processor;
78
79
    /** @var Executor */
80
    private $executor;
81
82
    /** @var boolean */
83
    private $skipProvisionedCandidates = false;
84
85
    /** @var CandidateBuilder */
86
    private $builder;
87
88
    /** @var bool */
89
    private $hasQueuedCandidates = false;
90
91
    /** @var integer */
92
    private $queuedCandidatesCount = 0;
93
94
    /** @var array */
95
    private $errorMessages = [];
96
97
98
99
    /**
100
     * @param string $name
101
     * @param WorkingDirectory $workingDirectory
102
     * @param Connection $connection
103
     * @param Sql $sqlFormatter
104
     * @param CandidateProcessor $processor
105
     * @param CandidateBuilder $builder
106
     * @param Executor $executor
107
     */
108
    public function __construct(
109
        $name,
110
        WorkingDirectory $workingDirectory,
111
        Connection $connection,
112
        Sql $sqlFormatter,
113
        CandidateProcessor $processor,
114
        CandidateBuilder $builder,
115
        Executor $executor
116
    ) {
117
        $this->workingDirectory = $workingDirectory;
118
        $this->connection = $connection;
119
        $this->sqlFormatter = $sqlFormatter;
120
        $this->processor = $processor;
121
        $this->builder = $builder;
122
        $this->executor = $executor;
123
124
        parent::__construct($name);
125
    }
126
127
128
129
    protected function configure()
130
    {
131
        $this
132
            ->setDescription('Execute the content of *.sql files from given')
133
            ->setHelp(self::HELP_MESSAGE);
134
        $this->addOption('init', null, InputOption::VALUE_NONE, 'Initialize .env in given directory');
135
        $this->addOption(
136
            'skip-provisioned',
137
            null,
138
            InputOption::VALUE_NONE,
139
            'Skip provisioned candidates from printing'
140
        );
141
        $this->addArgument('path', InputArgument::REQUIRED, 'Path to dbdeploys folder');
142
    }
143
144
145
146
    /**
147
     * @param InputInterface $input
148
     * @param OutputInterface $output
149
     * @return int
150
     */
151
    protected function execute(InputInterface $input, OutputInterface $output)
152
    {
153
        $this->start($input, $output);
154
        $this->io->section('Working directory processing');
155
156
        if ($input->getOption('skip-provisioned')) {
157
            $this->skipProvisionedCandidates = true;
158
            $this->io->warning('Hiding of provisioned candidates ENABLED');
159
        }
160
161
        $this->processWorkingDirectory($input);
162
        $this->processCandidates();
163
        $this->finish();
164
165
        return 0;
166
    }
167
168
169
170
    /**
171
     * @param InputInterface $input
172
     * @param OutputInterface $output
173
     */
174
    protected function start(InputInterface $input, OutputInterface $output)
175
    {
176
        $this->io = new SymfonyStyle($input, $output);
177
        $this->io->title('SQL Provisioner');
178
        $this->io->block(sprintf('Provisioning started at %s', date('Y-m-d H:i:s')));
179
    }
180
181
182
183
    protected function fetchCandidates()
184
    {
185
        $this->iterateOverWorkingDirectory();
186
187
        if (!empty($this->errorMessages)) {
188
            $this->showSyntaxErrors();
189
        }
190
191
        if (false === $this->hasQueuedCandidates) {
192
            $this->io->block('All candidates scripts were executed already.');
193
            $this->finish();
194
        }
195
    }
196
197
198
199
    /**
200
     * @param SplFileInfo $candidateFile
201
     */
202
    protected function processCandidateFile($candidateFile)
203
    {
204
        $candidate = $this->builder->build($candidateFile);
205
        array_push($this->workingDirectoryCandidates, $candidate);
206
207
        if ($this->processor->isValid($candidate)) {
208
            $candidate->markAsQueued();
209
            $candidate->setIndex($this->candidateIndexValue++);
210
            $this->hasQueuedCandidates = true;
211
            $this->queuedCandidatesCount++;
212
        } else {
213
            $candidate->markAsIgnored($this->processor->getLastError());
214
            $lastErrorMessage = $this->processor->getLastErrorMessage();
215
216
            if (!empty($lastErrorMessage)) {
217
                array_push($this->errorMessages, $lastErrorMessage);
218
            }
219
        }
220
    }
221
222
223
224
    protected function iterateOverWorkingDirectory()
225
    {
226
        foreach ($this->workingDirectory->getCandidates() as $candidateFile) {
227
            $this->processCandidateFile($candidateFile);
228
        }
229
230
        $this->io->text(sprintf('<info>%d</info> files found', count($this->workingDirectoryCandidates)));
231
232
        if (count($this->workingDirectoryCandidates) === 0) {
233
            throw Exception::noFilesInDirectory($this->workingDirectory);
234
        }
235
    }
236
237
238
239
    protected function showSyntaxErrors()
240
    {
241
        $this->io->warning(sprintf('Detected %d syntax checking issues', count($this->errorMessages)));
242
        $this->printAllCandidates();
243
        $this->io->writeln(sprintf('<error>%s</error>', implode("\n", $this->errorMessages)));
244
        $this->finish();
245
    }
246
247
248
249
    /**
250
     * @param InputInterface $input
251
     */
252
    protected function processWorkingDirectory(InputInterface $input)
253
    {
254
        $this->workingDirectory = $this->workingDirectory->cd($input->getArgument('path'));
255
        $this->loadDotEnv($input);
256
        $this->io->success('DONE');
257
    }
258
259
260
261
    /**
262
     * @param InputInterface $input
263
     */
264
    private function loadDotEnv(InputInterface $input)
265
    {
266
        if ($input->getOption('init')) {
267
            $this->workingDirectory->touchDotEnv();
268
            $this->io->success(sprintf('Initial .env file created in %s', $this->workingDirectory));
269
            die(0);
270
        }
271
272
        $this->workingDirectory->loadDotEnv();
273
    }
274
275
276
277
    private function setConnectionParameters()
278
    {
279
        $this->connection->setDatabaseName($_ENV['DATABASE_NAME']);
280
        $this->connection->setHost($_ENV['DATABASE_HOST']);
281
        $this->connection->setUser($_ENV['DATABASE_USER']);
282
        $this->connection->setPassword($_ENV['DATABASE_PASSWORD']);
283
        $this->connection->setProvisioningTable($_ENV['PROVISIONING_TABLE']);
284
        $this->connection->setCriteriaColumn($_ENV['PROVISIONING_TABLE_CANDIDATE_NUMBER_COLUMN']);
285
286
        $this->io->success(sprintf('Connection with `%s` established', $_ENV['DATABASE_NAME']));
287
    }
288
289
290
291
    private function processCandidates()
292
    {
293
        $this->io->newLine(2);
294
        $this->io->section('Candidates processing');
295
296
        $this->setConnectionParameters();
297
        $this->fetchCandidates();
298
        $this->printAllCandidates();
299
        $this->processQueuedCandidates();
300
    }
301
302
303
304
    /**
305
     * @param Candidate $candidate
306
     */
307
    private function executeCandidateScript(Candidate $candidate)
308
    {
309
        $this->io->warning(
310
            sprintf(
311
                'PROCESSING [%d/%d] %s',
312
                $candidate->getIndex(),
313
                $this->queuedCandidatesCount,
314
                $candidate->getName()
315
            )
316
        );
317
        $this->io->text($this->sqlFormatter->format($candidate->getContent()));
318
        $action = $this->io->choice('What action to perform', ['DEPLOY', 'SKIP', 'QUIT']);
319
320
        switch ($action) {
321
            case 'DEPLOY':
322
                $this->deployCandidate($candidate);
323
                break;
324
            case 'QUIT':
325
                $this->finish();
326
                break;
327
        }
328
    }
329
330
331
332
    private function printAllCandidates()
333
    {
334
        $self = $this;
335
        $rows = array_map(
336
            function (Candidate $candidate) use ($self) {
337
                return $self->buildCandidateRow($candidate);
338
            },
339
            $this->workingDirectoryCandidates
340
        );
341
342
        $this->io->table(
343
            self::TABLE_HEADERS,
344
            array_filter($rows)
345
        );
346
        $this->io->newLine(3);
347
    }
348
349
350
351
    private function processQueuedCandidates()
352
    {
353
        while (!empty($this->workingDirectoryCandidates)) {
354
            $candidate = array_shift($this->workingDirectoryCandidates);
355
356
            if ($candidate->isQueued()) {
357
                $this->executeCandidateScript($candidate);
358
            }
359
        }
360
        $this->io->writeln('<info>All candidates scripts were executed</info>');
361
    }
362
363
364
365
    /**
366
     * @param Candidate $candidate
367
     */
368
    private function deployCandidate(Candidate $candidate)
369
    {
370
        try {
371
            $this->executor->execute($candidate);
372
        } catch (DatabaseException $databaseException) {
373
            $this->io->error($databaseException->getMessage());
374
            $this->io->writeln(
375
                sprintf(
376
                    "<bg=yellow>%s\n\r%s</>",
377
                    $databaseException->getPrevious()->getMessage(),
378
                    $candidate->getContent()
379
                )
380
            );
381
            $this->terminate();
382
        }
383
    }
384
385
386
387
    private function finish()
388
    {
389
        $this->io->text(sprintf('Provisioning ended at %s', date('Y-m-d H:i:s')));
390
        die(0);
391
    }
392
393
394
395
    private function terminate()
396
    {
397
        $this->io->text(sprintf('Provisioning ended with error at %s', date('Y-m-d H:i:s')));
398
        die(1);
399
    }
400
401
402
403
    /**
404
     * @param Candidate $candidate
405
     * @return array|null
406
     */
407
    private function buildCandidateRow(Candidate $candidate)
408
    {
409
        $status = $candidate->getStatus();
410
411
        switch ($status) {
412
            case Candidate::STATUS_QUEUED:
413
                $status = sprintf('<comment>%s</comment>', $status);
414
                break;
415
            case HasDbDeployCheck::ERROR_STATUS:
416
                if ($this->skipProvisionedCandidates) {
417
                    return null;
418
                }
419
                break;
420
            case HasSyntaxCorrectCheck::ERROR_STATUS:
421
                $status = sprintf('<error>%s</error>', $status);
422
                break;
423
        }
424
425
        return [$candidate->getName(), $status];
426
    }
427
}