1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* WebHemi. |
4
|
|
|
* |
5
|
|
|
* PHP version 5.6 |
6
|
|
|
* |
7
|
|
|
* @copyright 2012 - 2016 Gixx-web (http://www.gixx-web.com) |
8
|
|
|
* @license https://opensource.org/licenses/MIT The MIT License (MIT) |
9
|
|
|
* |
10
|
|
|
* @link http://www.gixx-web.com |
11
|
|
|
*/ |
12
|
|
|
namespace WebHemi\Adapter\Data\PDO; |
13
|
|
|
|
14
|
|
|
use InvalidArgumentException; |
15
|
|
|
use PDO; |
16
|
|
|
use PDOStatement; |
17
|
|
|
use RuntimeException; |
18
|
|
|
use WebHemi\Adapter\Data\DataAdapterInterface; |
19
|
|
|
|
20
|
|
|
/** |
21
|
|
|
* Class PDOAdapter. |
22
|
|
|
*/ |
23
|
|
|
class PDOAdapter implements DataAdapterInterface |
24
|
|
|
{ |
25
|
|
|
/** @var PDO */ |
26
|
|
|
private $dataStorage; |
27
|
|
|
/** @var string */ |
28
|
|
|
private $dataGroup = null; |
29
|
|
|
/** @var string */ |
30
|
|
|
private $idKey = null; |
31
|
|
|
|
32
|
|
|
/** |
33
|
|
|
* PDOAdapter constructor. |
34
|
|
|
* |
35
|
|
|
* @param PDO $dataStorage |
36
|
|
|
* |
37
|
|
|
* @throws InvalidArgumentException |
38
|
|
|
*/ |
39
|
10 |
|
public function __construct($dataStorage = null) |
40
|
|
|
{ |
41
|
10 |
|
if (!$dataStorage instanceof PDO) { |
42
|
1 |
|
$type = gettype($dataStorage); |
43
|
|
|
|
44
|
1 |
|
if ($type == 'object') { |
45
|
1 |
|
$type = get_class($dataStorage); |
46
|
1 |
|
} |
47
|
|
|
|
48
|
1 |
|
$message = sprintf( |
49
|
1 |
|
'Can\'t create %s instance. The parameter must be an instance of PDO, %s given.', |
50
|
1 |
|
__CLASS__, |
51
|
|
|
$type |
52
|
1 |
|
); |
53
|
|
|
|
54
|
1 |
|
throw new InvalidArgumentException($message); |
55
|
|
|
} |
56
|
|
|
|
57
|
10 |
|
$this->dataStorage = $dataStorage; |
58
|
10 |
|
} |
59
|
|
|
|
60
|
|
|
/** |
61
|
|
|
* Returns the Data Storage instance. |
62
|
|
|
* |
63
|
|
|
* @return PDO |
64
|
|
|
*/ |
65
|
1 |
|
public function getDataStorage() |
66
|
|
|
{ |
67
|
1 |
|
return $this->dataStorage; |
68
|
|
|
} |
69
|
|
|
|
70
|
|
|
/** |
71
|
|
|
* Set adapter data group. For Databases this can be the Tables. |
72
|
|
|
* |
73
|
|
|
* @param string $dataGroup |
74
|
|
|
* |
75
|
|
|
* @throws RuntimeException |
76
|
|
|
* |
77
|
|
|
* @return PDOAdapter |
78
|
|
|
*/ |
79
|
4 |
|
public function setDataGroup($dataGroup) |
80
|
|
|
{ |
81
|
4 |
|
if (!empty($this->dataGroup)) { |
82
|
1 |
|
throw new RuntimeException('Can\'t re-initialize dataGroup property. Property is already set.'); |
83
|
|
|
} |
84
|
|
|
|
85
|
4 |
|
$this->dataGroup = $dataGroup; |
86
|
|
|
|
87
|
4 |
|
return $this; |
88
|
|
|
} |
89
|
|
|
|
90
|
|
|
/** |
91
|
|
|
* Set adapter ID key. For Databases this can be the Primary key. Only simple key is allowed. |
92
|
|
|
* |
93
|
|
|
* @param string $idKey |
94
|
|
|
* |
95
|
|
|
* @throws RuntimeException |
96
|
|
|
* |
97
|
|
|
* @return PDOAdapter |
98
|
|
|
*/ |
99
|
1 |
|
public function setIdKey($idKey) |
100
|
|
|
{ |
101
|
1 |
|
if (!empty($this->idKey)) { |
102
|
1 |
|
throw new RuntimeException('Can\'t re-initialize idKey property. Property is already set.'); |
103
|
|
|
} |
104
|
|
|
|
105
|
1 |
|
$this->idKey = $idKey; |
106
|
|
|
|
107
|
1 |
|
return $this; |
108
|
|
|
} |
109
|
|
|
|
110
|
|
|
/** |
111
|
|
|
* Get exactly one "row" of data according to the expression. |
112
|
|
|
* |
113
|
|
|
* @param mixed $identifier |
114
|
|
|
* |
115
|
|
|
* @return array |
116
|
|
|
* |
117
|
|
|
* @codeCoverageIgnore Don't test external library. |
118
|
|
|
*/ |
119
|
|
|
public function getData($identifier) |
120
|
|
|
{ |
121
|
|
|
$queryBind = []; |
122
|
|
|
|
123
|
|
|
$query = $this->getSelectQueryForExpression([$this->idKey => $identifier], $queryBind, 1, 0); |
124
|
|
|
$statement = $this->getDataStorage()->prepare($query); |
125
|
|
|
$this->bindValuesToStatement($statement, $queryBind); |
126
|
|
|
$statement->execute(); |
127
|
|
|
|
128
|
|
|
return $statement->fetch(PDO::FETCH_ASSOC); |
129
|
|
|
} |
130
|
|
|
|
131
|
|
|
/** |
132
|
|
|
* Get a set of data according to the expression and the chunk. |
133
|
|
|
* |
134
|
|
|
* @param array $expression |
135
|
|
|
* @param int $limit |
136
|
|
|
* @param int $offset |
137
|
|
|
* |
138
|
|
|
* @return array |
139
|
|
|
* |
140
|
|
|
* @codeCoverageIgnore Don't test external library. |
141
|
|
|
*/ |
142
|
|
|
public function getDataSet(array $expression, $limit = PHP_INT_MAX, $offset = 0) |
143
|
|
|
{ |
144
|
|
|
$queryBind = []; |
145
|
|
|
|
146
|
|
|
$query = $this->getSelectQueryForExpression($expression, $queryBind, $limit, $offset); |
147
|
|
|
$statement = $this->getDataStorage()->prepare($query); |
148
|
|
|
$this->bindValuesToStatement($statement, $queryBind); |
149
|
|
|
$statement->execute(); |
150
|
|
|
|
151
|
|
|
return $statement->fetchAll(PDO::FETCH_ASSOC); |
152
|
|
|
} |
153
|
|
|
|
154
|
|
|
/** |
155
|
|
|
* Get the number of matched data in the set according to the expression. |
156
|
|
|
* |
157
|
|
|
* @param array $expression |
158
|
|
|
* |
159
|
|
|
* @return int |
160
|
|
|
* |
161
|
|
|
* @codeCoverageIgnore Don't test external library. |
162
|
|
|
*/ |
163
|
|
|
public function getDataCardinality(array $expression) |
164
|
|
|
{ |
165
|
|
|
$queryBind = []; |
166
|
|
|
|
167
|
|
|
$query = $this->getSelectQueryForExpression($expression, $queryBind); |
168
|
|
|
$statement = $this->getDataStorage()->prepare($query); |
169
|
|
|
$this->bindValuesToStatement($statement, $queryBind); |
170
|
|
|
$statement->execute(); |
171
|
|
|
|
172
|
|
|
return $statement->rowCount(); |
173
|
|
|
} |
174
|
|
|
|
175
|
|
|
/** |
176
|
|
|
* Builds SQL query from the expression. |
177
|
|
|
* |
178
|
|
|
* @param array $expression |
179
|
|
|
* @param array $queryBind |
180
|
|
|
* @param int $limit |
181
|
|
|
* @param int $offset |
182
|
|
|
* |
183
|
|
|
* @return string |
184
|
|
|
*/ |
185
|
3 |
|
private function getSelectQueryForExpression( |
186
|
|
|
array $expression, |
187
|
|
|
array &$queryBind, |
188
|
|
|
$limit = self::DATA_SET_RECORD_LIMIT, |
189
|
|
|
$offset = 0 |
190
|
|
|
) { |
191
|
3 |
|
$query = "SELECT * FROM {$this->dataGroup}"; |
192
|
|
|
|
193
|
|
|
// Prepare WHERE expression. |
194
|
3 |
|
if (!empty($expression)) { |
195
|
2 |
|
$query .= $this->getWhereExpression($expression, $queryBind); |
196
|
2 |
|
} |
197
|
|
|
|
198
|
3 |
|
$query .= " LIMIT {$limit}"; |
199
|
3 |
|
$query .= " OFFSET {$offset}"; |
200
|
|
|
|
201
|
3 |
|
return $query; |
202
|
|
|
} |
203
|
|
|
|
204
|
|
|
/** |
205
|
|
|
* Creates a WHERE expression for the SQL query. |
206
|
|
|
* |
207
|
|
|
* @param array $expression |
208
|
|
|
* @param array $queryBind |
209
|
|
|
* |
210
|
|
|
* @return string |
211
|
|
|
*/ |
212
|
5 |
|
private function getWhereExpression(array $expression, array &$queryBind) |
213
|
|
|
{ |
214
|
5 |
|
$whereExpression = ''; |
215
|
5 |
|
$queryParams = []; |
216
|
|
|
|
217
|
5 |
|
foreach ($expression as $column => $value) { |
218
|
|
|
// allow special cases |
219
|
|
|
// @example ['my_column LIKE ?' => 'some value%'] |
|
|
|
|
220
|
4 |
|
$queryParams[] = strpos($column, '?') === false ? "{$column}=?" : $column; |
221
|
4 |
|
$queryBind[] = $value; |
222
|
5 |
|
} |
223
|
|
|
|
224
|
5 |
|
if (!empty($queryParams)) { |
225
|
4 |
|
$whereExpression = ' WHERE '.implode(' AND ', $queryParams); |
226
|
4 |
|
} |
227
|
|
|
|
228
|
5 |
|
return $whereExpression; |
229
|
|
|
} |
230
|
|
|
|
231
|
|
|
/** |
232
|
|
|
* Insert or update entity in the storage. |
233
|
|
|
* |
234
|
|
|
* @param mixed $identifier |
235
|
|
|
* @param array $data |
236
|
|
|
* |
237
|
|
|
* @return mixed The ID of the saved entity in the storage |
238
|
|
|
* |
239
|
|
|
* @codeCoverageIgnore Don't test external library. |
240
|
|
|
*/ |
241
|
|
|
public function saveData($identifier, array $data) |
242
|
|
|
{ |
243
|
|
|
if (empty($identifier)) { |
244
|
|
|
$query = "INSERT INTO {$this->dataGroup}"; |
245
|
|
|
} else { |
246
|
|
|
$query = "UPDATE {$this->dataGroup}"; |
247
|
|
|
} |
248
|
|
|
|
249
|
|
|
$queryData = []; |
250
|
|
|
$queryBind = []; |
251
|
|
|
|
252
|
|
|
foreach ($data as $fieldName => $value) { |
253
|
|
|
$queryData[] = "{$fieldName}=?"; |
254
|
|
|
$queryBind[] = $value; |
255
|
|
|
} |
256
|
|
|
|
257
|
|
|
$query .= ' SET '.implode(', ', $queryData); |
258
|
|
|
|
259
|
|
|
if (!empty($identifier)) { |
260
|
|
|
$query .= " WHERE {$this->idKey}=?"; |
261
|
|
|
$queryBind[] = $identifier; |
262
|
|
|
} |
263
|
|
|
|
264
|
|
|
$statement = $this->getDataStorage()->prepare($query); |
265
|
|
|
$this->bindValuesToStatement($statement, $queryBind); |
266
|
|
|
$statement->execute(); |
267
|
|
|
|
268
|
|
|
return empty($identifier) ? $this->getDataStorage()->lastInsertId() : $identifier; |
269
|
|
|
} |
270
|
|
|
|
271
|
|
|
/** |
272
|
|
|
* Binds values to the statement. |
273
|
|
|
* |
274
|
|
|
* @param PDOStatement $statement |
275
|
|
|
* @param array $queryBind |
276
|
|
|
* |
277
|
|
|
* @codeCoverageIgnore Don't test external library. |
278
|
|
|
*/ |
279
|
|
|
private function bindValuesToStatement(PDOStatement &$statement, array $queryBind) |
280
|
|
|
{ |
281
|
|
|
foreach ($queryBind as $index => $data) { |
282
|
|
|
$paramType = PDO::PARAM_STR; |
283
|
|
|
|
284
|
|
|
if (is_null($data)) { |
285
|
|
|
$paramType = PDO::PARAM_NULL; |
286
|
|
|
} elseif (is_numeric($data)) { |
287
|
|
|
$paramType = PDO::PARAM_INT; |
288
|
|
|
} |
289
|
|
|
|
290
|
|
|
$statement->bindValue($index + 1, $data, $paramType); |
291
|
|
|
} |
292
|
|
|
} |
293
|
|
|
|
294
|
|
|
/** |
295
|
|
|
* Removes an entity from the storage. |
296
|
|
|
* |
297
|
|
|
* @param mixed $identifier |
298
|
|
|
* |
299
|
|
|
* @return bool |
300
|
|
|
* |
301
|
|
|
* @codeCoverageIgnore Don't test external library. |
302
|
|
|
*/ |
303
|
|
|
public function deleteData($identifier) |
304
|
|
|
{ |
305
|
|
|
$statement = $this->getDataStorage()->prepare("DELETE FROM WHERE {$this->idKey}=?"); |
306
|
|
|
|
307
|
|
|
return $statement->execute([$identifier]); |
308
|
|
|
} |
309
|
|
|
} |
310
|
|
|
|
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.