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

ProvisionCommand::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 23
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 1

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 23
ccs 11
cts 11
cp 1
rs 9.0856
cc 1
eloc 19
nc 1
nop 9
crap 1

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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