Passed
Push — main ( ddd1a4...b98cb8 )
by Michiel
08:47
created

PDOSQLExecTask   F

Complexity

Total Complexity 75

Size/Duplication

Total Lines 586
Duplicated Lines 0 %

Test Coverage

Coverage 45.03%

Importance

Changes 0
Metric Value
wmc 75
eloc 186
dl 0
loc 586
rs 2.4
c 0
b 0
f 0
ccs 86
cts 191
cp 0.4503

23 Methods

Rating   Name   Duplication   Size   Complexity  
A isSelectSql() 0 5 2
A addText() 0 3 1
A setSrc() 0 3 1
A getDefaultOutput() 0 3 1
A addFileset() 0 3 1
A execSQL() 0 28 5
A createFormatter() 0 6 1
A setDelimiterType() 0 3 1
A setEncoding() 0 3 1
A createTransaction() 0 6 1
A setOnerror() 0 3 1
A evaluate() 0 19 3
A getDelimiter() 0 3 1
A setFetchmode() 0 9 3
A setDelimiter() 0 3 1
A processResults() 0 18 5
A getConfiguredFormatters() 0 8 2
A closeConnection() 0 5 2
A closeFormatters() 0 5 2
F main() 0 131 31
A initFormatters() 0 5 2
A runStatements() 0 17 6
A addFilelist() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like PDOSQLExecTask often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use PDOSQLExecTask, and based on these observations, apply Extract Interface, too.

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