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

ProvisionCommand::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 21
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 21
ccs 10
cts 10
cp 1
rs 9.3142
c 0
b 0
f 0
cc 1
eloc 17
nc 1
nop 8
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\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