Test Setup Failed
Pull Request — master (#20)
by
unknown
12:21
created

ProvisionCommand::execute()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
dl 0
loc 20
rs 9.6
c 0
b 0
f 0
ccs 0
cts 9
cp 0
cc 3
nc 4
nop 2
crap 12
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\Config\ProvisionConfig;
14
use Tworzenieweb\SqlProvisioner\Controller\ProvisionDispatcher;
15
use Tworzenieweb\SqlProvisioner\Database\Connection;
16
use Tworzenieweb\SqlProvisioner\Filesystem\Exception;
17
use Tworzenieweb\SqlProvisioner\Filesystem\WorkingDirectory;
18
use Tworzenieweb\SqlProvisioner\Model\Candidate;
19
use Tworzenieweb\SqlProvisioner\Model\CandidateBuilder;
20
use Tworzenieweb\SqlProvisioner\Table\DataRowsBuilder;
21
22
/**
23
 * @author  Luke Adamczewski
24
 * @package Tworzenieweb\SqlProvisioner\Command
25
 */
26
class ProvisionCommand extends Command
27
{
28
    const HELP_MESSAGE = <<<'EOF'
29
The <info>%command.name% [path-to-folder]</info> command will scan the content of [path-to-folder] directory.
30
 
31
The script will look for <info>.env</info> file containing connection information in format:
32
<comment>
33
DATABASE_USER=[user]
34
DATABASE_PASSWORD=[password]
35
DATABASE_HOST=[host]
36
DATABASE_PORT=[port]
37
DATABASE_NAME=[database]
38
PROVISIONING_TABLE=changelog_database_deployments
39
PROVISIONING_TABLE_CANDIDATE_NUMBER_COLUMN=deploy_script_number
40
</comment>
41
42
If you want to create initial .env use <info>--init</info>
43
44
<info>%command.name% --init [path-to-folder]</info>
45
46
The next step is searching for sql files and trying to queue them in numerical order.
47
First n-th digits of a filename will be treated as candidate number. 
48
This will be used then to check in database if a certain file was already deployed (PROVISIONING_TABLE_CANDIDATE_NUMBER_COLUMN).
49
Before the insert, it will print the formatted output of a file and result of internal syntax check.
50
Then you can either skip or execute each.
51
52
If you would like to skip already provisioned candidates use <info>--skip-provisioned</info>
53
If you would like to skip syntax checking (for speed purpose) of candidates use <info>--skip-syntax-check</info>
54
55
EOF;
56
57
    /** @var Candidate[] */
58
    private $workingDirectoryCandidates = [];
59
60
    /** @var WorkingDirectory */
61
    private $workingDirectory;
62
63
    /** @var SymfonyStyle */
64
    private $io;
65
66
    /** @var Connection */
67
    private $connection;
68
69
    /** @var boolean */
70
    private $skipProvisionedCandidates = false;
71
72
    /** @var CandidateBuilder */
73
    private $candidateBuilder;
74
75
    /** @var DataRowsBuilder */
76
    private $dataRowsBuilder;
77
78
    /** @var integer */
79
    private $queuedCandidatesCount = 0;
80
81
    /** @var array */
82
    private $errorMessages = [];
83
84
    /** @var ProvisionDispatcher */
85
    private $dispatcher;
86
87
    /** @var ProvisionConfig */
88
    private $config;
89
90
91
    /**
92
     * @param string              $name
93
     * @param WorkingDirectory    $workingDirectory
94
     * @param Connection          $connection
95
     * @param CandidateBuilder    $candidateBuilder
96 1
     * @param DataRowsBuilder     $dataRowsBuilder
97
     * @param ProvisionDispatcher $dispatcher
98
     * @param ProvisionConfig     $config
99
     */
100
    public function __construct(
101
        $name,
102
        WorkingDirectory $workingDirectory,
103
        Connection $connection,
104
        CandidateBuilder $candidateBuilder,
105 1
        DataRowsBuilder $dataRowsBuilder,
106 1
        ProvisionDispatcher $dispatcher,
107 1
        ProvisionConfig $config
108 1
    ) {
109 1
        $this->workingDirectory = $workingDirectory;
110
        $this->connection       = $connection;
111 1
        $this->candidateBuilder = $candidateBuilder;
112 1
        $this->dataRowsBuilder  = $dataRowsBuilder;
113
        $this->dispatcher       = $dispatcher;
114
        $this->config           = $config;
115 1
116
        parent::__construct($name);
117 1
    }
118 1
119 1
120 1
    protected function configure()
121 1
    {
122 1
        $this
123 1
            ->setDescription('Execute the content of *.sql files from given')
124 1
            ->setHelp(self::HELP_MESSAGE);
125
        $this->addOption('init', null, InputOption::VALUE_NONE, 'Initialize .env in given directory');
126 1
        $this->addOption(
127 1
            'skip-provisioned',
128 1
            null,
129 1
            InputOption::VALUE_NONE,
130 1
            'Skip provisioned candidates from printing'
131
        );
132 1
        $this->addOption(
133 1
            'skip-syntax-check',
134 1
            null,
135
            InputOption::VALUE_NONE,
136
            'Skip executing of sql syntax check for each entry'
137
        );
138
        $this->addOption(
139
            'skip-email',
140
            null,
141
            InputOption::VALUE_NONE,
142
            'Skip email notification after provision is done'
143
        );
144
        $this->addOption(
145
            'force',
146
            'f',
147
            InputOption::VALUE_NONE,
148
            'Execute provision candidates without asking for confirmation'
149
        );
150
        $this->addOption(
151
            'env-file',
152
            null,
153
            InputOption::VALUE_OPTIONAL,
154
            'Environment variables file path. Use this env file to seed base environment variables.'
155
        );
156
        $this->addArgument('path', InputArgument::REQUIRED, 'Path to dbdeploys folder');
157
    }
158
159
    protected function initialize(InputInterface $input, OutputInterface $output)
160
    {
161
        if ($envFile = $input->getOption('env-file')) {
162
            $this->config->withEnvPath($envFile);
163
        }
164
165
        if ($input->getOption('force')) {
166
            $this->config->force();
167
        }
168
169
        if ($input->getOption('skip-email')) {
170
            $this->config->skipEmail();
171
        }
172
173
        if ($input->getOption('skip-syntax-check')) {
174
            $this->config->skipSyntaxCheck();
175
        }
176
177
        if ($input->getOption('skip-provisioned')) {
178
            $this->config->skipProvisioned();
179
        }
180
181
        $this->config->load();
182
    }
183
184
    /**
185
     * @param InputInterface  $input
186
     * @param OutputInterface $output
187
     *
188
     * @return int
189
     */
190
    protected function execute(InputInterface $input, OutputInterface $output)
191
    {
192
        $this->start($input, $output);
193
        $this->io->section('Working directory processing');
194
        $this->io->comment(sprintf('Using env file from [%s]', $this->config->getEnvPath()));
195
196
        if ($this->config->isSkipProvisioned()) {
197
            $this->skipProvisionedCandidates = true;
198
            $this->io->warning('Hiding of provisioned candidates ENABLED');
199
        }
200
201
        if ($this->config->isSkipSyntaxCheck()) {
202
            $this->dispatcher->skipSyntaxCheck();
203
        }
204
205
        $this->processWorkingDirectory($input);
206
        $this->processCandidates();
207
208
        return 0;
209
    }
210
211
212
    /**
213
     * @param InputInterface  $input
214
     * @param OutputInterface $output
215
     */
216
    protected function start(InputInterface $input, OutputInterface $output)
217
    {
218
        $this->io = new SymfonyStyle($input, $output);
219
        $this->dispatcher->setInputOutput($this->io);
220
221
        $this->io->title('SQL Provisioner');
222
        $this->io->block(sprintf('Provisioning started at %s', date('Y-m-d H:i:s')));
223
    }
224
225
226
    protected function fetchCandidates()
227
    {
228
        $this->iterateOverWorkingDirectory();
229
230
        if (!empty($this->errorMessages)) {
231
            $this->showSyntaxErrors();
232
        }
233
234
        if (!$this->queuedCandidatesCount) {
235
            $this->io->block('All candidates scripts were executed already.');
236
            $this->dispatcher->finalizeAndExit();
237
        }
238
    }
239
240
241
    /**
242
     * @param SplFileInfo $candidateFile
243
     */
244
    protected function processCandidateFile($candidateFile)
245
    {
246
        $candidate = $this->candidateBuilder->build($candidateFile);
247
        array_push($this->workingDirectoryCandidates, $candidate);
248
249
        try {
250
            $this->dispatcher->validate($candidate);
251
252
            // can be also ignored but without error
253
            if ($candidate->isQueued()) {
254
                $this->queuedCandidatesCount++;
255
            }
256
        } catch (RuntimeException $validationError) {
257
            if ($validationError->getMessage()) {
258
                array_push($this->errorMessages, $validationError->getMessage());
259
            }
260
        }
261
    }
262
263
264
    protected function iterateOverWorkingDirectory()
265
    {
266
        foreach ($this->workingDirectory->getCandidates() as $candidateFile) {
267
            $this->processCandidateFile($candidateFile);
268
        }
269
270
        $this->io->text(sprintf('<info>%d</info> files found', count($this->workingDirectoryCandidates)));
271
272
        if (count($this->workingDirectoryCandidates) === 0) {
273
            throw Exception::noFilesInDirectory($this->workingDirectory);
274
        }
275
    }
276
277
278
    protected function showSyntaxErrors()
279
    {
280
        $this->io->warning(sprintf('Detected %d syntax checking issues', count($this->errorMessages)));
281
        $this->printAllCandidates();
282
        $this->io->warning(implode("\n", $this->errorMessages));
283
        $this->dispatcher->finalizeAndExit();
284
    }
285
286
287
    /**
288
     * @param InputInterface $input
289
     */
290
    protected function processWorkingDirectory(InputInterface $input)
291
    {
292
        $this->workingDirectory = $this->workingDirectory->cd($input->getArgument('path'));
0 ignored issues
show
Bug introduced by
It seems like $input->getArgument('path') targeting Symfony\Component\Consol...nterface::getArgument() can also be of type array<integer,string> or null; however, Tworzenieweb\SqlProvisio...\WorkingDirectory::cd() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
293
        $this->loadOrCreateEnvironment($input);
294
        $this->io->success('DONE');
295
    }
296
297
298
    /**
299
     * @param InputInterface $input
300
     */
301
    private function loadOrCreateEnvironment(InputInterface $input)
302
    {
303
        if ($input->getOption('init')) {
304
            $this->workingDirectory->createEnvironmentFile();
305
            $this->io->success(sprintf('Initial .env file created in %s', $this->workingDirectory));
306
            die(0);
307
        }
308
309
        $this->workingDirectory->loadEnvironment();
310
    }
311
312
313
    private function setConnectionParameters()
314
    {
315
        $this->connection->useMysql($_ENV['DATABASE_HOST'], $_ENV['DATABASE_PORT'], $_ENV['DATABASE_NAME'],
316
                                    $_ENV['DATABASE_USER'], $_ENV['DATABASE_PASSWORD']);
317
        $this->connection->setProvisioningTable($_ENV['PROVISIONING_TABLE']);
318
        $this->connection->setCriteriaColumn($_ENV['PROVISIONING_TABLE_CANDIDATE_NUMBER_COLUMN']);
319
320
        $this->io->success(sprintf('Connection with `%s` established', $_ENV['DATABASE_NAME']));
321
    }
322
323
324
    private function processCandidates()
325
    {
326
        $this->io->newLine(2);
327
        $this->io->section('Candidates processing');
328
329
        $this->setConnectionParameters();
330
        $this->fetchCandidates();
331
        $this->printAllCandidates();
332
        $this->dispatcher->deploy($this->workingDirectoryCandidates, $this->queuedCandidatesCount);
333
    }
334
335
336
    private function printAllCandidates()
337
    {
338
        $this->io->table(
339
            DataRowsBuilder::TABLE_HEADERS,
340
            $this->dataRowsBuilder->build(
341
                $this->workingDirectoryCandidates, $this->skipProvisionedCandidates)
342
        );
343
        $this->io->newLine(3);
344
    }
345
}
346