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

AbstractDoctrineRepository   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 384
Duplicated Lines 5.73 %

Coupling/Cohesion

Components 1
Dependencies 11

Importance

Changes 0
Metric Value
wmc 43
lcom 1
cbo 11
dl 22
loc 384
rs 8.3157
c 0
b 0
f 0

17 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 11 1
B find() 0 36 4
A findOneBy() 0 21 2
B findBy() 0 34 3
A addCriteriaToQueryBuilder() 0 7 2
A addOrderByToQueryBuilder() 0 10 3
C persist() 0 70 13
B remove() 0 36 6
A clear() 0 6 1
A insert() 11 11 1
A update() 11 11 1
A persistRelatedModels() 0 6 2
A persistRelatedModel() 0 4 1
A removeRelatedModels() 0 6 2
A removeRelatedModel() 0 4 1
fromPersistence() 0 1 ?
getTable() 0 1 ?

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like AbstractDoctrineRepository often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AbstractDoctrineRepository, and based on these observations, apply Extract Interface, too.

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