Passed
Push — main ( 1152fc...a2dce0 )
by Siad
07:08
created

PDOSQLExecTask::closeConnection()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 5
rs 10
c 0
b 0
f 0
ccs 4
cts 4
cp 1
cc 2
nc 2
nop 0
crap 2
1
<?php
2
3
/**
4
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
5
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
6
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
7
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
8
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
9
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
10
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
11
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
12
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
13
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
14
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
15
 *
16
 * This software consists of voluntary contributions made by many individuals
17
 * and is licensed under the LGPL. For more information please see
18
 * <http://phing.info>.
19
 */
20
21
namespace Phing\Task\System\Pdo;
22
23
use Exception;
24
use PDO;
25
use PDOException;
26
use PDOStatement;
27
use Phing\Exception\BuildException;
28
use Phing\Io\File;
29
use Phing\Io\IOException;
30
use Phing\Io\LogWriter;
31
use Phing\Io\Reader;
32
use Phing\Io\Writer;
33
use Phing\Project;
34
use Phing\Task\System\Condition\Condition;
35
use Phing\Type\FileList;
36
use Phing\Type\FileSet;
37
38
/**
39
 * Executes a series of SQL statements on a database using PDO.
40
 *
41
 * <p>Statements can
42
 * either be read in from a text file using the <i>src</i> attribute or from
43
 * between the enclosing SQL tags.</p>
44
 *
45
 * <p>Multiple statements can be provided, separated by semicolons (or the
46
 * defined <i>delimiter</i>). Individual lines within the statements can be
47
 * commented using either --, // or REM at the start of the line.</p>
48
 *
49
 * <p>The <i>autocommit</i> attribute specifies whether auto-commit should be
50
 * turned on or off whilst executing the statements. If auto-commit is turned
51
 * on each statement will be executed and committed. If it is turned off the
52
 * statements will all be executed as one transaction.</p>
53
 *
54
 * <p>The <i>onerror</i> attribute specifies how to proceed when an error occurs
55
 * during the execution of one of the statements.
56
 * The possible values are: <b>continue</b> execution, only show the error;
57
 * <b>stop</b> execution and commit transaction;
58
 * and <b>abort</b> execution and transaction and fail task.</p>
59
 *
60
 * This task can also be used as a Condition.
61
 *
62
 * @author  Hans Lellelid <[email protected]> (Phing)
63
 * @author  Jeff Martin <[email protected]> (Ant)
64
 * @author  Michael McCallum <[email protected]> (Ant)
65
 * @author  Tim Stephenson <[email protected]> (Ant)
66
 */
67
class PDOSQLExecTask extends PDOTask implements Condition
68
{
69
    public const DELIM_ROW = 'row';
70
    public const DELIM_NORMAL = 'normal';
71
    public const DELIM_NONE = 'none';
72
    /**
73
     * Count of how many statements were executed successfully.
74
     *
75
     * @var int
76
     */
77
    private $goodSql = 0;
78
79
    /**
80
     * Count of total number of SQL statements.
81
     *
82
     * @var int
83
     */
84
    private $totalSql = 0;
85
86
    /**
87
     * Database connection.
88
     *
89
     * @var PDO
90
     */
91
    private $conn;
92
93
    /**
94
     * Files to load.
95
     *
96
     * @var FileSet[]
97
     */
98
    private $filesets = [];
99
100
    /**
101
     * Files to load.
102
     *
103
     * @var FileList[]
104
     */
105
    private $filelists = [];
106
107
    /**
108
     * Formatter elements.
109
     *
110
     * @var PDOSQLExecFormatterElement[]
111
     */
112
    private $formatters = [];
113
114
    /**
115
     * SQL statement.
116
     *
117
     * @var PDOStatement
118
     */
119
    private $statement;
120
121
    /**
122
     * SQL input file.
123
     *
124
     * @var File
125
     */
126
    private $srcFile;
127
128
    /**
129
     * SQL input command.
130
     *
131
     * @var string
132
     */
133
    private $sqlCommand = '';
134
135
    /**
136
     * SQL transactions to perform.
137
     */
138
    private $transactions = [];
139
140
    /**
141
     * SQL Statement delimiter (for parsing files).
142
     *
143
     * @var string
144
     */
145
    private $delimiter = ';';
146
147
    /**
148
     * The delimiter type indicating whether the delimiter will
149
     * only be recognized on a line by itself.
150
     */
151
    private $delimiterType = self::DELIM_NONE;
152
153
    /**
154
     * Action to perform if an error is found.
155
     */
156
    private $onError = 'abort';
157
158
    /**
159
     * Encoding to use when reading SQL statements from a file.
160
     */
161
    private $encoding;
162
163
    /**
164
     * Fetch mode for PDO select queries.
165
     *
166
     * @var int
167
     */
168
    private $fetchMode;
169
170
    /**
171
     * Set the name of the SQL file to be run.
172
     * Required unless statements are enclosed in the build file.
173
     */
174 4
    public function setSrc(File $srcFile)
175
    {
176 4
        $this->srcFile = $srcFile;
177 4
    }
178
179
    /**
180
     * Set an inline SQL command to execute.
181
     * NB: Properties are not expanded in this text.
182
     *
183
     * @param string $sql
184
     */
185
    public function addText($sql)
186
    {
187
        $this->sqlCommand .= $sql;
188
    }
189
190
    /**
191
     * Adds a set of files (nested fileset attribute).
192
     */
193
    public function addFileset(FileSet $set)
194
    {
195
        $this->filesets[] = $set;
196
    }
197
198
    /**
199
     * Adds a set of files (nested filelist attribute).
200
     */
201
    public function addFilelist(FileList $list)
202
    {
203
        $this->filelists[] = $list;
204
    }
205
206
    /**
207
     * Creates a new PDOSQLExecFormatterElement for <formatter> element.
208
     *
209
     * @return PDOSQLExecFormatterElement
210
     */
211
    public function createFormatter()
212
    {
213
        $fe = new PDOSQLExecFormatterElement($this);
214
        $this->formatters[] = $fe;
215
216
        return $fe;
217
    }
218
219
    /**
220
     * Add a SQL transaction to execute.
221
     */
222 4
    public function createTransaction()
223
    {
224 4
        $t = new PDOSQLExecTransaction($this);
225 4
        $this->transactions[] = $t;
226
227 4
        return $t;
228
    }
229
230
    /**
231
     * Set the file encoding to use on the SQL files read in.
232
     *
233
     * @param string $encoding the encoding to use on the files
234
     */
235
    public function setEncoding($encoding)
236
    {
237
        $this->encoding = $encoding;
238
    }
239
240
    /**
241
     * Set the statement delimiter.
242
     *
243
     * <p>For example, set this to "go" and delimitertype to "ROW" for
244
     * Sybase ASE or MS SQL Server.</p>
245
     */
246 2
    public function setDelimiter(string $delimiter): void
247
    {
248 2
        $this->delimiter = $delimiter;
249 2
    }
250
251
    /**
252
     * Get the statement delimiter.
253
     */
254 4
    public function getDelimiter(): string
255
    {
256 4
        return $this->delimiter;
257
    }
258
259
    /**
260
     * Set the Delimiter type for this sql task. The delimiter type takes two
261
     * values - normal and row. Normal means that any occurrence of the delimiter
262
     * terminate the SQL command whereas with row, only a line containing just
263
     * the delimiter is recognized as the end of the command.
264
     */
265 4
    public function setDelimiterType(string $delimiterType): void
266
    {
267 4
        $this->delimiterType = $delimiterType;
268 4
    }
269
270
    /**
271
     * Action to perform when statement fails: continue, stop, or abort
272
     * optional; default &quot;abort&quot;.
273
     *
274
     * @param string $action continue|stop|abort
275
     */
276
    public function setOnerror($action): void
277
    {
278
        $this->onError = $action;
279
    }
280
281
    /**
282
     * Sets the fetch mode to use for the PDO resultset.
283
     *
284
     * @param mixed $mode the PDO fetchmode int or constant name
285
     *
286
     * @throws BuildException
287
     */
288
    public function setFetchmode($mode): void
289
    {
290
        if (is_numeric($mode)) {
291
            $this->fetchMode = (int) $mode;
292
        } else {
293
            if (defined($mode)) {
294
                $this->fetchMode = constant($mode);
295
            } else {
296
                throw new BuildException('Invalid PDO fetch mode specified: ' . $mode, $this->getLocation());
297
            }
298
        }
299
    }
300
301
    /**
302
     * Load the sql file and then execute it.
303
     *
304
     * {@inheritdoc}
305
     *
306
     * @throws BuildException
307
     */
308 4
    public function main()
309
    {
310
        // Set a default fetchmode if none was specified
311
        // (We're doing that here to prevent errors loading the class is PDO is not available.)
312 4
        if (null === $this->fetchMode) {
313 4
            $this->fetchMode = PDO::FETCH_ASSOC;
314
        }
315
316
        // Initialize the formatters here.  This ensures that any parameters passed to the formatter
317
        // element get passed along to the actual formatter object
318 4
        foreach ($this->formatters as $fe) {
319
            $fe->prepare($this->getLocation());
320
        }
321
322 4
        $savedTransaction = [];
323 4
        for ($i = 0, $size = count($this->transactions); $i < $size; ++$i) {
324
            $savedTransaction[] = clone $this->transactions[$i];
325
        }
326
327 4
        $savedSqlCommand = $this->sqlCommand;
328
329 4
        $this->sqlCommand = trim($this->sqlCommand);
330
331
        try {
332
            if (
333 4
                null === $this->srcFile
334 4
                && '' === $this->sqlCommand
335 4
                && empty($this->filesets)
336 4
                && empty($this->filelists)
337 4
                && 0 === count($this->transactions)
338
            ) {
339
                throw new BuildException(
340
                    'Source file or fileset/filelist, '
341
                    . 'transactions or sql statement '
342
                    . 'must be set!',
343
                    $this->getLocation()
344
                );
345
            }
346
347 4
            if (null !== $this->srcFile && !$this->srcFile->exists()) {
348
                throw new BuildException('Source file does not exist!', $this->getLocation());
349
            }
350
351
            // deal with the filesets
352 4
            foreach ($this->filesets as $fs) {
353
                $ds = $fs->getDirectoryScanner($this->project);
354
                $srcDir = $fs->getDir($this->project);
355
                $srcFiles = $ds->getIncludedFiles();
356
                // Make a transaction for each file
357
                foreach ($srcFiles as $srcFile) {
358
                    $t = $this->createTransaction();
359
                    $t->setSrc(new File($srcDir, $srcFile));
360
                }
361
            }
362
363
            // process filelists
364 4
            foreach ($this->filelists as $fl) {
365
                $srcDir = $fl->getDir($this->project);
366
                $srcFiles = $fl->getFiles($this->project);
367
                // Make a transaction for each file
368
                foreach ($srcFiles as $srcFile) {
369
                    $t = $this->createTransaction();
370
                    $t->setSrc(new File($srcDir, $srcFile));
371
                }
372
            }
373
374
            // Make a transaction group for the outer command
375 4
            $t = $this->createTransaction();
376 4
            if ($this->srcFile) {
377 4
                $t->setSrc($this->srcFile);
378
            }
379 4
            $t->addText($this->sqlCommand);
380 4
            $this->conn = $this->getConnection();
381
382
            try {
383 4
                $this->statement = null;
384
385
                // Initialize the formatters.
386 4
                $this->initFormatters();
387
388
                try {
389
                    // Process all transactions
390 4
                    for ($i = 0, $size = count($this->transactions); $i < $size; ++$i) {
391 4
                        if (!$this->isAutocommit()) {
392 1
                            $this->log('Beginning transaction', Project::MSG_VERBOSE);
393 1
                            $this->conn->beginTransaction();
394
                        }
395 4
                        $this->transactions[$i]->runTransaction();
396 4
                        if (!$this->isAutocommit()) {
397 1
                            $this->log('Committing transaction', Project::MSG_VERBOSE);
398 1
                            $this->conn->commit();
399
                        }
400
                    }
401
                } catch (Exception $e) {
402
                    $this->closeConnection();
403
404 4
                    throw $e;
405
                }
406
            } catch (IOException $e) {
407
                if (!$this->isAutocommit() && null !== $this->conn && 'abort' === $this->onError) {
408
                    try {
409
                        $this->conn->rollback();
410
                    } catch (PDOException $ex) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
411
                    }
412
                }
413
                $this->closeConnection();
414
415
                throw new BuildException($e->getMessage(), $this->getLocation());
416
            } catch (PDOException $e) {
417
                if (!$this->isAutocommit() && null !== $this->conn && 'abort' === $this->onError) {
418
                    try {
419
                        $this->conn->rollback();
420
                    } catch (PDOException $ex) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
421
                    }
422
                }
423
                $this->closeConnection();
424
425
                throw new BuildException($e->getMessage(), $this->getLocation());
426
            }
427
428
            // Close the formatters.
429 4
            $this->closeFormatters();
430
431 4
            $this->log(
432 4
                $this->goodSql . ' of ' . $this->totalSql .
433 4
                ' SQL statements executed successfully'
434
            );
435
        } catch (Exception $e) {
436
            throw $e;
437 4
        } finally {
438 4
            $this->transactions = $savedTransaction;
439 4
            $this->sqlCommand = $savedSqlCommand;
440 4
            $this->closeConnection();
441
        }
442 4
    }
443
444
    /**
445
     * read in lines and execute them.
446
     *
447
     * @throws BuildException
448
     */
449 4
    public function runStatements(Reader $reader)
450
    {
451 4
        if (self::DELIM_NONE == $this->delimiterType) {
452
            $splitter = new DummyPDOQuerySplitter($this, $reader);
453 4
        } elseif (self::DELIM_NORMAL == $this->delimiterType && 0 === strpos((string) $this->getUrl(), 'pgsql:')) {
454 1
            $splitter = new PgsqlPDOQuerySplitter($this, $reader);
455
        } else {
456 3
            $splitter = new DefaultPDOQuerySplitter($this, $reader, $this->delimiterType);
457
        }
458
459
        try {
460 4
            while (null !== ($query = $splitter->nextQuery())) {
461 4
                $this->log('SQL: ' . $query, Project::MSG_VERBOSE);
462 4
                $this->execSQL($query);
463
            }
464
        } catch (PDOException $e) {
465
            throw $e;
466
        }
467 4
    }
468
469
    /**
470
     * PDOSQLExecTask as condition.
471
     *
472
     * Returns false when the database connection fails, and true otherwise.
473
     * This method only uses three properties: url (required), userId and
474
     * password.
475
     *
476
     * The database connection is not stored in a variable, this allow to
477
     * immediately close the connections since there's no reference to it.
478
     *
479
     * @author Jawira Portugal <[email protected]>
480
     */
481 6
    public function evaluate(): bool
482
    {
483 6
        if (empty($this->getUrl())) {
484 1
            throw new BuildException('url is required');
485
        }
486
487 5
        $this->log('Trying to reach ' . $this->getUrl(), Project::MSG_DEBUG);
488
489
        try {
490 5
            new PDO($this->getUrl(), $this->getUserId(), $this->getPassword());
491 4
        } catch (PDOException $ex) {
492 4
            $this->log($ex->getMessage(), Project::MSG_VERBOSE);
493
494 4
            return false;
495
        }
496
497 1
        $this->log('Successful connection to ' . $this->getUrl(), Project::MSG_DEBUG);
498
499 1
        return true;
500
    }
501
502
    /**
503
     * Whether the passed-in SQL statement is a SELECT statement.
504
     * This does a pretty simple match, checking to see if statement starts with
505
     * 'select' (but not 'select into').
506
     *
507
     * @param string $sql
508
     *
509
     * @return bool whether specified SQL looks like a SELECT query
510
     */
511
    protected function isSelectSql($sql)
512
    {
513
        $sql = trim($sql);
514
515
        return 0 === stripos($sql, 'select') && 0 !== stripos($sql, 'select into ');
516
    }
517
518
    /**
519
     * Exec the sql statement.
520
     *
521
     * @param string $sql
522
     *
523
     * @throws BuildException
524
     * @throws Exception
525
     */
526 1
    protected function execSQL($sql)
527
    {
528
        // Check and ignore empty statements
529 1
        if ('' == trim($sql)) {
530
            return;
531
        }
532
533
        try {
534 1
            ++$this->totalSql;
535
536 1
            $this->statement = $this->conn->query($sql);
537 1
            $this->log($this->statement->rowCount() . ' rows affected', Project::MSG_VERBOSE);
538
539
            // only call processResults() for statements that return actual data (such as 'select')
540 1
            if ($this->statement->columnCount() > 0) {
541
                $this->processResults();
542
            }
543
544 1
            $this->statement->closeCursor();
545 1
            $this->statement = null;
546
547 1
            ++$this->goodSql;
548
        } catch (PDOException $e) {
549
            $this->log('Failed to execute: ' . $sql, Project::MSG_ERR);
550
            if ('continue' != $this->onError) {
551
                throw new BuildException('Failed to execute SQL', $e);
552
            }
553
            $this->log($e->getMessage(), Project::MSG_ERR);
554
        }
555 1
    }
556
557
    /**
558
     * Returns configured PDOResultFormatter objects
559
     * (which were created from PDOSQLExecFormatterElement objects).
560
     *
561
     * @return array PDOResultFormatter[]
562
     */
563 4
    protected function getConfiguredFormatters()
564
    {
565 4
        $formatters = [];
566 4
        foreach ($this->formatters as $fe) {
567
            $formatters[] = $fe->getFormatter();
568
        }
569
570 4
        return $formatters;
571
    }
572
573
    /**
574
     * Initialize the formatters.
575
     */
576 4
    protected function initFormatters()
577
    {
578 4
        $formatters = $this->getConfiguredFormatters();
579 4
        foreach ($formatters as $formatter) {
580
            $formatter->initialize();
581
        }
582 4
    }
583
584
    /**
585
     * Run cleanup and close formatters.
586
     */
587 4
    protected function closeFormatters()
588
    {
589 4
        $formatters = $this->getConfiguredFormatters();
590 4
        foreach ($formatters as $formatter) {
591
            $formatter->close();
592
        }
593 4
    }
594
595
    /**
596
     * Passes results from query to any formatters.
597
     *
598
     * @throws PDOException
599
     */
600
    protected function processResults()
601
    {
602
        $this->log('Processing new result set.', Project::MSG_VERBOSE);
603
604
        $formatters = $this->getConfiguredFormatters();
605
606
        try {
607
            while ($row = $this->statement->fetch($this->fetchMode)) {
608
                foreach ($formatters as $formatter) {
609
                    $formatter->processRow($row);
610
                }
611
            }
612
        } catch (Exception $x) {
613
            $this->log('Error processing reults: ' . $x->getMessage(), Project::MSG_ERR);
614
            foreach ($formatters as $formatter) {
615
                $formatter->close();
616
            }
617
618
            throw $x;
619
        }
620
    }
621
622
    /**
623
     * Closes current connection.
624
     */
625 4
    protected function closeConnection(): void
626
    {
627 4
        if ($this->conn) {
628 1
            unset($this->conn);
629 1
            $this->conn = null;
630
        }
631 4
    }
632
633
    /**
634
     * Gets a default output writer for this task.
635
     *
636
     * @return Writer
637
     */
638
    private function getDefaultOutput()
0 ignored issues
show
Unused Code introduced by
The method getDefaultOutput() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
639
    {
640
        return new LogWriter($this);
641
    }
642
}
643