Passed
Push — main ( bb891e...7e6e75 )
by Siad
09:02
created

PDOSQLExecTask::setErrorProp()   A

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