ProvisionCommand::execute()   A
last analyzed

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'));
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(getenv('DATABASE_HOST'), getenv('DATABASE_PORT'), getenv('DATABASE_NAME'),
316
                                    getenv('DATABASE_USER'), getenv('DATABASE_PASSWORD'));
317
        $this->connection->setProvisioningTable(getenv('PROVISIONING_TABLE'));
318
        $this->connection->setCriteriaColumn(getenv('PROVISIONING_TABLE_CANDIDATE_NUMBER_COLUMN'));
319
320
        $this->io->success(sprintf('Connection with `%s` established', getenv('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