Completed
Push — master ( 88e0af...09fe78 )
by Dominik
02:06
created

AbstractDoctrineRepository::persistRelatedModel()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 1
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
            $this->logger->info(
76
                'model: found row within cache for table {table} with id {id}',
77
                ['table' => $table, 'id' => $id]
78
            );
79
80
            return $this->fromPersistence($this->storageCache->get($id));
81
        }
82
83
        $qb = $this->connection->createQueryBuilder();
84
        $qb->select('*')->from($this->getTable())->where($qb->expr()->eq('id', ':id'))->setParameter('id', $id);
85
86
        $row = $qb->execute()->fetch(\PDO::FETCH_ASSOC);
87
        if (false === $row) {
88
            $this->logger->notice(
89
                'model: row within table {table} with id {id} not found',
90
                ['table' => $table, 'id' => $id]
91
            );
92
93
            return null;
94
        }
95
96
        $this->storageCache->set($row['id'], $row);
97
98
        return $this->fromPersistence($row);
99
    }
100
101
    /**
102
     * @param array $criteria
103
     *
104
     * @return null|ModelInterface
105
     */
106
    public function findOneBy(array $criteria, array $orderBy = null)
107
    {
108
        $models = $this->findBy($criteria, $orderBy, 1, 0);
109
110
        if ([] === $models) {
111
            $this->logger->notice(
112
                'model: row within table {table} with criteria {criteria} not found',
113
                [
114
                    'table' => $this->getTable(),
115
                    'criteria' => $criteria,
116
                    'orderBy' => $orderBy,
117
                    'limit' => 1,
118
                    'offset' => 0,
119
                ]
120
            );
121
122
            return null;
123
        }
124
125
        return reset($models);
126
    }
127
128
    /**
129
     * @param array $criteria
130
     *
131
     * @return ModelInterface[]|array
132
     */
133
    public function findBy(array $criteria, array $orderBy = null, int $limit = null, int $offset = null): array
134
    {
135
        $table = $this->getTable();
136
137
        $this->logger->info(
138
            'model: find rows within table {table} with criteria {criteria}',
139
            ['table' => $table, 'criteria' => $criteria, 'orderBy' => $orderBy, 'limit' => $limit, 'offset' => $offset]
140
        );
141
142
        $qb = $this->connection->createQueryBuilder()
143
            ->select('*')
144
            ->from($table)
145
            ->setFirstResult($offset)
146
            ->setMaxResults($limit)
147
        ;
148
149
        $this->addCriteriaToQueryBuilder($qb, $criteria);
150
        $this->addOrderByToQueryBuilder($qb, $orderBy);
151
152
        $rows = $qb->execute()->fetchAll(\PDO::FETCH_ASSOC);
153
154
        if ([] === $rows) {
155
            return [];
156
        }
157
158
        $models = [];
159
        foreach ($rows as $row) {
160
            $this->storageCache->set($row['id'], $row);
161
162
            $models[] = $this->fromPersistence($row);
163
        }
164
165
        return $models;
166
    }
167
168
    /**
169
     * @param QueryBuilder $qb
170
     * @param array        $criteria
171
     */
172
    protected function addCriteriaToQueryBuilder(QueryBuilder $qb, array $criteria)
173
    {
174
        foreach ($criteria as $field => $value) {
175
            $qb->andWhere($qb->expr()->eq($field, ':'.$field));
176
            $qb->setParameter($field, $value);
177
        }
178
    }
179
180
    /**
181
     * @param QueryBuilder $qb
182
     * @param array|null   $orderBy
183
     */
184
    protected function addOrderByToQueryBuilder(QueryBuilder $qb, array $orderBy = null)
185
    {
186
        if (null === $orderBy) {
187
            return;
188
        }
189
190
        foreach ($orderBy as $field => $direction) {
191
            $qb->addOrderBy($field, $direction);
192
        }
193
    }
194
195
    /**
196
     * @param ModelInterface $model
197
     *
198
     * @return RepositoryInterface
199
     */
200
    public function persist(ModelInterface $model): RepositoryInterface
201
    {
202
        $id = $model->getId();
203
        $row = $model->toPersistence();
204
205
        $this->connection->beginTransaction();
206
207
        /** @var ModelInterface[] $toPersistModels */
208
        $toPersistModels = [];
209
210
        /** @var ModelInterface[] $toRemoveRelatedModels */
211
        $toRemoveRelatedModels = [];
212
213
        /** @var ModelCollectionInterface[] $modelCollections */
214
        $modelCollections = [];
215
216
        foreach ($row as $field => $value) {
217
            if ($value instanceof ModelCollectionInterface) {
218
                $modelCollections[] = $value;
219
                unset($row[$field]);
220
            } else if ($value instanceof ModelReferenceInterface) {
221
                $initialModel = $value->getInitialModel();
222
                $model = $value->getModel();
223
224
                if (null !== $initialModel && (null === $model || $model->getId() !== $initialModel->getId())) {
225
                    $toRemoveRelatedModels[$initialModel->getId()] = $initialModel;
226
                }
227
228
                if (null !== $model) {
229
                    $this->persistRelatedModel($model);
230
                    $row[$field.'Id'] = $model->getId();
231
                } else {
232
                    $row[$field.'Id'] = null;
233
                }
234
235
                unset($row[$field]);
236
            }
237
        }
238
239
        if (null === $this->find($id)) {
240
            $this->insert($id, $row);
241
        } else {
242
            $this->update($id, $row);
243
        }
244
245
        foreach ($modelCollections as $modelCollection) {
246
            $initialModels = $modelCollection->getInitialModels();
247
            $models = $modelCollection->getModels();
248
            
249
            foreach ($initialModels as $initialModel) {
250
                $toRemoveRelatedModels[$initialModel->getId()] = $initialModel;
251
            }
252
253
            foreach ($models as $model) {
254
                if (isset($toRemoveRelatedModels[$model->getId()])) {
255
                    unset($toRemoveRelatedModels[$model->getId()]);
256
                }
257
                $toPersistModels[$model->getId()] = $model;
258
            }
259
        }
260
261
        $this->persistRelatedModels($toPersistModels);
262
        $this->removeRelatedModels($toRemoveRelatedModels);
263
264
        $this->storageCache->set($id, $row);
265
266
        $this->connection->commit();
267
268
        return $this;
269
    }
270
271
    /**
272
     * @param ModelInterface $model
273
     *
274
     * @return RepositoryInterface
275
     */
276
    public function remove(ModelInterface $model): RepositoryInterface
277
    {
278
        $id = $model->getId();
279
        $table = $this->getTable();
280
281
        if (null === $this->find($id)) {
282
            return $this;
283
        }
284
285
        $this->connection->beginTransaction();
286
287
        $this->logger->info(
288
            'model: remove row from table {table} with id {id}',
289
            ['table' => $table, 'id' => $id]
290
        );
291
292
        $row = $model->toPersistence();
293
294
        foreach ($row as $field => $value) {
295
            if ($value instanceof ModelCollectionInterface) {
296
                $this->removeRelatedModels($value->getInitialModels());
297
            } else if ($value instanceof ModelReferenceInterface) {
298
                if (null !== $initialModel = $value->getInitialModel()) {
299
                    $this->removeRelatedModel($initialModel);
300
                }
301
            }
302
        }
303
304
        $this->connection->delete($table, ['id' => $id]);
305
306
        $this->storageCache->remove($id);
307
308
        $this->connection->commit();
309
310
        return $this;
311
    }
312
313
    /**
314
     * @return RepositoryInterface
315
     */
316
    public function clear(): RepositoryInterface
317
    {
318
        $this->storageCache->clear();
319
320
        return $this;
321
    }
322
323
    /**
324
     * @param string $id
325
     * @param array  $row
326
     */
327 View Code Duplication
    protected function insert(string $id, array $row)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
328
    {
329
        $table = $this->getTable();
330
331
        $this->logger->info(
332
            'model: insert row into table {table} with id {id}',
333
            ['table' => $table, 'id' => $id]
334
        );
335
336
        $this->connection->insert($table, $row);
337
    }
338
339
    /**
340
     * @param string $id
341
     * @param array  $row
342
     */
343 View Code Duplication
    protected function update(string $id, array $row)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
344
    {
345
        $table = $this->getTable();
346
347
        $this->logger->info(
348
            'model: update row into table {table} with id {id}',
349
            ['table' => $table, 'id' => $id]
350
        );
351
352
        $this->connection->update($table, $row, ['id' => $id]);
353
    }
354
355
    /**
356
     * @param ModelInterface[]|array $toRemoveRelatedModels
357
     */
358
    private function persistRelatedModels(array $toRemoveRelatedModels)
359
    {
360
        foreach ($toRemoveRelatedModels as $toRemoveRelatedModel) {
361
            $this->persistRelatedModel($toRemoveRelatedModel);
362
        }
363
    }
364
365
    /**
366
     * @param ModelInterface $model
367
     */
368
    private function persistRelatedModel(ModelInterface $model)
369
    {
370
        $this->resolver->persist($model);
371
    }
372
373
    /**
374
     * @param ModelInterface[]|array $toRemoveRelatedModels
375
     */
376
    private function removeRelatedModels(array $toRemoveRelatedModels)
377
    {
378
        foreach ($toRemoveRelatedModels as $toRemoveRelatedModel) {
379
            $this->removeRelatedModel($toRemoveRelatedModel);
380
        }
381
    }
382
383
    /**
384
     * @param ModelInterface $model
385
     */
386
    private function removeRelatedModel(ModelInterface $model)
387
    {
388
        $this->resolver->remove($model);
389
    }
390
391
    /**
392
     * @param array $row
393
     *
394
     * @return ModelInterface
395
     */
396
    abstract protected function fromPersistence(array $row): ModelInterface;
397
398
    /**
399
     * @return string
400
     */
401
    abstract protected function getTable(): string;
402
}
403