Completed
Push — master ( 679558...de1094 )
by Dominik
01:55
created

AbstractDoctrineRepository   B

Complexity

Total Complexity 46

Size/Duplication

Total Lines 410
Duplicated Lines 5.37 %

Coupling/Cohesion

Components 1
Dependencies 12

Importance

Changes 0
Metric Value
wmc 46
lcom 1
cbo 12
dl 22
loc 410
rs 8.3999
c 0
b 0
f 0

19 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
B persist() 0 42 6
B remove() 0 40 6
A clear() 0 6 1
A insert() 11 11 1
A update() 11 11 1
B callbackIfReference() 0 21 5
B persistModelReference() 0 17 5
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
        if (!$alreadyExists = (bool) $this->find($id)) {
208
            $alreadyExists = $this->callbackIfReference($id, $row, function (string $id, array $row) {
209
                $this->insert($id, $row);
210
            });
211
        }
212
213
        $stack = new RelatedModelManipulationStack();
214
215
        foreach ($row as $field => $value) {
216
            if ($value instanceof ModelCollectionInterface) {
217
                $stack->addToRemoveModels($value->getInitialModels());
218
                $stack->addToPersistModels($value->getModels());
219
                unset($row[$field]);
220
            } else if ($value instanceof ModelReferenceInterface) {
221
                $row[$field.'Id'] = $this->persistModelReference($value, $stack);
222
223
                unset($row[$field]);
224
            }
225
        }
226
227
        if (!$alreadyExists) {
228
            $this->insert($id, $row);
229
        } else {
230
            $this->update($id, $row);
231
        }
232
233
        $this->persistRelatedModels($stack->getToPersistModels());
234
        $this->removeRelatedModels($stack->getToRemoveModels());
235
236
        $this->storageCache->set($id, $row);
237
238
        $this->connection->commit();
239
240
        return $this;
241
    }
242
243
    /**
244
     * @param ModelInterface $model
245
     *
246
     * @return RepositoryInterface
247
     */
248
    public function remove(ModelInterface $model): RepositoryInterface
249
    {
250
        $id = $model->getId();
251
        $table = $this->getTable();
252
253
        if (null === $this->find($id)) {
254
            return $this;
255
        }
256
257
        $this->connection->beginTransaction();
258
259
        $row = $model->toPersistence();
260
261
        $this->callbackIfReference($id, $row, function (string $id, array $row) {
262
            $this->update($id, $row);
263
        });
264
265
        foreach ($row as $field => $value) {
266
            if ($value instanceof ModelCollectionInterface) {
267
                $this->removeRelatedModels($value->getInitialModels());
268
            } else if ($value instanceof ModelReferenceInterface) {
269
                if (null !== $initialModel = $value->getInitialModel()) {
270
                    $this->removeRelatedModel($initialModel);
271
                }
272
            }
273
        }
274
275
        $this->logger->info(
276
            'model: remove row from table {table} with id {id}',
277
            ['table' => $table, 'id' => $id]
278
        );
279
280
        $this->connection->delete($table, ['id' => $id]);
281
282
        $this->storageCache->remove($id);
283
284
        $this->connection->commit();
285
286
        return $this;
287
    }
288
289
    /**
290
     * @return RepositoryInterface
291
     */
292
    public function clear(): RepositoryInterface
293
    {
294
        $this->storageCache->clear();
295
296
        return $this;
297
    }
298
299
    /**
300
     * @param string $id
301
     * @param array  $row
302
     */
303 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...
304
    {
305
        $table = $this->getTable();
306
307
        $this->logger->info(
308
            'model: insert row into table {table} with id {id}',
309
            ['table' => $table, 'id' => $id]
310
        );
311
312
        $this->connection->insert($table, $row);
313
    }
314
315
    /**
316
     * @param string $id
317
     * @param array  $row
318
     */
319 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...
320
    {
321
        $table = $this->getTable();
322
323
        $this->logger->info(
324
            'model: update row from table {table} with id {id}',
325
            ['table' => $table, 'id' => $id]
326
        );
327
328
        $this->connection->update($table, $row, ['id' => $id]);
329
    }
330
331
    /**
332
     * @param string $id
333
     * @param array $row
334
     * @return bool
335
     */
336
    private function callbackIfReference(string $id, array $row, \Closure $callback): bool
337
    {
338
        $gotReference = false;
339
        foreach ($row as $field => $value) {
340
            if ($value instanceof ModelCollectionInterface) {
341
                unset($row[$field]);
342
            } else if ($value instanceof ModelReferenceInterface) {
343
                $row[$field.'Id'] = null;
344
                $gotReference = true;
345
                unset($row[$field]);
346
            }
347
        }
348
349
        if ($gotReference) {
350
            $callback($id, $row);
351
352
            return true;
353
        }
354
355
        return false;
356
    }
357
358
    /**
359
     * @param ModelReferenceInterface $reference
360
     * @param RelatedModelManipulationStack $stack
361
     * @return null|string
362
     */
363
    private function persistModelReference(ModelReferenceInterface $reference, RelatedModelManipulationStack $stack)
364
    {
365
        $initialModel = $reference->getInitialModel();
366
        $model = $reference->getModel();
367
368
        if (null !== $initialModel && (null === $model || $model->getId() !== $initialModel->getId())) {
369
            $stack->addToRemoveModel($initialModel);
370
        }
371
372
        if (null !== $model) {
373
            $this->persistRelatedModel($model);
374
375
            return $model->getId();
376
        }
377
378
        return null;
379
    }
380
381
    /**
382
     * @param ModelInterface[]|array $toRemoveModels
383
     */
384
    private function persistRelatedModels(array $toRemoveModels)
385
    {
386
        foreach ($toRemoveModels as $toRemoveRelatedModel) {
387
            $this->persistRelatedModel($toRemoveRelatedModel);
388
        }
389
    }
390
391
    /**
392
     * @param ModelInterface $model
393
     */
394
    private function persistRelatedModel(ModelInterface $model)
395
    {
396
        $this->resolver->persist($model);
397
    }
398
399
    /**
400
     * @param ModelInterface[]|array $toRemoveModels
401
     */
402
    private function removeRelatedModels(array $toRemoveModels)
403
    {
404
        foreach ($toRemoveModels as $toRemoveRelatedModel) {
405
            $this->removeRelatedModel($toRemoveRelatedModel);
406
        }
407
    }
408
409
    /**
410
     * @param ModelInterface $model
411
     */
412
    private function removeRelatedModel(ModelInterface $model)
413
    {
414
        $this->resolver->remove($model);
415
    }
416
417
    /**
418
     * @param array $row
419
     *
420
     * @return ModelInterface
421
     */
422
    abstract protected function fromPersistence(array $row): ModelInterface;
423
424
    /**
425
     * @return string
426
     */
427
    abstract protected function getTable(): string;
428
}
429