PDOSQLExecTask::setErrorProp()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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