Test Failed
Push — master ( 2d6bce...bcfc8e )
by Dominik
03:31
created

AbstractDoctrineRepository   B

Complexity

Total Complexity 46

Size/Duplication

Total Lines 409
Duplicated Lines 5.38 %

Coupling/Cohesion

Components 1
Dependencies 12

Test Coverage

Coverage 98.8%

Importance

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