Passed
Push — master ( 30bcbc...eba4fe )
by Siad
06:53
created

PDOSQLExecTask::runStatements()   A

Complexity

Conditions 6
Paths 18

Size

Total Lines 17
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 6.73

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 12
nc 18
nop 1
dl 0
loc 17
ccs 8
cts 11
cp 0.7272
crap 6.73
rs 9.2222
c 1
b 0
f 0
1
<?php
2
/**
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the LGPL. For more information please see
17
 * <http://phing.info>.
18
 */
19
20
/**
21
 * Executes a series of SQL statements on a database using PDO.
22
 *
23
 * <p>Statements can
24
 * either be read in from a text file using the <i>src</i> attribute or from
25
 * between the enclosing SQL tags.</p>
26
 *
27
 * <p>Multiple statements can be provided, separated by semicolons (or the
28
 * defined <i>delimiter</i>). Individual lines within the statements can be
29
 * commented using either --, // or REM at the start of the line.</p>
30
 *
31
 * <p>The <i>autocommit</i> attribute specifies whether auto-commit should be
32
 * turned on or off whilst executing the statements. If auto-commit is turned
33
 * on each statement will be executed and committed. If it is turned off the
34
 * statements will all be executed as one transaction.</p>
35
 *
36
 * <p>The <i>onerror</i> attribute specifies how to proceed when an error occurs
37
 * during the execution of one of the statements.
38
 * The possible values are: <b>continue</b> execution, only show the error;
39
 * <b>stop</b> execution and commit transaction;
40
 * and <b>abort</b> execution and transaction and fail task.</p>
41
 *
42
 * This task can also be used as a Condition.
43
 *
44
 * @author  Hans Lellelid <[email protected]> (Phing)
45
 * @author  Jeff Martin <[email protected]> (Ant)
46
 * @author  Michael McCallum <[email protected]> (Ant)
47
 * @author  Tim Stephenson <[email protected]> (Ant)
48
 * @package phing.tasks.ext.pdo
49
 */
50
class PDOSQLExecTask extends PDOTask implements Condition
51
{
52
    /**
53
     * Count of how many statements were executed successfully.
54
     *
55
     * @var int
56
     */
57
    private $goodSql = 0;
58
59
    /**
60
     * Count of total number of SQL statements.
61
     *
62
     * @var int
63
     */
64
    private $totalSql = 0;
65
66
    const DELIM_ROW = "row";
67
    const DELIM_NORMAL = "normal";
68
    const DELIM_NONE = "none";
69
70
    /**
71
     * Database connection
72
     *
73
     * @var PDO
74
     */
75
    private $conn = null;
76
77
    /**
78
     * Files to load
79
     *
80
     * @var FileSet[]
81
     */
82
    private $filesets = [];
83
84
    /**
85
     * Files to load
86
     *
87
     * @var FileList[]
88
     */
89
    private $filelists = [];
90
91
    /**
92
     * Formatter elements.
93
     *
94
     * @var PDOSQLExecFormatterElement[]
95
     */
96
    private $formatters = [];
97
98
    /**
99
     * SQL statement
100
     *
101
     * @var PDOStatement
102
     */
103
    private $statement;
104
105
    /**
106
     * SQL input file
107
     *
108
     * @var PhingFile
109
     */
110
    private $srcFile;
111
112
    /**
113
     * SQL input command
114
     *
115
     * @var string
116
     */
117
    private $sqlCommand = "";
118
119
    /**
120
     * SQL transactions to perform
121
     */
122
    private $transactions = [];
123
124
    /**
125
     * SQL Statement delimiter (for parsing files)
126
     *
127
     * @var string
128
     */
129
    private $delimiter = ";";
130
131
    /**
132
     * The delimiter type indicating whether the delimiter will
133
     * only be recognized on a line by itself
134
     */
135
    private $delimiterType = self::DELIM_NONE;
136
137
    /**
138
     * Action to perform if an error is found
139
     **/
140
    private $onError = "abort";
141
142
    /**
143
     * Encoding to use when reading SQL statements from a file
144
     */
145
    private $encoding = null;
146
147
    /**
148
     * Fetch mode for PDO select queries.
149
     *
150
     * @var int
151
     */
152
    private $fetchMode;
153
154
    /**
155
     * Set the name of the SQL file to be run.
156
     * Required unless statements are enclosed in the build file
157
     *
158
     * @param PhingFile $srcFile
159
     */
160 3
    public function setSrc(PhingFile $srcFile)
161
    {
162 3
        $this->srcFile = $srcFile;
163 3
    }
164
165
    /**
166
     * Set an inline SQL command to execute.
167
     * NB: Properties are not expanded in this text.
168
     *
169
     * @param $sql
170
     */
171
    public function addText($sql)
172
    {
173
        $this->sqlCommand .= $sql;
174
    }
175
176
    /**
177
     * Adds a set of files (nested fileset attribute).
178
     *
179
     * @param FileSet $set
180
     */
181
    public function addFileset(FileSet $set)
182
    {
183
        $this->filesets[] = $set;
184
    }
185
186
    /**
187
     * Adds a set of files (nested filelist attribute).
188
     *
189
     * @param FileList $list
190
     */
191
    public function addFilelist(FileList $list)
192
    {
193
        $this->filelists[] = $list;
194
    }
195
196
    /**
197
     * Creates a new PDOSQLExecFormatterElement for <formatter> element.
198
     *
199
     * @return PDOSQLExecFormatterElement
200
     */
201
    public function createFormatter()
202
    {
203
        $fe = new PDOSQLExecFormatterElement($this);
204
        $this->formatters[] = $fe;
205
206
        return $fe;
207
    }
208
209
    /**
210
     * Add a SQL transaction to execute
211
     */
212 3
    public function createTransaction()
213
    {
214 3
        $t = new PDOSQLExecTransaction($this);
215 3
        $this->transactions[] = $t;
216
217 3
        return $t;
218
    }
219
220
    /**
221
     * Set the file encoding to use on the SQL files read in
222
     *
223
     * @param string $encoding the encoding to use on the files
224
     */
225
    public function setEncoding($encoding)
226
    {
227
        $this->encoding = $encoding;
228
    }
229
230
    /**
231
     * Set the statement delimiter.
232
     *
233
     * <p>For example, set this to "go" and delimitertype to "ROW" for
234
     * Sybase ASE or MS SQL Server.</p>
235
     *
236
     * @param string $delimiter
237
     */
238 2
    public function setDelimiter(string $delimiter): void
239
    {
240 2
        $this->delimiter = $delimiter;
241 2
    }
242
243
    /**
244
     * Get the statement delimiter.
245
     *
246
     * @return string
247
     */
248 3
    public function getDelimiter(): string
249
    {
250 3
        return $this->delimiter;
251
    }
252
253
    /**
254
     * Set the Delimiter type for this sql task. The delimiter type takes two
255
     * values - normal and row. Normal means that any occurrence of the delimiter
256
     * terminate the SQL command whereas with row, only a line containing just
257
     * the delimiter is recognized as the end of the command.
258
     *
259
     * @param string $delimiterType
260
     */
261 3
    public function setDelimiterType(string $delimiterType): void
262
    {
263 3
        $this->delimiterType = $delimiterType;
264 3
    }
265
266
    /**
267
     * Action to perform when statement fails: continue, stop, or abort
268
     * optional; default &quot;abort&quot;
269
     *
270
     * @param string $action continue|stop|abort
271
     */
272
    public function setOnerror($action): void
273
    {
274
        $this->onError = $action;
275
    }
276
277
    /**
278
     * Sets the fetch mode to use for the PDO resultset.
279
     *
280
     * @param  mixed $mode The PDO fetchmode integer or constant name.
281
     * @throws BuildException
282
     */
283
    public function setFetchmode($mode): void
284
    {
285
        if (is_numeric($mode)) {
286
            $this->fetchMode = (int) $mode;
287
        } else {
288
            if (defined($mode)) {
289
                $this->fetchMode = constant($mode);
290
            } else {
291
                throw new BuildException("Invalid PDO fetch mode specified: " . $mode, $this->getLocation());
292
            }
293
        }
294
    }
295
296
    /**
297
     * Gets a default output writer for this task.
298
     *
299
     * @return Writer
300
     */
301
    private function getDefaultOutput()
0 ignored issues
show
Unused Code introduced by
The method getDefaultOutput() is not used, and could be removed.

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

Loading history...
302
    {
303
        return new LogWriter($this);
304
    }
305
306
    /**
307
     * Load the sql file and then execute it.
308
     *
309
     * {@inheritdoc}
310
     *
311
     * @throws BuildException
312
     */
313 3
    public function main()
314
    {
315
316
        // Set a default fetchmode if none was specified
317
        // (We're doing that here to prevent errors loading the class is PDO is not available.)
318 3
        if ($this->fetchMode === null) {
319 3
            $this->fetchMode = PDO::FETCH_ASSOC;
320
        }
321
322
        // Initialize the formatters here.  This ensures that any parameters passed to the formatter
323
        // element get passed along to the actual formatter object
324 3
        foreach ($this->formatters as $fe) {
325
            $fe->prepare($this->getLocation());
326
        }
327
328 3
        $savedTransaction = [];
329 3
        for ($i = 0, $size = count($this->transactions); $i < $size; $i++) {
330
            $savedTransaction[] = clone $this->transactions[$i];
331
        }
332
333 3
        $savedSqlCommand = $this->sqlCommand;
334
335 3
        $this->sqlCommand = trim($this->sqlCommand);
336
337
        try {
338 3
            if ($this->srcFile === null && $this->sqlCommand === ""
339 3
                && empty($this->filesets) && empty($this->filelists)
340 3
                && count($this->transactions) === 0
341
            ) {
342
                throw new BuildException(
343
                    "Source file or fileset/filelist, "
344
                    . "transactions or sql statement "
345
                    . "must be set!",
346
                    $this->getLocation()
347
                );
348
            }
349
350 3
            if ($this->srcFile !== null && !$this->srcFile->exists()) {
351
                throw new BuildException("Source file does not exist!", $this->getLocation());
352
            }
353
354
            // deal with the filesets
355 3
            foreach ($this->filesets as $fs) {
356
                $ds = $fs->getDirectoryScanner($this->project);
357
                $srcDir = $fs->getDir($this->project);
358
                $srcFiles = $ds->getIncludedFiles();
359
                // Make a transaction for each file
360
                foreach ($srcFiles as $srcFile) {
361
                    $t = $this->createTransaction();
362
                    $t->setSrc(new PhingFile($srcDir, $srcFile));
363
                }
364
            }
365
366
            // process filelists
367 3
            foreach ($this->filelists as $fl) {
368
                $srcDir = $fl->getDir($this->project);
369
                $srcFiles = $fl->getFiles($this->project);
370
                // Make a transaction for each file
371
                foreach ($srcFiles as $srcFile) {
372
                    $t = $this->createTransaction();
373
                    $t->setSrc(new PhingFile($srcDir, $srcFile));
374
                }
375
            }
376
377
            // Make a transaction group for the outer command
378 3
            $t = $this->createTransaction();
379 3
            if ($this->srcFile) {
380 3
                $t->setSrc($this->srcFile);
381
            }
382 3
            $t->addText($this->sqlCommand);
383 3
            $this->conn = $this->getConnection();
384
385
            try {
386 3
                $this->statement = null;
387
388
                // Initialize the formatters.
389 3
                $this->initFormatters();
390
391
                try {
392
                    // Process all transactions
393 3
                    for ($i = 0, $size = count($this->transactions); $i < $size; $i++) {
394 3
                        if (!$this->isAutocommit()) {
395
                            $this->log("Beginning transaction", Project::MSG_VERBOSE);
396
                            $this->conn->beginTransaction();
397
                        }
398 3
                        $this->transactions[$i]->runTransaction();
399 3
                        if (!$this->isAutocommit()) {
400
                            $this->log("Committing transaction", Project::MSG_VERBOSE);
401
                            $this->conn->commit();
402
                        }
403
                    }
404
                } catch (Exception $e) {
405
                    $this->closeConnection();
406 3
                    throw $e;
407
                }
408
            } catch (IOException $e) {
409
                if (!$this->isAutocommit() && $this->conn !== null && $this->onError == "abort") {
410
                    try {
411
                        $this->conn->rollback();
412
                    } catch (PDOException $ex) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
413
                    }
414
                }
415
                $this->closeConnection();
416
                throw new BuildException($e->getMessage(), $this->getLocation());
417
            } catch (PDOException $e) {
418
                if (!$this->isAutocommit() && $this->conn !== null && $this->onError == "abort") {
419
                    try {
420
                        $this->conn->rollback();
421
                    } catch (PDOException $ex) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
422
                    }
423
                }
424
                $this->closeConnection();
425
                throw new BuildException($e->getMessage(), $this->getLocation());
426
            }
427
428
            // Close the formatters.
429 3
            $this->closeFormatters();
430
431 3
            $this->log(
432 3
                $this->goodSql . " of " . $this->totalSql .
433 3
                " SQL statements executed successfully"
434
            );
435
        } catch (Exception $e) {
436
            throw $e;
437 3
        } finally {
438 3
            $this->transactions = $savedTransaction;
439 3
            $this->sqlCommand = $savedSqlCommand;
440 3
            $this->closeConnection();
441
        }
442 3
    }
443
444
    /**
445
     * read in lines and execute them
446
     *
447
     * @param  Reader $reader
448
     * @throws BuildException
449
     */
450 3
    public function runStatements(Reader $reader)
451
    {
452 3
        if (self::DELIM_NONE == $this->delimiterType) {
453
            $splitter = new DummyPDOQuerySplitter($this, $reader);
454 3
        } elseif (self::DELIM_NORMAL == $this->delimiterType && 0 === strpos($this->getUrl(), 'pgsql:')) {
455 1
            $splitter = new PgsqlPDOQuerySplitter($this, $reader);
456
        } else {
457 2
            $splitter = new DefaultPDOQuerySplitter($this, $reader, $this->delimiterType);
458
        }
459
460
        try {
461 3
            while (null !== ($query = $splitter->nextQuery())) {
462 3
                $this->log("SQL: " . $query, Project::MSG_VERBOSE);
463 3
                $this->execSQL($query);
464
            }
465
        } catch (PDOException $e) {
466
            throw $e;
467
        }
468 3
    }
469
470
    /**
471
     * Whether the passed-in SQL statement is a SELECT statement.
472
     * This does a pretty simple match, checking to see if statement starts with
473
     * 'select' (but not 'select into').
474
     *
475
     * @param string $sql
476
     *
477
     * @return boolean Whether specified SQL looks like a SELECT query.
478
     */
479
    protected function isSelectSql($sql)
480
    {
481
        $sql = trim($sql);
482
483
        return (stripos($sql, 'select') === 0 && stripos($sql, 'select into ') !== 0);
484
    }
485
486
    /**
487
     * Exec the sql statement.
488
     *
489
     * @param $sql
490
     *
491
     * @throws BuildException
492
     * @throws Exception
493
     */
494
    protected function execSQL($sql)
495
    {
496
        // Check and ignore empty statements
497
        if (trim($sql) == "") {
498
            return;
499
        }
500
501
        try {
502
            $this->totalSql++;
503
504
            $this->statement = $this->conn->query($sql);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->conn->query($sql) can also be of type false. However, the property $statement is declared as type PDOStatement. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
505
            $this->log($this->statement->rowCount() . " rows affected", Project::MSG_VERBOSE);
506
507
            // only call processResults() for statements that return actual data (such as 'select')
508
            if ($this->statement->columnCount() > 0) {
509
                $this->processResults();
510
            }
511
512
            $this->statement->closeCursor();
513
            $this->statement = null;
514
515
            $this->goodSql++;
516
        } catch (PDOException $e) {
517
            $this->log("Failed to execute: " . $sql, Project::MSG_ERR);
518
            if ($this->onError != "continue") {
519
                throw new BuildException("Failed to execute SQL", $e);
520
            }
521
            $this->log($e->getMessage(), Project::MSG_ERR);
522
        }
523
    }
524
525
    /**
526
     * Returns configured PDOResultFormatter objects
527
     * (which were created from PDOSQLExecFormatterElement objects).
528
     *
529
     * @return array PDOResultFormatter[]
530
     */
531 3
    protected function getConfiguredFormatters()
532
    {
533 3
        $formatters = [];
534 3
        foreach ($this->formatters as $fe) {
535
            $formatters[] = $fe->getFormatter();
536
        }
537
538 3
        return $formatters;
539
    }
540
541
    /**
542
     * Initialize the formatters.
543
     */
544 3
    protected function initFormatters()
545
    {
546 3
        $formatters = $this->getConfiguredFormatters();
547 3
        foreach ($formatters as $formatter) {
548
            $formatter->initialize();
549
        }
550 3
    }
551
552
    /**
553
     * Run cleanup and close formatters.
554
     */
555 3
    protected function closeFormatters()
556
    {
557 3
        $formatters = $this->getConfiguredFormatters();
558 3
        foreach ($formatters as $formatter) {
559
            $formatter->close();
560
        }
561 3
    }
562
563
    /**
564
     * Passes results from query to any formatters.
565
     *
566
     * @throws PDOException
567
     */
568
    protected function processResults()
569
    {
570
        try {
571
            $this->log("Processing new result set.", Project::MSG_VERBOSE);
572
573
            $formatters = $this->getConfiguredFormatters();
574
575
            while ($row = $this->statement->fetch($this->fetchMode)) {
576
                foreach ($formatters as $formatter) {
577
                    $formatter->processRow($row);
578
                }
579
            }
580
        } catch (Exception $x) {
581
            $this->log("Error processing reults: " . $x->getMessage(), Project::MSG_ERR);
582
            foreach ($formatters as $formatter) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $formatters does not seem to be defined for all execution paths leading up to this point.
Loading history...
583
                $formatter->close();
584
            }
585
            throw $x;
586
        }
587
    }
588
589
    /**
590
     * Closes current connection
591
     */
592 3
    protected function closeConnection(): void
593
    {
594 3
        if ($this->conn) {
595
            unset($this->conn);
596
            $this->conn = null;
597
        }
598 3
    }
599
600
    /**
601
     * PDOSQLExecTask as condition
602
     *
603
     * Returns false when the database connection fails, and true otherwise.
604
     * This method only uses three properties: url (required), userId and
605
     * password.
606
     *
607
     * The database connection is not stored in a variable, this allow to
608
     * immediately close the connections since there's no reference to it.
609
     *
610
     * @author Jawira Portugal <[email protected]>
611
     *
612
     * @return bool
613
     */
614 6
    public function evaluate(): bool
615
    {
616 6
        if (empty($this->getUrl())) {
617 1
            throw new BuildException('url is required');
618
        }
619
620 5
        $this->log('Trying to reach ' . $this->getUrl(), Project::MSG_DEBUG);
621
622
        try {
623 5
            new PDO($this->getUrl(), $this->getUserId(), $this->getPassword());
624 4
        } catch (PDOException $ex) {
625 4
            $this->log($ex->getMessage(), Project::MSG_VERBOSE);
626
627 4
            return false;
628
        }
629
630 1
        $this->log('Successful connection to ' . $this->getUrl(), Project::MSG_DEBUG);
631
632 1
        return true;
633
    }
634
}
635