Passed
Push — main ( 14c5bb...4a341e )
by Siad
07:40
created

PDOSQLExecTask::addFileset()   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 1
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
     * @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 14
    public function createTransaction()
242
    {
243 14
        $t = new PDOSQLExecTransaction($this);
244 14
        $this->transactions[] = $t;
245
246 14
        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 14
    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 14
        if (null === $this->fetchMode) {
357 14
            $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 14
        foreach ($this->formatters as $fe) {
363 3
            $fe->prepare($this->getLocation());
364
        }
365
366 14
        $savedTransaction = [];
367 14
        for ($i = 0, $size = count($this->transactions); $i < $size; ++$i) {
368 8
            $savedTransaction[] = clone $this->transactions[$i];
369
        }
370
371 14
        $savedSqlCommand = $this->sqlCommand;
372
373 14
        $this->sqlCommand = trim($this->sqlCommand);
374
375
        try {
376
            if (
377 14
                null === $this->srcFile
378 14
                && '' === $this->sqlCommand
379 14
                && empty($this->filesets)
380 14
                && empty($this->filelists)
381 14
                && 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 14
            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 14
            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 14
            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 14
            $t = $this->createTransaction();
420 14
            if ($this->srcFile) {
421 4
                $t->setSrc($this->srcFile);
422
            }
423 14
            $t->addText($this->sqlCommand);
424
425 14
            $this->conn = $this->getConnection();
426 14
            if ($this->conn === null) {
427 1
                return;
428
            }
429
430
            try {
431 13
                $this->statement = null;
432
433
                // Initialize the formatters.
434 13
                $this->initFormatters();
435
436
                try {
437
                    // Process all transactions
438 13
                    for ($i = 0, $size = count($this->transactions); $i < $size; ++$i) {
439 13
                        if (!$this->isAutocommit() || $this->conn->inTransaction()) {
440 10
                            $this->log('Beginning transaction', Project::MSG_VERBOSE);
441 10
                            $this->conn->beginTransaction();
442
                        }
443 13
                        $this->transactions[$i]->runTransaction();
444 13
                        if (!$this->isAutocommit() || $this->conn->inTransaction()) {
445 10
                            $this->log('Committing transaction', Project::MSG_VERBOSE);
446 10
                            $this->conn->commit();
447
                        }
448
                    }
449
                } catch (Exception $e) {
450
                    $this->closeConnection();
451
452 13
                    throw new BuildException($e);
453
                }
454
            } catch (IOException | PDOException $e) {
455
                $this->closeQuietly();
456
                $this->setErrorProp();
457
                if ('abort' === $this->onError) {
458
                    throw new BuildException($e->getMessage(), $this->getLocation());
459
                }
460
            }
461
462
            // Close the formatters.
463 13
            $this->closeFormatters();
464
465 13
            $this->log(
466 13
                $this->goodSql . ' of ' . $this->totalSql .
467 13
                ' SQL statements executed successfully'
468
            );
469 13
            $this->setStatementCountProp($this->goodSql);
470
        } catch (Exception $e) {
471
            throw new BuildException($e);
472 13
        } finally {
473 14
            $this->transactions = $savedTransaction;
474 14
            $this->sqlCommand = $savedSqlCommand;
475 14
            $this->closeConnection();
476
        }
477 13
    }
478
479
    /**
480
     * read in lines and execute them.
481
     *
482
     * @throws BuildException
483
     */
484 13
    public function runStatements(Reader $reader): void
485
    {
486 13
        if (self::DELIM_NONE === $this->delimiterType) {
487 9
            $splitter = new DummyPDOQuerySplitter($this, $reader);
488 4
        } elseif (self::DELIM_NORMAL === $this->delimiterType && 0 === strpos((string) $this->getUrl(), 'pgsql:')) {
489 1
            $splitter = new PgsqlPDOQuerySplitter($this, $reader);
490
        } else {
491 3
            $splitter = new DefaultPDOQuerySplitter($this, $reader, $this->delimiterType);
492
        }
493
494 13
        $splitter->setExpandProperties($this->expandProperties);
495 13
        $splitter->setKeepformat($this->keepformat);
496
497
        try {
498 13
            while (null !== ($query = $splitter->nextQuery())) {
499 13
                $this->log('SQL: ' . $query, Project::MSG_VERBOSE);
500 13
                $this->execSQL($query);
501
            }
502
        } catch (PDOException $e) {
503
            throw new BuildException($e);
504
        }
505 13
    }
506
507
    /**
508
     * PDOSQLExecTask as condition.
509
     *
510
     * Returns false when the database connection fails, and true otherwise.
511
     * This method only uses three properties: url (required), userId and
512
     * password.
513
     *
514
     * The database connection is not stored in a variable, this allow to
515
     * immediately close the connections since there's no reference to it.
516
     *
517
     * @author Jawira Portugal <[email protected]>
518
     */
519 6
    public function evaluate(): bool
520
    {
521 6
        if (empty($this->getUrl())) {
522 1
            throw new BuildException('url is required');
523
        }
524
525 5
        $this->log('Trying to reach ' . $this->getUrl(), Project::MSG_DEBUG);
526
527
        try {
528 5
            new PDO($this->getUrl(), $this->getUserId(), $this->getPassword());
529 4
        } catch (PDOException $ex) {
530 4
            $this->log($ex->getMessage(), Project::MSG_VERBOSE);
531
532 4
            return false;
533
        }
534
535 1
        $this->log('Successful connection to ' . $this->getUrl(), Project::MSG_DEBUG);
536
537 1
        return true;
538
    }
539
540
    /**
541
     * Whether the passed-in SQL statement is a SELECT statement.
542
     * This does a pretty simple match, checking to see if statement starts with
543
     * 'select' (but not 'select into').
544
     *
545
     * @param string $sql
546
     *
547
     * @return bool whether specified SQL looks like a SELECT query
548
     */
549
    protected function isSelectSql($sql): bool
550
    {
551
        $sql = trim($sql);
552
553
        return 0 === stripos($sql, 'select') && 0 !== stripos($sql, 'select into ');
554
    }
555
556
    /**
557
     * Exec the sql statement.
558
     *
559
     * @param string $sql
560
     *
561
     * @throws BuildException
562
     */
563 10
    protected function execSQL($sql): void
564
    {
565
        // Check and ignore empty statements
566 10
        if (empty(trim($sql))) {
567
            return;
568
        }
569
570
        try {
571 10
            ++$this->totalSql;
572
573 10
            $this->statement = $this->conn->query($sql);
574
575
            // only call processResults() for statements that return actual data (such as 'select')
576 8
            if ($this->statement->columnCount() > 0) {
577 1
                $this->processResults();
578
            }
579
580 8
            $this->statement->closeCursor();
581 8
            $this->statement = null;
582
583 8
            ++$this->goodSql;
584 2
        } catch (PDOException $e) {
585 2
            $this->log('Failed to execute: ' . $sql, Project::MSG_ERR);
586 2
            $this->setErrorProp();
587 2
            if ('abort' !== $this->onError) {
588 2
                $this->log((string) $e, Project::MSG_ERR);
589
            }
590 2
            if ('continue' !== $this->onError) {
591
                throw new BuildException('Failed to execute SQL', $e);
592
            }
593 2
            $this->log($e->getMessage(), Project::MSG_ERR);
594
        }
595 10
    }
596
597
    /**
598
     * Returns configured PDOResultFormatter objects
599
     * (which were created from PDOSQLExecFormatterElement objects).
600
     *
601
     * @return PDOResultFormatter[]
602
     */
603 13
    protected function getConfiguredFormatters(): array
604
    {
605 13
        $formatters = [];
606 13
        foreach ($this->formatters as $fe) {
607 3
            $formatter = $fe->getFormatter();
608 3
            if ($formatter instanceof PlainPDOResultFormatter) {
609 2
                $formatter->setStatementCounter($this->goodSql);
610
            }
611 3
            $formatters[] = $formatter;
612
        }
613
614 13
        return $formatters;
615
    }
616
617
    /**
618
     * Initialize the formatters.
619
     */
620 13
    protected function initFormatters(): void
621
    {
622 13
        $formatters = $this->getConfiguredFormatters();
623 13
        foreach ($formatters as $formatter) {
624 3
            $formatter->initialize();
625
        }
626 13
    }
627
628
    /**
629
     * Run cleanup and close formatters.
630
     */
631 13
    protected function closeFormatters(): void
632
    {
633 13
        $formatters = $this->getConfiguredFormatters();
634 13
        foreach ($formatters as $formatter) {
635 3
            $formatter->close();
636
        }
637 13
    }
638
639
    /**
640
     * Passes results from query to any formatters.
641
     *
642
     * @throws PDOException
643
     */
644 1
    protected function processResults(): void
645
    {
646 1
        $this->log('Processing new result set.', Project::MSG_VERBOSE);
647
648 1
        $formatters = $this->getConfiguredFormatters();
649
650
        try {
651 1
            while ($row = $this->statement->fetch($this->fetchMode)) {
652 1
                foreach ($formatters as $formatter) {
653 1
                    $formatter->processRow($row);
654
                }
655
            }
656
        } catch (Exception $x) {
657
            $this->log('Error processing results: ' . $x->getMessage(), Project::MSG_ERR);
658
            foreach ($formatters as $formatter) {
659
                $formatter->close();
660
            }
661
662
            throw new BuildException($x);
663
        }
664 1
    }
665
666
    /**
667
     * Closes current connection.
668
     */
669 14
    protected function closeConnection(): void
670
    {
671 14
        if ($this->conn) {
672 13
            unset($this->conn);
673 13
            $this->conn = null;
674
        }
675 14
    }
676
677 2
    final protected function setErrorProp(): void
678
    {
679 2
        $this->setProperty($this->errorProperty, 'true');
680 2
    }
681
682 13
    final protected function setStatementCountProp(int $statementCount): void
683
    {
684 13
        $this->setProperty($this->statementCountProperty, (string) $statementCount);
685 13
    }
686
687
    /**
688
     * @param string|null $name
689
     * @param string $value
690
     */
691 13
    private function setProperty(?string $name, string $value): void
692
    {
693 13
        if ($name !== null) {
694 2
            $this->getProject()->setNewProperty($name, $value);
695
        }
696 13
    }
697
698
    /**
699
     * Closes an unused connection after an error and doesn't rethrow
700
     * a possible PDOException
701
     */
702
    private function closeQuietly(): void
703
    {
704
        if (null !== $this->conn && 'abort' === $this->onError && !$this->isAutocommit()) {
705
            try {
706
                $this->conn->rollback();
707
            } catch (PDOException $ex) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
708
            }
709
        }
710
        $this->closeConnection();
711
    }
712
}
713