Passed
Push — main ( c100f9...14c5bb )
by Siad
07:13
created

PDOSQLExecTask::setExpandProperties()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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