1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
namespace Chubbyphp\Model\Doctrine\DBAL\Repository; |
6
|
|
|
|
7
|
|
|
use Chubbyphp\Model\Collection\ModelCollectionInterface; |
8
|
|
|
use Chubbyphp\Model\ModelInterface; |
9
|
|
|
use Chubbyphp\Model\Reference\ModelReferenceInterface; |
10
|
|
|
use Chubbyphp\Model\RepositoryInterface; |
11
|
|
|
use Chubbyphp\Model\ResolverInterface; |
12
|
|
|
use Chubbyphp\Model\StorageCache\NullStorageCache; |
13
|
|
|
use Chubbyphp\Model\StorageCache\StorageCacheInterface; |
14
|
|
|
use Doctrine\DBAL\Connection; |
15
|
|
|
use Doctrine\DBAL\Query\QueryBuilder; |
16
|
|
|
use Psr\Log\LoggerInterface; |
17
|
|
|
use Psr\Log\NullLogger; |
18
|
|
|
|
19
|
|
|
abstract class AbstractDoctrineRepository implements RepositoryInterface |
20
|
|
|
{ |
21
|
|
|
/** |
22
|
|
|
* @var Connection |
23
|
|
|
*/ |
24
|
|
|
protected $connection; |
25
|
|
|
|
26
|
|
|
/** |
27
|
|
|
* @var ResolverInterface |
28
|
|
|
*/ |
29
|
|
|
protected $resolver; |
30
|
|
|
|
31
|
|
|
/** |
32
|
|
|
* @var StorageCacheInterface |
33
|
|
|
*/ |
34
|
|
|
protected $storageCache; |
35
|
|
|
|
36
|
|
|
/** |
37
|
|
|
* @var LoggerInterface |
38
|
|
|
*/ |
39
|
|
|
protected $logger; |
40
|
|
|
|
41
|
|
|
/** |
42
|
|
|
* @param Connection $connection |
43
|
|
|
* @param ResolverInterface $resolver |
44
|
|
|
* @param StorageCacheInterface $storageCache |
45
|
|
|
* @param LoggerInterface|null $logger |
46
|
|
|
*/ |
47
|
|
|
public function __construct( |
48
|
|
|
Connection $connection, |
49
|
|
|
ResolverInterface $resolver, |
50
|
|
|
StorageCacheInterface $storageCache, |
51
|
|
|
LoggerInterface $logger = null |
52
|
|
|
) { |
53
|
|
|
$this->connection = $connection; |
54
|
|
|
$this->resolver = $resolver; |
55
|
|
|
$this->storageCache = $storageCache ?? new NullStorageCache(); |
56
|
|
|
$this->logger = $logger ?? new NullLogger(); |
57
|
|
|
} |
58
|
|
|
|
59
|
|
|
/** |
60
|
|
|
* @param string $id |
61
|
|
|
* |
62
|
|
|
* @return ModelInterface|null |
63
|
|
|
*/ |
64
|
|
|
public function find(string $id = null) |
65
|
|
|
{ |
66
|
|
|
if (null === $id) { |
67
|
|
|
return null; |
68
|
|
|
} |
69
|
|
|
|
70
|
|
|
$table = $this->getTable(); |
71
|
|
|
|
72
|
|
|
$this->logger->info('model: find row within table {table} with id {id}', ['table' => $table, 'id' => $id]); |
73
|
|
|
|
74
|
|
|
if ($this->storageCache->has($id)) { |
75
|
|
|
return $this->fromPersistence($this->storageCache->get($id)); |
76
|
|
|
} |
77
|
|
|
|
78
|
|
|
$qb = $this->connection->createQueryBuilder(); |
79
|
|
|
$qb->select('*')->from($this->getTable())->where($qb->expr()->eq('id', ':id'))->setParameter('id', $id); |
80
|
|
|
|
81
|
|
|
$row = $qb->execute()->fetch(\PDO::FETCH_ASSOC); |
82
|
|
|
if (false === $row) { |
83
|
|
|
$this->logger->notice( |
84
|
|
|
'model: row within table {table} with id {id} not found', |
85
|
|
|
['table' => $table, 'id' => $id] |
86
|
|
|
); |
87
|
|
|
|
88
|
|
|
return null; |
89
|
|
|
} |
90
|
|
|
|
91
|
|
|
$this->storageCache->set($row['id'], $row); |
92
|
|
|
|
93
|
|
|
return $this->fromPersistence($row); |
94
|
|
|
} |
95
|
|
|
|
96
|
|
|
/** |
97
|
|
|
* @param array $criteria |
98
|
|
|
* |
99
|
|
|
* @return null|ModelInterface |
100
|
|
|
*/ |
101
|
|
|
public function findOneBy(array $criteria, array $orderBy = null) |
102
|
|
|
{ |
103
|
|
|
$models = $this->findBy($criteria, $orderBy, 1, 0); |
104
|
|
|
|
105
|
|
|
if ([] === $models) { |
106
|
|
|
$this->logger->notice( |
107
|
|
|
'model: row within table {table} with criteria {criteria} not found', |
108
|
|
|
[ |
109
|
|
|
'table' => $this->getTable(), |
110
|
|
|
'criteria' => $criteria, |
111
|
|
|
'orderBy' => $orderBy, |
112
|
|
|
'limit' => 1, |
113
|
|
|
'offset' => 0, |
114
|
|
|
] |
115
|
|
|
); |
116
|
|
|
|
117
|
|
|
return null; |
118
|
|
|
} |
119
|
|
|
|
120
|
|
|
return reset($models); |
121
|
|
|
} |
122
|
|
|
|
123
|
|
|
/** |
124
|
|
|
* @param array $criteria |
125
|
|
|
* |
126
|
|
|
* @return ModelInterface[]|array |
127
|
|
|
*/ |
128
|
|
|
public function findBy(array $criteria, array $orderBy = null, int $limit = null, int $offset = null): array |
129
|
|
|
{ |
130
|
|
|
$table = $this->getTable(); |
131
|
|
|
|
132
|
|
|
$this->logger->info( |
133
|
|
|
'model: find rows within table {table} with criteria {criteria}', |
134
|
|
|
['table' => $table, 'criteria' => $criteria, 'orderBy' => $orderBy, 'limit' => $limit, 'offset' => $offset] |
135
|
|
|
); |
136
|
|
|
|
137
|
|
|
$qb = $this->connection->createQueryBuilder() |
138
|
|
|
->select('*') |
139
|
|
|
->from($table) |
140
|
|
|
->setFirstResult($offset) |
141
|
|
|
->setMaxResults($limit) |
142
|
|
|
; |
143
|
|
|
|
144
|
|
|
$this->addCriteriaToQueryBuilder($qb, $criteria); |
145
|
|
|
$this->addOrderByToQueryBuilder($qb, $orderBy); |
146
|
|
|
|
147
|
|
|
$rows = $qb->execute()->fetchAll(\PDO::FETCH_ASSOC); |
148
|
|
|
|
149
|
|
|
if ([] === $rows) { |
150
|
|
|
return []; |
151
|
|
|
} |
152
|
|
|
|
153
|
|
|
$models = []; |
154
|
|
|
foreach ($rows as $row) { |
155
|
|
|
$this->storageCache->set($row['id'], $row); |
156
|
|
|
|
157
|
|
|
$models[] = $this->fromPersistence($row); |
158
|
|
|
} |
159
|
|
|
|
160
|
|
|
return $models; |
161
|
|
|
} |
162
|
|
|
|
163
|
|
|
/** |
164
|
|
|
* @param QueryBuilder $qb |
165
|
|
|
* @param array $criteria |
166
|
|
|
*/ |
167
|
|
|
protected function addCriteriaToQueryBuilder(QueryBuilder $qb, array $criteria) |
168
|
|
|
{ |
169
|
|
|
foreach ($criteria as $field => $value) { |
170
|
|
|
$qb->andWhere($qb->expr()->eq($field, ':'.$field)); |
171
|
|
|
$qb->setParameter($field, $value); |
172
|
|
|
} |
173
|
|
|
} |
174
|
|
|
|
175
|
|
|
/** |
176
|
|
|
* @param QueryBuilder $qb |
177
|
|
|
* @param array|null $orderBy |
178
|
|
|
*/ |
179
|
|
|
protected function addOrderByToQueryBuilder(QueryBuilder $qb, array $orderBy = null) |
180
|
|
|
{ |
181
|
|
|
if (null === $orderBy) { |
182
|
|
|
return; |
183
|
|
|
} |
184
|
|
|
|
185
|
|
|
foreach ($orderBy as $field => $direction) { |
186
|
|
|
$qb->addOrderBy($field, $direction); |
187
|
|
|
} |
188
|
|
|
} |
189
|
|
|
|
190
|
|
|
/** |
191
|
|
|
* @param ModelInterface $model |
192
|
|
|
* |
193
|
|
|
* @return RepositoryInterface |
194
|
|
|
*/ |
195
|
|
|
public function persist(ModelInterface $model): RepositoryInterface |
196
|
|
|
{ |
197
|
|
|
$id = $model->getId(); |
198
|
|
|
$row = $model->toPersistence(); |
199
|
|
|
|
200
|
|
|
$modelCollections = []; |
201
|
|
|
foreach ($row as $field => $value) { |
202
|
|
|
if ($value instanceof ModelCollectionInterface) { |
203
|
|
|
$modelCollections[] = $value; |
204
|
|
|
unset($row[$field]); |
205
|
|
|
} elseif ($value instanceof ModelReferenceInterface) { |
206
|
|
|
$row[$field.'Id'] = $this->persistReference($value); |
207
|
|
|
unset($row[$field]); |
208
|
|
|
} |
209
|
|
|
} |
210
|
|
|
|
211
|
|
|
if (null === $this->find($id)) { |
212
|
|
|
$this->insert($id, $row); |
213
|
|
|
} else { |
214
|
|
|
$this->update($id, $row); |
215
|
|
|
} |
216
|
|
|
|
217
|
|
|
foreach ($modelCollections as $modelCollection) { |
218
|
|
|
$this->persistCollection($modelCollection); |
219
|
|
|
} |
220
|
|
|
|
221
|
|
|
$this->storageCache->set($id, $row); |
222
|
|
|
|
223
|
|
|
return $this; |
224
|
|
|
} |
225
|
|
|
|
226
|
|
|
/** |
227
|
|
|
* @param ModelInterface $model |
228
|
|
|
* |
229
|
|
|
* @return RepositoryInterface |
230
|
|
|
*/ |
231
|
|
|
public function remove(ModelInterface $model): RepositoryInterface |
232
|
|
|
{ |
233
|
|
|
$table = $this->getTable(); |
234
|
|
|
|
235
|
|
|
$this->logger->info( |
236
|
|
|
'model: remove row from table {table} with id {id}', |
237
|
|
|
['table' => $table, 'id' => $model->getId()] |
238
|
|
|
); |
239
|
|
|
|
240
|
|
|
$row = $model->toPersistence(); |
241
|
|
|
|
242
|
|
|
foreach ($row as $field => $value) { |
243
|
|
|
if ($value instanceof ModelCollectionInterface) { |
244
|
|
|
$this->removeRelatedModels($value); |
245
|
|
|
} elseif ($value instanceof ModelReferenceInterface) { |
246
|
|
|
if (null !== $initialModel = $value->getInitialModel()) { |
247
|
|
|
$this->removeRelatedModel($initialModel); |
248
|
|
|
} |
249
|
|
|
} |
250
|
|
|
} |
251
|
|
|
|
252
|
|
|
$this->connection->delete($table, ['id' => $model->getId()]); |
253
|
|
|
|
254
|
|
|
$this->storageCache->remove($model->getId()); |
255
|
|
|
|
256
|
|
|
return $this; |
257
|
|
|
} |
258
|
|
|
|
259
|
|
|
/** |
260
|
|
|
* @return RepositoryInterface |
261
|
|
|
*/ |
262
|
|
|
public function clear(): RepositoryInterface |
263
|
|
|
{ |
264
|
|
|
$this->storageCache->clear(); |
265
|
|
|
|
266
|
|
|
return $this; |
267
|
|
|
} |
268
|
|
|
|
269
|
|
|
/** |
270
|
|
|
* @param string $id |
271
|
|
|
* @param array $row |
272
|
|
|
*/ |
273
|
|
View Code Duplication |
protected function insert(string $id, array $row) |
|
|
|
|
274
|
|
|
{ |
275
|
|
|
$table = $this->getTable(); |
276
|
|
|
|
277
|
|
|
$this->logger->info( |
278
|
|
|
'model: insert row into table {table} with id {id}', |
279
|
|
|
['table' => $table, 'id' => $id] |
280
|
|
|
); |
281
|
|
|
|
282
|
|
|
$this->connection->insert($table, $row); |
283
|
|
|
} |
284
|
|
|
|
285
|
|
|
/** |
286
|
|
|
* @param string $id |
287
|
|
|
* @param array $row |
288
|
|
|
*/ |
289
|
|
View Code Duplication |
protected function update(string $id, array $row) |
|
|
|
|
290
|
|
|
{ |
291
|
|
|
$table = $this->getTable(); |
292
|
|
|
|
293
|
|
|
$this->logger->info( |
294
|
|
|
'model: update row into table {table} with id {id}', |
295
|
|
|
['table' => $table, 'id' => $id] |
296
|
|
|
); |
297
|
|
|
|
298
|
|
|
$this->connection->update($table, $row, ['id' => $id]); |
299
|
|
|
} |
300
|
|
|
|
301
|
|
|
/** |
302
|
|
|
* @param ModelReferenceInterface $reference |
303
|
|
|
* @return null|string |
304
|
|
|
*/ |
305
|
|
|
private function persistReference(ModelReferenceInterface $reference) |
306
|
|
|
{ |
307
|
|
|
$initialModel = $reference->getInitialModel(); |
308
|
|
|
$model = $reference->getModel(); |
309
|
|
|
|
310
|
|
|
if (null !== $initialModel && (null === $model || $model->getId() !== $initialModel->getId())) { |
311
|
|
|
$this->removeRelatedModel($initialModel); |
312
|
|
|
} |
313
|
|
|
|
314
|
|
|
if (null !== $model) { |
315
|
|
|
$this->persistRelatedModel($model); |
316
|
|
|
|
317
|
|
|
return $model->getId(); |
318
|
|
|
} |
319
|
|
|
|
320
|
|
|
return null; |
321
|
|
|
} |
322
|
|
|
|
323
|
|
|
/** |
324
|
|
|
* @param ModelCollectionInterface $modelCollection |
325
|
|
|
*/ |
326
|
|
|
private function persistCollection(ModelCollectionInterface $modelCollection) |
327
|
|
|
{ |
328
|
|
|
$initialModels = $modelCollection->getInitialModels(); |
329
|
|
|
$models = $modelCollection->getModels(); |
330
|
|
|
|
331
|
|
|
foreach ($models as $model) { |
332
|
|
|
$this->persistRelatedModel($model); |
333
|
|
|
if (isset($initialModels[$model->getId()])) { |
334
|
|
|
unset($initialModels[$model->getId()]); |
335
|
|
|
} |
336
|
|
|
} |
337
|
|
|
|
338
|
|
|
foreach ($initialModels as $initialModel) { |
339
|
|
|
$this->removeRelatedModel($initialModel); |
340
|
|
|
} |
341
|
|
|
} |
342
|
|
|
|
343
|
|
|
/** |
344
|
|
|
* @param ModelInterface $model |
345
|
|
|
*/ |
346
|
|
|
private function persistRelatedModel(ModelInterface $model) |
347
|
|
|
{ |
348
|
|
|
$this->resolver->persist($model); |
349
|
|
|
} |
350
|
|
|
|
351
|
|
|
/** |
352
|
|
|
* @param ModelCollectionInterface $modelCollection |
353
|
|
|
*/ |
354
|
|
|
private function removeRelatedModels(ModelCollectionInterface $modelCollection) |
355
|
|
|
{ |
356
|
|
|
foreach ($modelCollection->getInitialModels() as $initialModel) { |
357
|
|
|
$this->removeRelatedModel($initialModel); |
358
|
|
|
} |
359
|
|
|
} |
360
|
|
|
|
361
|
|
|
/** |
362
|
|
|
* @param ModelInterface $model |
363
|
|
|
*/ |
364
|
|
|
private function removeRelatedModel(ModelInterface $model) |
365
|
|
|
{ |
366
|
|
|
$this->resolver->remove($model); |
367
|
|
|
} |
368
|
|
|
|
369
|
|
|
/** |
370
|
|
|
* @param array $row |
371
|
|
|
* |
372
|
|
|
* @return ModelInterface |
373
|
|
|
*/ |
374
|
|
|
abstract protected function fromPersistence(array $row): ModelInterface; |
375
|
|
|
|
376
|
|
|
/** |
377
|
|
|
* @return string |
378
|
|
|
*/ |
379
|
|
|
abstract protected function getTable(): string; |
380
|
|
|
} |
381
|
|
|
|
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.