1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace LRC\Repository; |
4
|
|
|
|
5
|
|
|
/** |
6
|
|
|
* Base class for database-backed repositories for data access. |
7
|
|
|
* |
8
|
|
|
* @SuppressWarnings(PHPMD.ExcessiveClassComplexity) |
9
|
|
|
*/ |
10
|
|
|
class DbRepository extends ManagedRepository implements RepositoryInterface |
11
|
|
|
{ |
12
|
|
|
/** |
13
|
|
|
* @var \Anax\Database\DatabaseQueryBuilder Database service. |
14
|
|
|
*/ |
15
|
|
|
protected $db; |
16
|
|
|
|
17
|
|
|
/** |
18
|
|
|
* @var string Database table name. |
19
|
|
|
*/ |
20
|
|
|
protected $table; |
21
|
|
|
|
22
|
|
|
/** |
23
|
|
|
* @var string Model class name. |
24
|
|
|
*/ |
25
|
|
|
protected $modelClass; |
26
|
|
|
|
27
|
|
|
/** |
28
|
|
|
* @var string Primary key column. |
29
|
|
|
*/ |
30
|
|
|
protected $key; |
31
|
|
|
|
32
|
|
|
|
33
|
|
|
/** |
34
|
|
|
* Constructor. |
35
|
|
|
* |
36
|
|
|
* @param \Anax\Database\DatabaseQueryBuilder $db Database service. |
37
|
|
|
* @param string $table Database table name. |
38
|
|
|
* @param string $modelClass Model class name. |
39
|
|
|
* @param string $key Primary key column. |
40
|
|
|
*/ |
41
|
30 |
|
public function __construct($db, $table, $modelClass, $key = 'id') |
42
|
|
|
{ |
43
|
30 |
|
$this->db = $db; |
44
|
30 |
|
$this->table = $table; |
45
|
30 |
|
$this->modelClass = $modelClass; |
46
|
30 |
|
$this->key = $key; |
47
|
30 |
|
} |
48
|
|
|
|
49
|
|
|
|
50
|
|
|
/** |
51
|
|
|
* Return the name of the database table represented by the repository. |
52
|
|
|
*/ |
53
|
7 |
|
public function getCollectionName() |
54
|
|
|
{ |
55
|
7 |
|
return $this->table; |
56
|
|
|
} |
57
|
|
|
|
58
|
|
|
|
59
|
|
|
/** |
60
|
|
|
* Return the class of the model handled by the repository. |
61
|
|
|
*/ |
62
|
14 |
|
public function getModelClass() |
63
|
|
|
{ |
64
|
14 |
|
return $this->modelClass; |
65
|
|
|
} |
66
|
|
|
|
67
|
|
|
|
68
|
|
|
/** |
69
|
|
|
* Find and return first entry by key. |
70
|
|
|
* |
71
|
|
|
* @param string|null $column Key column name (pass null to use registered primary key). |
72
|
|
|
* @param mixed $value Key value. |
73
|
|
|
* |
74
|
|
|
* @return mixed Model instance. |
75
|
|
|
*/ |
76
|
18 |
|
public function find($column, $value) |
77
|
|
|
{ |
78
|
18 |
|
return $this->getFirst((is_null($column) ? $this->key : $column) . ' = ?', [$value]); |
79
|
|
|
} |
80
|
|
|
|
81
|
|
|
|
82
|
|
|
/** |
83
|
|
|
* Retrieve first entry, optionally filtered by search criteria. |
84
|
|
|
* |
85
|
|
|
* @param string $conditions Where conditions. |
86
|
|
|
* @param array $values Array of condition values to bind. |
87
|
|
|
* @param array $options Query options. |
88
|
|
|
* |
89
|
|
|
* @return mixed Model instance. |
90
|
|
|
*/ |
91
|
19 |
View Code Duplication |
public function getFirst($conditions = null, $values = [], $options = []) |
|
|
|
|
92
|
|
|
{ |
93
|
19 |
|
$query = $this->executeQuery(null, $conditions, $values, $options); |
94
|
19 |
|
if (!empty($this->fetchRefs)) { |
95
|
3 |
|
$res = $query->fetch(); |
96
|
3 |
|
$model = ($res ? $this->populateModelFromJoin($res) : $res); |
|
|
|
|
97
|
3 |
|
} else { |
98
|
19 |
|
$model = $query->fetchClass($this->modelClass); |
|
|
|
|
99
|
|
|
} |
100
|
19 |
|
if ($model && isset($this->manager)) { |
101
|
10 |
|
$this->manager->manageModel($model); |
|
|
|
|
102
|
10 |
|
} |
103
|
19 |
|
$this->fetchReferences(false); |
104
|
19 |
|
return $model; |
105
|
|
|
} |
106
|
|
|
|
107
|
|
|
|
108
|
|
|
/** |
109
|
|
|
* Retrieve all entries, optionally filtered by search criteria. |
110
|
|
|
* |
111
|
|
|
* @param string $conditions Where conditions. |
112
|
|
|
* @param array $values Array of condition values to bind. |
113
|
|
|
* @param array $options Query options. |
114
|
|
|
* |
115
|
|
|
* @return array Array of all matching entries. |
116
|
|
|
*/ |
117
|
4 |
View Code Duplication |
public function getAll($conditions = null, $values = [], $options = []) |
|
|
|
|
118
|
|
|
{ |
119
|
4 |
|
$query = $this->executeQuery(null, $conditions, $values, $options); |
120
|
4 |
|
if (!empty($this->fetchRefs)) { |
121
|
2 |
|
$models = []; |
122
|
2 |
|
foreach ($query->fetchAll() as $model) { |
123
|
2 |
|
$models[] = $this->populateModelFromJoin($model); |
124
|
2 |
|
} |
125
|
2 |
|
} else { |
126
|
2 |
|
$models = $query->fetchAllClass($this->modelClass); |
|
|
|
|
127
|
|
|
} |
128
|
4 |
|
if (isset($this->manager)) { |
129
|
3 |
|
foreach ($models as $model) { |
130
|
3 |
|
$this->manager->manageModel($model); |
131
|
3 |
|
} |
132
|
3 |
|
} |
133
|
4 |
|
$this->fetchReferences(false); |
134
|
4 |
|
return $models; |
135
|
|
|
} |
136
|
|
|
|
137
|
|
|
|
138
|
|
|
/** |
139
|
|
|
* Save entry by inserting if ID is missing and updating if ID exists. |
140
|
|
|
* |
141
|
|
|
* @param mixed $model Model instance. |
142
|
|
|
* |
143
|
|
|
* @return void |
144
|
|
|
*/ |
145
|
5 |
|
public function save($model) |
146
|
|
|
{ |
147
|
5 |
|
if (isset($model->{$this->key})) { |
148
|
5 |
|
return $this->update($model); |
149
|
|
|
} |
150
|
|
|
|
151
|
3 |
|
return $this->create($model); |
152
|
|
|
} |
153
|
|
|
|
154
|
|
|
|
155
|
|
|
/** |
156
|
|
|
* Delete entry. |
157
|
|
|
* |
158
|
|
|
* @param mixed $model Model instance. |
159
|
|
|
*/ |
160
|
1 |
|
public function delete($model) |
161
|
|
|
{ |
162
|
1 |
|
$this->db->connect() |
163
|
1 |
|
->deleteFrom($this->table) |
164
|
1 |
|
->where($this->key . ' = ?') |
165
|
1 |
|
->execute([$model->{$this->key}]); |
|
|
|
|
166
|
1 |
|
$model->{$this->key} = null; |
167
|
1 |
|
} |
168
|
|
|
|
169
|
|
|
|
170
|
|
|
/** |
171
|
|
|
* Count entries, optionally filtered by search criteria. |
172
|
|
|
* |
173
|
|
|
* @param string $conditions Where conditions. |
174
|
|
|
* @param array $values Array of condition values to bind. |
175
|
|
|
* |
176
|
|
|
* @return int Number of entries. |
177
|
|
|
*/ |
178
|
4 |
|
public function count($conditions = null, $values = []) |
179
|
|
|
{ |
180
|
4 |
|
$res = $this->executeQuery('COUNT(' . $this->key . ') AS num', $conditions, $values) |
181
|
4 |
|
->fetch(); |
182
|
4 |
|
return (isset($res->num) ? (int)$res->num : 0); |
183
|
|
|
} |
184
|
|
|
|
185
|
|
|
|
186
|
|
|
/** |
187
|
|
|
* Execute query for selection methods. |
188
|
|
|
* |
189
|
|
|
* @param string $select Selection criteria. |
190
|
|
|
* @param string $conditions Where conditions. |
191
|
|
|
* @param array $values Array of where condition values to bind. |
192
|
|
|
* @param array $options Query options. |
193
|
|
|
* |
194
|
|
|
* @return \Anax\Database\DatabaseQueryBuilder Database service instance with executed internal query. |
195
|
|
|
*/ |
196
|
26 |
|
protected function executeQuery($select = null, $conditions = null, $values = [], $options = []) |
197
|
|
|
{ |
198
|
26 |
|
$query = $this->db->connect(); |
199
|
26 |
|
if (!empty($this->fetchRefs)) { |
200
|
6 |
|
$query = $this->setupJoin($query, $select, $conditions, (isset($options['order']) ? $options['order'] : null)); |
201
|
6 |
|
} else { |
202
|
26 |
|
$query = (!is_null($select) ? $query->select($select) : $query->select()); |
203
|
26 |
|
$query = $query->from($this->table); |
204
|
26 |
|
if (!is_null($conditions)) { |
205
|
26 |
|
$query = $query->where($conditions); |
206
|
26 |
|
} |
207
|
26 |
|
if (isset($options['order'])) { |
208
|
4 |
|
$query = $query->orderBy($options['order']); |
209
|
4 |
|
} |
210
|
|
|
} |
211
|
|
|
|
212
|
26 |
|
if (isset($options['limit'])) { |
213
|
2 |
|
$query = $query->limit($options['limit']); |
214
|
2 |
|
} |
215
|
26 |
|
if (isset($options['offset'])) { |
216
|
2 |
|
$query = $query->offset($options['offset']); |
217
|
2 |
|
} |
218
|
|
|
|
219
|
26 |
|
return $query->execute($values); |
|
|
|
|
220
|
|
|
} |
221
|
|
|
|
222
|
|
|
|
223
|
|
|
|
224
|
|
|
/** |
225
|
|
|
* Populate model instance including retrieved references from join query result. |
226
|
|
|
* |
227
|
|
|
* @param object $result Query result. |
228
|
|
|
* |
229
|
|
|
* @return mixed Populated model instance. |
230
|
|
|
*/ |
231
|
6 |
|
protected function populateModelFromJoin($result) |
232
|
|
|
{ |
233
|
|
|
// extract main model |
234
|
6 |
|
$model = new $this->modelClass(); |
235
|
6 |
|
foreach (array_keys(get_object_vars($model)) as $attr) { |
236
|
6 |
|
$model->$attr = $result->$attr; |
237
|
6 |
|
} |
238
|
|
|
|
239
|
|
|
// extract referenced models |
240
|
6 |
|
$refs = $model->getReferences(); |
241
|
6 |
|
$refs2 = (is_array($this->fetchRefs) ? $this->fetchRefs : array_keys($refs)); |
242
|
6 |
|
sort($refs2); |
243
|
6 |
|
foreach ($refs2 as $idx => $name) { |
244
|
6 |
|
$prefix = "REF{$idx}_{$name}__"; |
245
|
|
|
|
246
|
|
|
// handle null result |
247
|
6 |
|
if (is_null($result->{$prefix . $refs[$name]['key']})) { |
248
|
3 |
|
$refModel = null; |
249
|
3 |
|
} else { |
250
|
6 |
|
$refModel = new $refs[$name]['model'](); |
251
|
6 |
|
foreach (array_keys(get_object_vars($refModel)) as $attr) { |
252
|
6 |
|
$refModel->$attr = $result->{$prefix . $attr}; |
253
|
6 |
|
} |
254
|
|
|
} |
255
|
|
|
|
256
|
|
|
// inject manager reference |
257
|
6 |
|
if ($refModel && $this->manager) { |
258
|
6 |
|
$this->manager->manageModel($refModel); |
259
|
6 |
|
} |
260
|
|
|
|
261
|
6 |
|
$model->$name = $refModel; |
262
|
6 |
|
} |
263
|
|
|
|
264
|
6 |
|
return $model; |
265
|
|
|
} |
266
|
|
|
|
267
|
|
|
|
268
|
|
|
/** |
269
|
|
|
* Set up join query for reference retrieval. |
270
|
|
|
* |
271
|
|
|
* @param \Anax\Database\DatabaseQueryBuilder $query Database service instance with initialized query. |
272
|
|
|
* @param string $select Selection criteria. |
273
|
|
|
* @param string $conditions Where conditions. |
274
|
|
|
* @param string $order Order by clause. |
275
|
|
|
* |
276
|
|
|
* @return \Anax\Database\DatabaseQueryBuilder Database service instance with prepared join query. |
277
|
|
|
* |
278
|
|
|
* @SuppressWarnings(PHPMD.CyclomaticComplexity) |
279
|
|
|
* @SuppressWarnings(PHPMD.NPathComplexity) |
280
|
|
|
*/ |
281
|
6 |
|
private function setupJoin($query, $select, $conditions, $order = null) |
282
|
|
|
{ |
283
|
|
|
// find references |
284
|
6 |
|
$model = new $this->modelClass(); |
285
|
6 |
|
$refs = $model->getReferences(); |
286
|
6 |
|
if (is_array($this->fetchRefs)) { |
287
|
5 |
|
$refs = array_intersect_key($refs, array_flip($this->fetchRefs)); |
288
|
5 |
|
} |
289
|
6 |
|
ksort($refs); |
290
|
|
|
|
291
|
|
|
// prefix main model selection |
292
|
6 |
|
if (!is_null($select)) { |
293
|
1 |
|
$select = $this->prefixModelAttributes($select, $model); |
294
|
1 |
|
} else { |
295
|
6 |
|
$select = $this->table . '.*'; |
296
|
|
|
} |
297
|
|
|
|
298
|
|
|
// set up reference aliases and join conditions |
299
|
6 |
|
$select = [$select]; |
300
|
6 |
|
$join = []; |
301
|
6 |
|
$idx = 0; |
302
|
6 |
|
foreach ($refs as $name => $ref) { |
303
|
|
|
// prefix attributes |
304
|
6 |
|
$refTable = "REF{$idx}_{$name}"; |
305
|
6 |
|
$idx++; |
306
|
6 |
|
foreach (array_keys(get_object_vars(new $ref['model']())) as $attr) { |
307
|
6 |
|
$select[] = "{$refTable}.{$attr} AS '{$refTable}__{$attr}'"; |
308
|
6 |
|
} |
309
|
|
|
|
310
|
|
|
// generate join conditions |
311
|
6 |
|
$joinCond = $this->table . '.' . $ref['attribute'] . " = {$refTable}." . $ref['key']; |
312
|
6 |
|
$refRepo = $this->manager->getByClass($ref['model']); |
313
|
6 |
|
if ($this->softRefs && $refRepo instanceof SoftDbRepository) { |
314
|
3 |
|
$joinCond .= " AND $refTable." . $refRepo->getDeletedAttribute() . ' IS NULL'; |
315
|
3 |
|
} |
316
|
6 |
|
$join[] = [$refRepo->getCollectionName() . " AS $refTable", $joinCond]; |
317
|
6 |
|
} |
318
|
|
|
|
319
|
|
|
// generate join query |
320
|
6 |
|
$query = $query->select(implode(', ', $select))->from($this->table); |
321
|
6 |
|
foreach ($join as $args) { |
322
|
6 |
|
$query = $query->leftJoin(...$args); |
|
|
|
|
323
|
6 |
|
} |
324
|
|
|
|
325
|
|
|
// prefix where conditions |
326
|
6 |
|
if (!is_null($conditions)) { |
327
|
5 |
|
$query = $query->where($this->prefixModelAttributes($conditions, $model)); |
328
|
5 |
|
} |
329
|
|
|
|
330
|
|
|
// prefix order by clause |
331
|
6 |
|
if (!is_null($order)) { |
332
|
5 |
|
$query = $query->orderBy($this->prefixModelAttributes($order, $model)); |
333
|
5 |
|
} |
334
|
|
|
|
335
|
6 |
|
return $query; |
336
|
|
|
} |
337
|
|
|
|
338
|
|
|
|
339
|
|
|
/** |
340
|
|
|
* Prefix model attributes with the associated table name. |
341
|
|
|
* |
342
|
|
|
* @param string $input Input string. |
343
|
|
|
* @param object $model Model instance. |
344
|
|
|
* |
345
|
|
|
* @return string String with table-prefixed attributes. |
346
|
|
|
*/ |
347
|
6 |
|
private function prefixModelAttributes($input, $model) |
348
|
|
|
{ |
349
|
6 |
|
foreach (array_keys(get_object_vars($model)) as $attr) { |
350
|
6 |
|
$input = preg_replace('/\\b' . $attr . '\\b/', $this->table . ".$attr", $input); |
351
|
6 |
|
} |
352
|
6 |
|
return $input; |
353
|
|
|
} |
354
|
|
|
|
355
|
|
|
|
356
|
|
|
/** |
357
|
|
|
* Create new entry. |
358
|
|
|
* |
359
|
|
|
* @param mixed $model Model instance. |
360
|
|
|
*/ |
361
|
3 |
|
private function create($model) |
362
|
|
|
{ |
363
|
3 |
|
$attrs = $this->getMutableAttributes($model); |
364
|
3 |
|
$this->db |
365
|
3 |
|
->connect() |
366
|
3 |
|
->insert($this->table, array_keys($attrs)) |
367
|
3 |
|
->execute(array_values($attrs)); |
|
|
|
|
368
|
3 |
|
$model->{$this->key} = $this->db->lastInsertId(); |
369
|
3 |
|
} |
370
|
|
|
|
371
|
|
|
|
372
|
|
|
/** |
373
|
|
|
* Update entry. |
374
|
|
|
* |
375
|
|
|
* @param mixed $model Model instance. |
376
|
|
|
*/ |
377
|
5 |
|
private function update($model) |
378
|
|
|
{ |
379
|
5 |
|
$attrs = $this->getMutableAttributes($model); |
380
|
5 |
|
$values = array_values($attrs); |
381
|
5 |
|
$values[] = $model->{$this->key}; |
382
|
5 |
|
$this->db |
383
|
5 |
|
->connect() |
384
|
5 |
|
->update($this->table, array_keys($attrs)) |
385
|
5 |
|
->where($this->key . ' = ?') |
386
|
5 |
|
->execute($values); |
|
|
|
|
387
|
5 |
|
} |
388
|
|
|
|
389
|
|
|
|
390
|
|
|
/** |
391
|
|
|
* Get mutable model attributes. |
392
|
|
|
* |
393
|
|
|
* @param object $model Model instance. |
394
|
|
|
* |
395
|
|
|
* @return array Array of attributes. |
396
|
|
|
*/ |
397
|
5 |
|
private function getMutableAttributes($model) |
398
|
|
|
{ |
399
|
5 |
|
$attrs = get_object_vars($model); |
400
|
5 |
|
unset($attrs[$this->key]); |
401
|
|
|
|
402
|
|
|
// remove reference attributes, if any |
403
|
5 |
|
if ($model instanceof ManagedModelInterface) { |
404
|
2 |
|
foreach (array_keys($model->getReferences()) as $ref) { |
405
|
2 |
|
unset($attrs[$ref]); |
406
|
2 |
|
} |
407
|
2 |
|
} |
408
|
|
|
|
409
|
5 |
|
return $attrs; |
410
|
|
|
} |
411
|
|
|
} |
412
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.