Passed
Push — master ( 0d80ff...1482f0 )
by Dominik
04:16
created

AbstractDoctrineRepository::removeRelatedModel()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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