Completed
Push — master ( 456549...47da6e )
by Łukasz
02:25
created

ProvisionCommand::loadDotEnv()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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