Completed
Push — master ( bc42f8...f7fec7 )
by Łukasz
02:09
created

ProvisionCommand::iterateOverWorkingDirectory()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 12
rs 9.4285
cc 3
eloc 6
nc 4
nop 0
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\Filesystem\Filesystem;
12
use Symfony\Component\Finder\SplFileInfo;
13
use Tworzenieweb\SqlProvisioner\Check\HasDbDeployCheck;
14
use Tworzenieweb\SqlProvisioner\Check\HasSyntaxCorrectCheck;
15
use Tworzenieweb\SqlProvisioner\Database\Connection;
16
use Tworzenieweb\SqlProvisioner\Database\Exception as DatabaseException;
17
use Tworzenieweb\SqlProvisioner\Database\Executor;
18
use Tworzenieweb\SqlProvisioner\Filesystem\CandidatesFinder;
19
use Tworzenieweb\SqlProvisioner\Filesystem\Exception;
20
use Tworzenieweb\SqlProvisioner\Filesystem\WorkingDirectory;
21
use Tworzenieweb\SqlProvisioner\Formatter\Sql;
22
use Tworzenieweb\SqlProvisioner\Model\Candidate;
23
use Tworzenieweb\SqlProvisioner\Model\CandidateBuilder;
24
use Tworzenieweb\SqlProvisioner\Processor\CandidateProcessor;
25
26
/**
27
 * @author Luke Adamczewski
28
 * @package Tworzenieweb\SqlProvisioner\Command
29
 */
30
class ProvisionCommand extends Command
31
{
32
    const HELP_MESSAGE = <<<'EOF'
33
The <info>%command.name% [path-to-folder]</info> command will scan the content of [path-to-folder] directory.
34
 
35
The script will look for <info>.env</info> file containing connection information in format:
36
<comment>
37
DATABASE_USER=[user]
38
DATABASE_PASSWORD=[password]
39
DATABASE_HOST=[host]
40
DATABASE_PORT=[port]
41
DATABASE_NAME=[database]
42
PROVISIONING_TABLE=changelog_database_deployments
43
PROVISIONING_TABLE_CANDIDATE_NUMBER_COLUMN=deploy_script_number
44
</comment>
45
46
If you want to create initial .env use <info>--init</info>
47
48
<info>%command.name% --init [path-to-folder]</info>
49
50
The next step is searching for sql files and trying to queue them in numerical order.
51
First n-th digits of a filename will be treated as candidate number. 
52
This will be used then to check in database if a certain file was already deployed (PROVISIONING_TABLE_CANDIDATE_NUMBER_COLUMN).
53
Before the insert, it will print the formatted output of a file and result of internal syntax check.
54
Then you can either skip or execute each.
55
56
If you would like to skip already provisioned candidates use <info>--skip-provisioned</info>
57
EOF;
58
    const TABLE_HEADERS = ['FILENAME', 'STATUS'];
59
    private $candidateIndexValue = 1;
60
61
    /** @var Candidate[] */
62
    private $workingDirectoryCandidates;
63
64
    /** @var Sql */
65
    private $sqlFormatter;
66
67
    /** @var Filesystem */
68
    private $filesystem;
69
70
    /** @var string */
71
    private $workingDirectory;
72
73
    /** @var CandidatesFinder */
74
    private $finder;
75
76
    /** @var SymfonyStyle */
77
    private $io;
78
79
    /** @var Connection */
80
    private $connection;
81
82
    /** @var CandidateProcessor */
83
    private $processor;
84
85
    /** @var Executor */
86
    private $executor;
87
88
    /** @var boolean */
89
    private $skipProvisionedCandidates;
90
91
    /** @var CandidateBuilder */
92
    private $builder;
93
94
    /** @var bool */
95
    private $hasQueuedCandidates;
96
97
    /** @var integer */
98
    private $queuedCandidatesCount;
99
100
    /** @var array */
101
    private $errorMessages;
102
103
104
105
    /**
106
     * @param string $name
107
     * @param WorkingDirectory $workingDirectory
108
     * @param Connection $connection
109
     * @param Sql $sqlFormatter
110
     * @param CandidateProcessor $processor
111
     * @param CandidateBuilder $builder
112
     * @param Executor $executor
113
     */
114
    public function __construct(
115
        $name,
116
        WorkingDirectory $workingDirectory,
117
        Connection $connection,
118
        Sql $sqlFormatter,
119
        CandidateProcessor $processor,
120
        CandidateBuilder $builder,
121
        Executor $executor
122
    ) {
123
        $this->workingDirectory = $workingDirectory;
0 ignored issues
show
Documentation Bug introduced by
It seems like $workingDirectory of type object<Tworzenieweb\SqlP...ystem\WorkingDirectory> is incompatible with the declared type string of property $workingDirectory.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
124
        $this->connection = $connection;
125
        $this->sqlFormatter = $sqlFormatter;
126
        $this->filesystem = new Filesystem();
127
        $this->processor = $processor;
128
        $this->builder = $builder;
129
        $this->executor = $executor;
130
131
        $this->workingDirectoryCandidates = [];
132
        $this->skipProvisionedCandidates = false;
133
        $this->hasQueuedCandidates = false;
134
        $this->queuedCandidatesCount = 0;
135
        $this->errorMessages = [];
136
137
        parent::__construct($name);
138
    }
139
140
141
142
    protected function configure()
143
    {
144
        $this
145
            ->setDescription('Execute the content of *.sql files from given')
146
            ->setHelp(self::HELP_MESSAGE);
147
        $this->addOption('init', null, InputOption::VALUE_NONE, 'Initialize .env in given directory');
148
        $this->addOption(
149
            'skip-provisioned',
150
            null,
151
            InputOption::VALUE_NONE,
152
            'Skip provisioned candidates from printing'
153
        );
154
        $this->addArgument('path', InputArgument::REQUIRED, 'Path to dbdeploys folder');
155
    }
156
157
158
159
    /**
160
     * @param InputInterface $input
161
     * @param OutputInterface $output
162
     * @return int
163
     */
164
    protected function execute(InputInterface $input, OutputInterface $output)
165
    {
166
        $this->start($input, $output);
167
        $this->io->section('Working directory processing');
168
169
        if ($input->getOption('skip-provisioned')) {
170
            $this->skipProvisionedCandidates = true;
171
            $this->io->warning('Hiding of provisioned candidates ENABLED');
172
        }
173
174
        $this->processWorkingDirectory($input);
175
        $this->processCandidates();
176
        $this->finish();
177
178
        return 0;
179
    }
180
181
182
183
    /**
184
     * @param InputInterface $input
185
     * @param OutputInterface $output
186
     */
187
    protected function start(InputInterface $input, OutputInterface $output)
188
    {
189
        $this->io = new SymfonyStyle($input, $output);
190
        $this->io->title('SQL Provisioner');
191
        $this->io->block(sprintf('Provisioning started at %s', date('Y-m-d H:i:s')));
192
    }
193
194
195
196
    protected function fetchCandidates()
197
    {
198
        $this->iterateOverWorkingDirectory();
199
200
        if (!empty($this->errorMessages)) {
201
            $this->showSyntaxErrors();
202
        }
203
204
        if (false === $this->hasQueuedCandidates) {
205
            $this->io->block('All candidates scripts were executed already.');
206
            $this->finish();
207
        }
208
    }
209
210
211
212
    /**
213
     * @param SplFileInfo $candidateFile
214
     */
215
    protected function processCandidateFile($candidateFile)
216
    {
217
        $candidate = $this->builder->build($candidateFile);
218
        array_push($this->workingDirectoryCandidates, $candidate);
219
220
        if ($this->processor->isValid($candidate)) {
221
            $candidate->markAsQueued();
222
            $candidate->setIndex($this->candidateIndexValue++);
223
            $this->hasQueuedCandidates = true;
224
            $this->queuedCandidatesCount++;
225
        } else {
226
            $candidate->markAsIgnored($this->processor->getLastError());
227
            $lastErrorMessage = $this->processor->getLastErrorMessage();
228
229
            if (!empty($lastErrorMessage)) {
230
                array_push($this->errorMessages, $lastErrorMessage);
231
            }
232
        }
233
    }
234
235
236
237
    protected function iterateOverWorkingDirectory()
238
    {
239
        foreach ($this->workingDirectory->getCandidates() as $candidateFile) {
0 ignored issues
show
Bug introduced by
The method getCandidates cannot be called on $this->workingDirectory (of type string).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
240
            $this->processCandidateFile($candidateFile);
241
        }
242
243
        $this->io->text(sprintf('<info>%d</info> files found', count($this->workingDirectoryCandidates)));
244
245
        if (count($this->workingDirectoryCandidates) === 0) {
246
            throw Exception::noFilesInDirectory($this->workingDirectory);
247
        }
248
    }
249
250
251
252
    protected function showSyntaxErrors()
253
    {
254
        $this->io->warning(sprintf('Detected %d syntax checking issues', count($this->errorMessages)));
255
        $this->printAllCandidates();
256
        $this->io->writeln(sprintf('<error>%s</error>', implode("\n", $this->errorMessages)));
257
        $this->finish();
258
    }
259
260
261
262
    /**
263
     * @param InputInterface $input
264
     */
265
    protected function processWorkingDirectory(InputInterface $input)
266
    {
267
        $this->workingDirectory = $this->workingDirectory->cd($input->getArgument('path'));
0 ignored issues
show
Bug introduced by
The method cd cannot be called on $this->workingDirectory (of type string).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
268
        $this->loadDotEnv($input);
269
        $this->io->success('DONE');
270
    }
271
272
273
274
    /**
275
     * @param InputInterface $input
276
     */
277
    private function loadDotEnv(InputInterface $input)
278
    {
279
        if ($input->getOption('init')) {
280
            $this->workingDirectory->touchDotEnv();
0 ignored issues
show
Bug introduced by
The method touchDotEnv cannot be called on $this->workingDirectory (of type string).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
281
            $this->io->success(sprintf('Initial .env file created in %s', $this->workingDirectory));
282
            die(0);
283
        }
284
285
        $this->workingDirectory->loadDotEnv();
0 ignored issues
show
Bug introduced by
The method loadDotEnv cannot be called on $this->workingDirectory (of type string).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
286
    }
287
288
289
290
    private function setConnectionParameters()
291
    {
292
        $this->connection->setDatabaseName($_ENV['DATABASE_NAME']);
293
        $this->connection->setHost($_ENV['DATABASE_HOST']);
294
        $this->connection->setUser($_ENV['DATABASE_USER']);
295
        $this->connection->setPassword($_ENV['DATABASE_PASSWORD']);
296
        $this->connection->setProvisioningTable($_ENV['PROVISIONING_TABLE']);
297
        $this->connection->setCriteriaColumn($_ENV['PROVISIONING_TABLE_CANDIDATE_NUMBER_COLUMN']);
298
299
        $this->io->success(sprintf('Connection with `%s` established', $_ENV['DATABASE_NAME']));
300
    }
301
302
303
304
    private function processCandidates()
305
    {
306
        $this->io->newLine(2);
307
        $this->io->section('Candidates processing');
308
309
        $this->setConnectionParameters();
310
        $this->fetchCandidates();
311
        $this->printAllCandidates();
312
        $this->processQueuedCandidates();
313
    }
314
315
316
317
    /**
318
     * @param Candidate $candidate
319
     */
320
    private function executeCandidateScript(Candidate $candidate)
321
    {
322
        $this->io->warning(
323
            sprintf(
324
                'PROCESSING [%d/%d] %s',
325
                $candidate->getIndex(),
326
                $this->queuedCandidatesCount,
327
                $candidate->getName()
328
            )
329
        );
330
        $this->io->text($this->sqlFormatter->format($candidate->getContent()));
331
        $action = $this->io->choice('What action to perform', ['DEPLOY', 'SKIP', 'QUIT']);
332
333
        switch ($action) {
334
            case 'DEPLOY':
335
                $this->deployCandidate($candidate);
336
                break;
337
            case 'QUIT':
338
                $this->finish();
339
                break;
340
        }
341
    }
342
343
344
345
    private function printAllCandidates()
346
    {
347
        $self = $this;
348
        $rows = array_map(
349
            function (Candidate $candidate) use ($self) {
350
                $status = $candidate->getStatus();
351
352
                switch ($status) {
353
                    case Candidate::STATUS_QUEUED:
354
                        $status = sprintf('<comment>%s</comment>', $status);
355
                        break;
356
                    case HasDbDeployCheck::ERROR_STATUS:
357
                        if ($self->skipProvisionedCandidates) {
358
                            return null;
359
                        }
360
                        break;
361
                    case HasSyntaxCorrectCheck::ERROR_STATUS:
362
                        $status = sprintf('<error>%s</error>', $status);
363
                        break;
364
                }
365
366
                return [$candidate->getName(), $status];
367
            },
368
            $this->workingDirectoryCandidates
369
        );
370
371
        $this->io->table(
372
            self::TABLE_HEADERS,
373
            array_filter($rows)
374
        );
375
        $this->io->newLine(3);
376
    }
377
378
379
380
    private function processQueuedCandidates()
381
    {
382
        while (!empty($this->workingDirectoryCandidates)) {
383
            $candidate = array_shift($this->workingDirectoryCandidates);
384
385
            if ($candidate->isQueued()) {
386
                $this->executeCandidateScript($candidate);
387
            }
388
        }
389
        $this->io->writeln('<info>All candidates scripts were executed</info>');
390
    }
391
392
393
394
    /**
395
     * @param Candidate $candidate
396
     */
397
    private function deployCandidate(Candidate $candidate)
398
    {
399
        try {
400
            $this->executor->execute($candidate);
401
        } catch (DatabaseException $databaseException) {
402
            $this->io->error($databaseException->getMessage());
403
            $this->io->writeln(
404
                sprintf(
405
                    "<bg=yellow>%s\n\r%s</>",
406
                    $databaseException->getPrevious()->getMessage(),
407
                    $candidate->getContent()
408
                )
409
            );
410
            $this->terminate();
411
        }
412
    }
413
414
415
416
    private function finish()
417
    {
418
        $this->io->text(sprintf('Provisioning ended at %s', date('Y-m-d H:i:s')));
419
        die(0);
420
    }
421
422
423
424
    private function terminate()
425
    {
426
        $this->io->text(sprintf('Provisioning ended with error at %s', date('Y-m-d H:i:s')));
427
        die(1);
428
    }
429
}