Completed
Push — master ( 85ff48...477c1e )
by Dominik
01:54
created

persistModelReference()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 5

Importance

Changes 0
Metric Value
dl 0
loc 17
ccs 9
cts 9
cp 1
rs 8.8571
c 0
b 0
f 0
cc 5
eloc 9
nc 4
nop 2
crap 5
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 2
                $stack->addToRemoveModels($value->getInitialModels());
215 2
                $stack->addToPersistModels($value->getModels());
216 2
                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 2
        $this->removeRelatedModels($stack->getToRemoveModels());
231 2
        $this->persistRelatedModels($stack->getToPersistModels());
232
233 2
        $this->storageCache->set($id, $row);
234
235 2
        $this->connection->commit();
236
237 2
        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
        foreach ($row as $field => $value) {
259 1
            if ($value instanceof ModelCollectionInterface) {
260 1
                $this->removeRelatedModels($value->getInitialModels());
261 1
            } elseif ($value instanceof ModelReferenceInterface) {
262 1
                if (null !== $initialModel = $value->getInitialModel()) {
263 1
                    $this->removeRelatedModel($initialModel);
264
                }
265
            }
266
        }
267
268 1
        $this->logger->info(
269 1
            'model: remove row from table {table} with id {id}',
270 1
            ['table' => $table, 'id' => $id]
271
        );
272
273 1
        $this->connection->delete($table, ['id' => $id]);
274
275 1
        $this->storageCache->remove($id);
276
277 1
        $this->connection->commit();
278
279 1
        return $this;
280
    }
281
282
    /**
283
     * @return RepositoryInterface
284
     */
285 1
    public function clear(): RepositoryInterface
286
    {
287 1
        $this->storageCache->clear();
288
289 1
        return $this;
290
    }
291
292
    /**
293
     * @param string $id
294
     * @param array  $row
295
     */
296 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...
297
    {
298 1
        $table = $this->getTable();
299
300 1
        $this->logger->info(
301 1
            'model: insert row into table {table} with id {id}',
302 1
            ['table' => $table, 'id' => $id]
303
        );
304
305 1
        $this->connection->insert($table, $row);
306 1
    }
307
308
    /**
309
     * @param string $id
310
     * @param array  $row
311
     */
312 1 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...
313
    {
314 1
        $table = $this->getTable();
315
316 1
        $this->logger->info(
317 1
            'model: update row from table {table} with id {id}',
318 1
            ['table' => $table, 'id' => $id]
319
        );
320
321 1
        $this->connection->update($table, $row, ['id' => $id]);
322 1
    }
323
324
    /**
325
     * @param ModelReferenceInterface       $reference
326
     * @param RelatedModelManipulationStack $stack
327
     *
328
     * @return null|string
329
     */
330 2
    private function persistModelReference(ModelReferenceInterface $reference, RelatedModelManipulationStack $stack)
331
    {
332 2
        $initialModel = $reference->getInitialModel();
333 2
        $model = $reference->getModel();
334
335 2
        if (null !== $initialModel && (null === $model || $model->getId() !== $initialModel->getId())) {
336 1
            $stack->addToRemoveModel($initialModel);
337
        }
338
339 2
        if (null !== $model) {
340 1
            $this->persistRelatedModel($model);
341
342 1
            return $model->getId();
343
        }
344
345 1
        return null;
346
    }
347
348
    /**
349
     * @param ModelInterface[]|array $toRemoveModels
350
     */
351 2
    private function persistRelatedModels(array $toRemoveModels)
352
    {
353 2
        foreach ($toRemoveModels as $toRemoveRelatedModel) {
354 1
            $this->persistRelatedModel($toRemoveRelatedModel);
355
        }
356 2
    }
357
358
    /**
359
     * @param ModelInterface $model
360
     */
361 2
    private function persistRelatedModel(ModelInterface $model)
362
    {
363 2
        $this->resolver->persist($model);
364 2
    }
365
366
    /**
367
     * @param ModelInterface[]|array $toRemoveModels
368
     */
369 2
    private function removeRelatedModels(array $toRemoveModels)
370
    {
371 2
        foreach ($toRemoveModels as $toRemoveRelatedModel) {
372 2
            $this->removeRelatedModel($toRemoveRelatedModel);
373
        }
374 2
    }
375
376
    /**
377
     * @param ModelInterface $model
378
     */
379 2
    private function removeRelatedModel(ModelInterface $model)
380
    {
381 2
        $this->resolver->remove($model);
382 2
    }
383
384
    /**
385
     * @param array $row
386
     *
387
     * @return ModelInterface
388
     */
389
    abstract protected function fromPersistence(array $row): ModelInterface;
390
391
    /**
392
     * @return string
393
     */
394
    abstract protected function getTable(): string;
395
}
396