Completed
Push — master ( cfcf82...657c44 )
by Nick
21:51 queued 16:40
created

Query::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 3
ccs 3
cts 3
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace MySociety\TheyWorkForYou\Db;
4
5
/**
6
 * Database Query
7
 *
8
 * Represents a single query to the database.
9
 *
10
 * ### After a `SELECT`
11
 *
12
 * If successful:
13
 * - `$this->success()` returns `true`.
14
 * - `$this->rows()` returns the number of rows selected
15
 * - `$this->row(n)` returns an array of the nth row, with the keys being column names.
16
 * - `$this->field(n,col)` returns the contents of the "col" column in the nth row.
17
 * - `$this->insert_id()` returns `null`.
18
 * - `$this->affected_rows()` returns `null`.
19
 *
20
 * If 0 rows selected:
21
 * - `$this->success()` returns `true`.
22
 * - `$this->rows()` returns `0`.
23
 * - `$this->row(n)` returns an empty array.
24
 * - `$this->field(n,col)` returns an empty string.
25
 * - `$this->insert_id()` returns `null`.
26
 * - `$this->affected_rows()` returns `null`.
27
 *
28
 * ### After an `INSERT`
29
 *
30
 * If successful:
31
 * - `$this->success()` returns `true`.
32
 * - `$this->rows()` returns `null`.
33
 * - `$this->row(n)` returns an empty array.
34
 * - `$this->field(n,col)` returns an empty string.
35
 * - `$this->insert_id()` returns the last_insert_id (if there's AUTO_INCREMENT on a column)`.
36
 * - `$this->affected_rows()` returns `1`.
37
 *
38
 * ### After an `UPDATE`
39
 *
40
 * If rows have been changed:
41
 * - `$this->success()` returns `true`.
42
 * - `$this->rows()` returns `null`.
43
 * - `$this->row(n)` returns an empty array.
44
 * - `$this->field(n,col)` returns an empty string.
45
 * - `$this->insert_id()` returns `0`.
46
 * - `$this->affected_rows()` returns the number of rows changed.
47
 *
48
 * ### After a `DELETE`
49
 *
50
 * If rows have been deleted:
51
 * - `$this->success()` returns `true`.
52
 * - `$this->rows()` returns `null`.
53
 * - `$this->row(n)` returns an empty array.
54
 * - `$this->field(n,col)` returns an empty string.
55
 * - `$this->insert_id()` returns `0`.
56
 * - `$this->affected_rows()` returns the number of rows changed.
57
 *
58
 * If no rows are deleted:
59
 * - `$this->success()` returns `true`.
60
 * - `$this->rows()` returns `null`.
61
 * - `$this->row(n)` returns an empty array.
62
 * - `$this->field(n,col)` returns an empty string.
63
 * - `$this->insert_id()` returns `0`.
64
 * - `$this->affected_rows()` returns `0`.
65
 *
66
 * ### Errors
67
 *
68
 * If there's an error for any of the above actions:
69
 * - `$this->success()` returns `false`.
70
 * - `$this->rows()` returns `null`.
71
 * - `$this->row(n)` returns an empty array.
72
 * - `$this->field(n,col)` returns an empty string.
73
 * - `$this->insert_id()` returns `null`.
74
 * - `$this->affected_rows()` returns `null`.
75
 */
76
77
class Query {
78
79
    public $success = true;
80
    public $rows = null;
81
    public $data = array();
82
    public $insert_id = null;
83
    public $affected_rows = null;
84
85 76
    public function __construct($conn) {
86 76
        $this->conn = $conn;
0 ignored issues
show
Bug introduced by
The property conn does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
87 76
    }
88
89 76
    public function query($sql = "", $params = null) {
90
91 76
        if (empty($sql)) {
92
            $this->success = false;
93
94
            return;
95
        }
96
97 76
        if (empty($this->conn)) {
98
            $this->success = false;
99
100
            return;
101
        }
102
103 76
        twfy_debug("SQL", $sql);
104 76
        twfy_debug("SQL", print_r($params, 1));
105
106 76
        if ($params !== null) {
107
            // Prepare and execute a statement
108 71
            $pdoStatement = $this->conn->prepare($sql);
109
110 71
            foreach ($params as $paramKey => $paramValue) {
111
112 64
                if (is_int($paramValue)) {
113 32
                    $paramType = \PDO::PARAM_INT;
114 32
                } else {
115 63
                    $paramType = \PDO::PARAM_STR;
116
                }
117
118 64
                $pdoStatement->bindValue($paramKey, $paramValue, $paramType);
119 71
            }
120
121 71
            $pdoStatement->execute();
122
123 71
        } else {
124
            // Execute the raw query
125 17
            $pdoStatement = $this->conn->query($sql);
126
        }
127
128
        // Test the query actually worked
129 76
        if (!$pdoStatement) {
130
            $this->error($this->conn->errorCode() . ': ' . $this->conn->errorInfo()[2]);
131
        }
132
133 76
        if (!$this->success) return;
134
135 76
        if ((!$pdoStatement) or (empty($pdoStatement))) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using logical operators such as or instead of || is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
136
            // A failed query.
137
            $this->success = false;
138
139
        } else {
140
141
            // A successful SELECT, SHOW, EXPLAIN or DESCRIBE query.
142 76
            $this->success = true;
143
144 76
            $result = $pdoStatement->fetchAll(\PDO::FETCH_ASSOC);
145
146 76
            $this->rows = count($result);
147 76
            $this->data = $result;
148
149
            // Sanity check that lastInsertId() is actually a number, otherwise panic
150 76
            if (is_numeric($this->conn->lastInsertId())) {
151 76
                $this->insert_id = (int) $this->conn->lastInsertId();
152 76
            } else {
153
                throw new Exception('Last connection ID was not numeric!');
154
            }
155 76
            $this->affected_rows = $pdoStatement->rowCount();
156
157 76
            twfy_debug("SQLRESULT", array($this, 'displayResult'));
0 ignored issues
show
Documentation introduced by
array($this, 'displayResult') is of type array<integer,this<MySoc...\Query>","1":"string"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
158
            // mysql_free_result($q);
0 ignored issues
show
Unused Code Comprehensibility introduced by
67% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
159
        }
160 76
    }
161
162 17
    public function success() {
163 17
        return $this->success;
164
    }
165
166
    // After INSERTS.
167 2
    public function insert_id() {
168 2
        return $this->insert_id;
169
    }
170
171
    // After INSERT, UPDATE, DELETE.
172
    public function affected_rows() {
173
        return $this->affected_rows;
174
    }
175
176
    // After SELECT.
177
178
    /**
179
     * @param integer $row_index
180
     * @param string $column_name
181
     */
182 63
    public function field($row_index, $column_name) {
183 63
        if ($this->rows > 0)
184 63
            return $this->data[$row_index][$column_name];
185
        return "";
186
    }
187
188
    // After SELECT.
189 70
    public function rows() {
190 70
        return $this->rows;
191
    }
192
193
    // After SELECT.
194
195
    /**
196
     * @param integer $row_index
197
     */
198 9
    public function row($row_index) {
199 9
        if ($this->success && $this->rows > 0)
200 9
            return $this->data[$row_index];
201
        return array();
202
    }
203
204
    # Used when debugging
205
    private function displayResult() {
206
        $html = "";
207
208
        if ($this->rows > 0) {
209
210
            $html .= "<table border=\"1\">\n<tr>\n";
211
212
            foreach (array_keys($this->data[0]) as $fieldname) {
213
                $html .= "<th>" . _htmlentities($fieldname) . "</th>";
214
            }
215
            $html .= "</tr>\n";
216
217
            foreach ($this->data as $index => $row) {
218
                $html .= "<tr>";
219
                foreach ($row as $n => $field) {
220
                    if ($n == "email" || $n == "password" || $n == "postcode") {
221
                        // Don't want to risk this data being displayed on any page.
222
                        $html .= "<td>**MASKED**</td>";
223
                    } else {
224
                        $html .= "<td>" . _htmlentities($field) . "</td>";
225
                    }
226
                }
227
                $html .= "</tr>\n";
228
            }
229
            $html .= "</table>\n";
230
        }
231
232
        return $html;
233
    }
234
235
    /**
236
     * @param string $errormsg
237
     */
238
    public function error($errormsg) {
239
        // When a query goes wrong...
240
        $this->success = false;
241
        trigger_error($errormsg, E_USER_ERROR);
242
    }
243
244
}
245