Completed
Push — master ( 4604a2...31c42c )
by Radu
02:05
created

AbstractPdoDatabase::transaction()   A

Complexity

Conditions 4
Paths 10

Size

Total Lines 16
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 12
c 2
b 0
f 0
dl 0
loc 16
rs 9.8666
cc 4
nc 10
nop 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace WebServCo\Framework\Database;
6
7
use WebServCo\Framework\Environment\Config;
8
use WebServCo\Framework\Exceptions\DatabaseException;
9
10
abstract class AbstractPdoDatabase extends \WebServCo\Framework\AbstractLibrary
11
{
12
    use \WebServCo\Framework\Traits\DatabaseTrait;
13
    use \WebServCo\Framework\Traits\DatabaseAddQueryTrait;
14
15
    protected \PDO $db;
16
    protected \PDOStatement $stmt;
17
18
    abstract protected function getDataSourceName(string $host, int $port, string $dbname): string;
19
20
    /**
21
    * @param array<string,string|array<mixed>> $settings
22
    */
23
    public function __construct(array $settings = [])
24
    {
25
        parent::__construct($settings);
26
27
        try {
28
            $dsn = $this->getDataSourceName(
29
                Config::string('APP_DBMS_HOST'),
30
                Config::int('APP_DBMS_PORT'),
31
                Config::string('APP_DBMS_DBNAME'),
32
            );
33
            $this->db = new \PDO(
34
                $dsn,
35
                Config::string('APP_DBMS_USERNAME'),
36
                Config::string('APP_DBMS_PASSWD'),
37
                [
38
                    \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
39
                    \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
40
                    \PDO::ATTR_EMULATE_PREPARES => false,
41
                    \PDO::ATTR_PERSISTENT => false,
42
                ],
43
            );
44
        } catch (\Throwable $e) { // PDOException/RuntimeException/Exception
45
            throw new DatabaseException($e->getMessage(), $e);
46
        }
47
    }
48
49
    public function affectedRows(): int
50
    {
51
        if (!($this->stmt instanceof \PDOStatement)) {
0 ignored issues
show
introduced by
$this->stmt is always a sub-type of PDOStatement.
Loading history...
52
            throw new DatabaseException('No Statement object available.');
53
        }
54
        return $this->stmt->rowCount();
55
    }
56
57
    public function escape(string $string): string
58
    {
59
        return $this->db->quote($string);
60
    }
61
62
    /**
63
    * @param array<int,float|int|string> $params
64
    * @return bool|int|string|null
65
    */
66
    public function getColumn(string $query, array $params = [], int $columnNumber = 0)
67
    {
68
        $this->query($query, $params);
69
        return $this->stmt->fetchColumn($columnNumber);
70
    }
71
72
    /**
73
    * @param array<int,float|int|string> $params
74
    * @return array<string,float|int|string>
75
    */
76
    public function getRow(string $query, array $params = []): array
77
    {
78
        $this->query($query, $params);
79
        $result = $this->stmt->fetch(\PDO::FETCH_ASSOC);
80
        return $this->handleStatementReturn($result);
81
    }
82
83
    /**
84
    * @param array<int,float|int|string> $params
85
    * @return array<int,array<string,float|int|string>>
86
    */
87
    public function getRows(string $query, array $params = []): array
88
    {
89
        $this->query($query, $params);
90
        $result = $this->stmt->fetchAll(\PDO::FETCH_ASSOC);
91
        return $this->handleStatementReturn($result);
92
    }
93
94
    /**
95
    * Get last inserted Id.
96
    *
97
    * https://dev.mysql.com/doc/refman/5.5/en/information-functions.html#function_last-insert-id
98
    * If you insert multiple rows using a single INSERT statement,
99
    * LAST_INSERT_ID() returns the value generated for the first inserted row only.
100
    * The reason for this is to make it possible to reproduce easily the same
101
    * INSERT statement against some other server.
102
    *
103
    * PDO:
104
    * Returns the ID of the last inserted row, or the last value from a sequence object,
105
    * depending on the underlying driver.
106
    * For example, PDO_PGSQL requires you to specify the name of a sequence object for the name parameter.
107
    */
108
    public function lastInsertId(string $name = ''): int
109
    {
110
        return (int) $this->db->lastInsertId($name);
111
    }
112
113
    public function numRows(): int
114
    {
115
        if (!($this->stmt instanceof \PDOStatement)) {
0 ignored issues
show
introduced by
$this->stmt is always a sub-type of PDOStatement.
Loading history...
116
            throw new DatabaseException('No Statement object available.');
117
        }
118
        return $this->stmt->rowCount();
119
    }
120
121
    /**
122
    * @param array<int,float|int|string|null> $params
123
    */
124
    public function query(string $query, array $params = []): \PDOStatement
125
    {
126
        if (!$query) {
127
            throw new DatabaseException('No query specified.');
128
        }
129
130
        try {
131
            $this->stmt = $this->db->prepare($query);
132
            if ($params) {
133
                $this->bindParams($params);
134
            }
135
            $this->stmt->execute();
136
            return $this->stmt;
137
        } catch (\Throwable $e) { // \PDOException, \RuntimeException
138
            throw new DatabaseException($e->getMessage(), $e);
139
        }
140
    }
141
142
    /**
143
    * @param mixed $value
144
    */
145
    public function setAttribute(int $attribute, $value): bool
146
    {
147
        return $this->db->setAttribute($attribute, $value);
148
    }
149
150
    /**
151
    * @param array<int,array<int,mixed>> $queries
152
    */
153
    public function transaction(array $queries): bool
154
    {
155
        try {
156
            $this->db->beginTransaction();
157
            foreach ($queries as $item) {
158
                if (!isset($item[0])) {
159
                    throw new DatabaseException('No query specified.');
160
                }
161
                $params = $item[1] ?? [];
162
                $this->query($item[0], $params);
163
            }
164
            $this->db->commit();
165
            return true;
166
        } catch (\Throwable $e) { // DatabaseException, \PDOException, \RuntimeException
167
            $this->db->rollBack();
168
            throw new DatabaseException($e->getMessage(), $e);
169
        }
170
    }
171
172
    /**
173
    * @param array<mixed> $data
174
    */
175
    protected function bindParams(array $data): bool
176
    {
177
        if (!$data) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $data of type array<mixed,mixed> is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
178
            return false;
179
        }
180
181
        $i = 1;
182
        foreach ($data as $item) {
183
            if (\is_array($item)) {
184
                foreach ($item as $v) {
185
                    $this->validateParam($v);
186
                    $this->stmt->bindValue($i, $v, $this->getDataType((string) $v));
187
                    $i++;
188
                }
189
            } else {
190
                $this->validateParam($item);
191
                $this->stmt->bindValue($i, $item, $this->getDataType((string) $item));
192
                $i++;
193
            }
194
        }
195
        return true;
196
    }
197
198
    protected function getDataType(string $variable): int
199
    {
200
        $type = \gettype($variable);
201
202
        switch ($type) {
203
            case 'NULL':
204
                return \PDO::PARAM_NULL;
205
            case 'integer':
206
                return \PDO::PARAM_INT;
207
            case 'boolean':
208
            // causes data not to be inserted
209
            //return \PDO::PARAM_BOOL;
210
            case 'string':
211
            case 'double':
212
            case 'array':
213
            case 'object':
214
            case 'resource':
215
            case 'resource (closed)':
216
            case 'unknown type':
217
            default:
218
                return \PDO::PARAM_STR;
219
        }
220
    }
221
222
    /**
223
    * @param mixed $param
224
    */
225
    protected function validateParam($param): bool
226
    {
227
        if (\is_array($param)) {
228
            throw new DatabaseException('Parameter is an array.');
229
        }
230
        return true;
231
    }
232
233
    /**
234
    * Make sure PDO Statement returns an array when there are no errors.
235
    *
236
    * "In all cases, false is returned on failure."
237
    * However, false is also returned when there are no results.
238
    *
239
    * @param bool|array<mixed> $result
240
    * @return array<mixed>
241
    */
242
    protected function handleStatementReturn($result): array
243
    {
244
        if (\is_array($result)) {
245
            // All is ok.
246
            return $result;
247
        }
248
        $errorInfo = $this->stmt->errorInfo();
249
        // 0 = "SQLSTATE"
250
        // 1 = "Driver specific error code"
251
        // 2 = "Driver specific error message"
252
        if ('00000' === $errorInfo[0]) {
253
            // "Successful completion", so no results
254
            return [];
255
        }
256
        throw new DatabaseException($errorInfo[2]);
257
    }
258
}
259