Completed
Push — master ( d401ab...1f0fb6 )
by Łukasz
02:47
created

ProvisionCommand   A

Complexity

Total Complexity 32

Size/Duplication

Total Lines 363
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 13

Test Coverage

Coverage 13.25%

Importance

Changes 2
Bugs 2 Features 0
Metric Value
wmc 32
lcom 2
cbo 13
dl 0
loc 363
ccs 22
cts 166
cp 0.1325
rs 9.6
c 2
b 2
f 0

18 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 21 1
A configure() 0 14 1
A execute() 0 16 2
A start() 0 7 1
A fetchCandidates() 0 13 3
A processCandidateFile() 0 19 3
A iterateOverWorkingDirectory() 0 12 3
A showSyntaxErrors() 0 7 1
A processWorkingDirectory() 0 6 1
A loadOrCreateEnvironment() 0 10 2
A setConnectionParameters() 0 8 1
A processCandidates() 0 10 1
A executeCandidateScript() 0 22 3
A printAllCandidates() 0 9 1
A processQueuedCandidates() 0 11 3
A deployCandidate() 0 20 3
A finish() 0 9 1
A terminate() 0 5 1
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