Database::statementException()   A
last analyzed

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 3
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 3
b 0
f 0
nc 1
nop 2
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
1
<?php
2
3
namespace Anax\Database;
4
5
use Anax\Database\Exception\Exception;
6
7
/**
8
 * Database wrapper, provides a database API on top of PHP PDO for
9
 * enhancing the API and dealing with error reporting and tracking.
10
 */
11
class Database
12
{
13
    /**
14
     * @var array        $options used when creating the PDO object
15
     * @var PDO          $pdo     the PDO object
16
     * @var PDOStatement $stmt    the latest PDOStatement used
17
     */
18
    protected $options;
19
    private $pdo = null;
20
    private $stmt = null;
21
22
23
24
    /**
25
     * Constructor creating a PDO object connecting to a choosen database.
26
     *
27
     * @param array $options containing details for connecting to the database.
28
     */
29 43
    public function __construct(array $options = [])
30
    {
31 43
        $this->setOptions($options);
32 43
    }
33
34
35
36
    /**
37
     * Set options and connection details.
38
     *
39
     * @param array $options containing details for connecting to the database.
40
     *
41
     * @return void
42
     */
43 43
    public function setOptions(array $options = []) : void
44
    {
45
        $default = [
46 43
            "dsn"             => null,
47
            "username"        => null,
48
            "password"        => null,
49
            "driver_options"  => null,
50
            "table_prefix"    => null,
51
            "fetch_mode"      => \PDO::FETCH_OBJ,
52
            "emulate_prepares" => false,
53
            "session_key"     => "Anax\Database",
54
            "verbose"         => false,
55
            "debug_connect"   => false,
56
        ];
57
58 43
        $this->options = array_merge($default, $options);
59 43
    }
60
61
62
63
    /**
64
     * Set a single option for configuration and connection details.
65
     *
66
     * @param string $option which to set.
67
     * @param mixed  $value  to set.
68
     *
69
     * @return self
70
     */
71 2
    public function setOption(string $option, $value) : object
72
    {
73 2
        $this->options[$option] = $value;
74 2
        return $this;
75
    }
76
77
78
79
    /**
80
     * Connect to the database, allow being called multiple times
81
     * but ignore when connection is already made.
82
     *
83
     * @throws \Anax\Database\Exception
84
     *
85
     * @return self
86
     */
87 34
    public function connect() : object
88
    {
89 34
        if ($this->pdo) {
90 1
            return $this;
91
        }
92
93 34
        if (!isset($this->options["dsn"])) {
94 1
            throw new Exception("You can not connect, missing dsn.");
95
        }
96
97
        try {
98 33
            $this->pdo = new \PDO(
99 33
                $this->options["dsn"],
100 33
                $this->options["username"],
101 33
                $this->options["password"],
102 33
                $this->options["driver_options"]
103
            );
104 31
            $this->pdo->setAttribute(\PDO::ATTR_DEFAULT_FETCH_MODE, $this->options['fetch_mode']);
105 31
            $this->pdo->setAttribute(\PDO::ATTR_EMULATE_PREPARES, $this->options['emulate_prepares']);
106 2
        } catch (\PDOException $e) {
107 2
            if ($this->options["debug_connect"]) {
108 1
                throw $e;
109
            }
110 1
            throw new Exception("Could not connect to database, hiding connection details.");
111
        }
112
113 31
        return $this;
114
    }
115
116
117
118
    /**
119
     * Support arrays in params, extract array items and add to $params
120
     * and insert ? for each entry in the array.
121
     *
122
     * @param string $query  as the query to prepare.
123
     * @param array  $params the parameters that may contain arrays.
124
     *
125
     * @return array with query and params.
126
     */
127 30
    private function expandParamArray(string $query, array $params) : array
128
    {
129 30
        $param = [];
130 30
        $offset = -1;
131
132 30
        foreach ($params as $val) {
133 24
            $offset = strpos($query, "?", $offset + 1);
134
135 24
            if (is_array($val)) {
136
                $nrOfItems = count($val);
137
138
                if ($nrOfItems) {
139
                    $query = substr($query, 0, $offset)
140
                        . str_repeat("?,", $nrOfItems  - 1)
141
                        . "?"
142
                        . substr($query, $offset + 1);
143
                    $param = array_merge($param, $val);
144
                } else {
145
                    $param[] = null;
146
                }
147
            } else {
148 24
                $param[] = $val;
149
            }
150
        }
151
152 30
        return [$query, $param];
153
    }
154
155
156
157
    /**
158
     * Execute a select-query with arguments and return the resultset.
159
     *
160
     * @param string $query  the SQL statement
161
     * @param array  $params the params array
162
     *
163
     * @return mixed with resultset
164
     */
165 2
    public function executeFetchAll(string $query, array $params = []) : array
166
    {
167 2
        return $this->execute($query, $params)->fetchAll();
168
    }
169
170
171
172
    /**
173
     * Execute a select-query with arguments and return the first row
174
     * in the resultset.
175
     *
176
     * @param string $query  the SQL statement
177
     * @param array  $params the params array
178
     *
179
     * @return mixed with resultset
180
     */
181 3
    public function executeFetch(string $query, array $params = [])
182
    {
183 3
        return $this->execute($query, $params)->fetch();
184
    }
185
186
187
188
    /**
189
     * Execute a select-query with arguments and insert the results into
190
     * a new object of the class.
191
     *
192
     * @param string $query  the SQL statement
193
     * @param array  $params the params array
194
     * @param string $class  the class to create an object of and insert into
195
     *
196
     * @return null|object with resultset, null when no resultset
197
     */
198 2
    public function executeFetchClass(
199
        string $query,
200
        array $params,
201
        string $class
202
    ) :? object {
203 2
        return $this->execute($query, $params)->fetchClass($class);
204
    }
205
206
207
208
    /**
209
     * Execute a select-query with arguments and insert the results into
210
     * an existing object.
211
     *
212
     * @param string $query  the SQL statement
213
     * @param array  $params the params array
214
     * @param object $object the existing object to insert into
215
     *
216
     * @return null|object with resultset or null when no match
217
     */
218 2
    public function executeFetchInto(
219
        string $query,
220
        array $params,
221
        object $object
222
    ) :? object {
223 2
        return $this->execute($query, $params)->fetchInto($object);
224
    }
225
226
227
228
    /**
229
     * Fetch all rows into the resultset.
230
     *
231
     * @return array with resultset.
232
     */
233 4
    public function fetchAll() : array
234
    {
235 4
        return $this->stmt->fetchAll();
236
    }
237
238
239
240
    /**
241
     * Fetch one row as the resultset.
242
     *
243
     * @return mixed with resultset.
244
     */
245 19
    public function fetch()
246
    {
247 19
        $res = $this->stmt->fetch();
248 19
        return $res === false ? null : $res;
249
    }
250
251
252
253
    /**
254
     * Fetch one resultset as a new object from this class.
255
     *
256
     * @param string $classname which type of object to instantiate.
257
     *
258
     * @return null|object with details, null when no resultset
259
     */
260 5
    public function fetchClass(string $classname) :? object
261
    {
262 5
        $this->stmt->setFetchMode(\PDO::FETCH_CLASS, $classname);
263 5
        return $this->fetch();
264
    }
265
266
267
268
    /**
269
     * Fetch all rows as the resultset instantiated as new objects from
270
     * this class.
271
     *
272
     * @param string $classname which type of object to instantiate.
273
     *
274
     * @return array with resultset containing objects of $classname.
275
     */
276 2
    public function fetchAllClass(string $classname) : array
277
    {
278 2
        $this->stmt->setFetchMode(\PDO::FETCH_CLASS, $classname);
279 2
        return $this->stmt->fetchAll();
280
    }
281
282
283
284
    /**
285
     * Fetch one resultset into an object.
286
     *
287
     * @param object $object to insert values into.
288
     *
289
     * @return null|object with resultset or null when no match
290
     */
291 5
    public function fetchInto(object $object) :? object
292
    {
293 5
        $this->stmt->setFetchMode(\PDO::FETCH_INTO, $object);
294 5
        return $this->fetch();
295
    }
296
297
298
299
    /**
300
     * Execute a SQL-query and ignore the resultset.
301
     *
302
     * @param string $query  the SQL statement
303
     * @param array  $params the params array
304
     *
305
     * @throws Exception when failing to prepare question or when not connected.
306
     *
307
     * @return self
308
     */
309 30
    public function execute(string $query, array $params = []) : object
310
    {
311 30
        list($query, $params) = $this->expandParamArray($query, $params);
312
313 30
        if (!$this->pdo) {
314 1
            $this->createException("Did you forget to connect to the database?", $query, $params);
315
        }
316
317 29
        $this->stmt = $this->pdo->prepare($query);
318 29
        if (!$this->stmt) {
319 2
            $this->pdoException($query, $params);
320
        }
321
322 27
        if ($this->options["verbose"]) {
323 1
            echo $query . "\n";
324 1
            print_r($params);
325
        }
326
327 27
        $res = $this->stmt->execute($params);
328 27
        if (!$res) {
329 1
            $this->statementException($query, $params);
330
        }
331
332 26
        return $this;
333
    }
334
335
336
337
    /**
338
     * Throw exception with detailed message.
339
     *
340
     * @param string $msg    detailed error message from PDO
341
     * @param string $query  query to execute
342
     * @param array  $params to match ? in statement
343
     *
344
     * @throws Anax\Database\Exception
345
     *
346
     * @return void
347
     */
348 4
    protected function createException(
349
        string $msg,
350
        string $query,
351
        array $params
352
    ) : void {
353 4
        throw new Exception(
354
            $msg
355 4
            . "<br><br>SQL ("
356 4
            . substr_count($query, "?")
357 4
            . " params):<br><pre>$query</pre><br>PARAMS ("
358 4
            . count($params)
359 4
            . "):<br><pre>"
360 4
            . implode("\n", $params)
361 4
            . "</pre>"
362
        );
363
    }
364
365
366
367
    /**
368
     * Throw exception when pdo failed using detailed message.
369
     *
370
     * @param string $query  query to execute
371
     * @param array  $params to match ? in statement
372
     *
373
     * @return void
374
     */
375 2
    protected function pdoException(string $query, array $params) : void
376
    {
377 2
        $this->createException($this->pdo->errorInfo()[2], $query, $params);
378
    }
379
380
381
382
    /**
383
     * Throw exception when statement failed using detailed message.
384
     *
385
     * @param string $query  query to execute
386
     * @param array  $params to match ? in statement
387
     *
388
     * @return void
389
     */
390 1
    protected function statementException(string $query, array $params) : void
391
    {
392 1
        $this->createException($this->stmt->errorInfo()[2], $query, $params);
393
    }
394
395
396
397
    /**
398
     * Return last insert id from autoincremented key on INSERT.
399
     *
400
     * @return integer as last insert id.
401
     */
402 22
    public function lastInsertId() : int
403
    {
404 22
        return $this->pdo->lastInsertId();
405
    }
406
407
408
409
    /**
410
    * Return rows affected of last INSERT, UPDATE, DELETE
411
    *
412
    * @return integer as rows affected on last statement
413
    */
414 22
    public function rowCount() : int
415
    {
416 22
        return $this->stmt->rowCount();
417
    }
418
}
419