Completed
Push — master ( 448920...85583a )
by Dominik
01:59
created

AbstractDoctrineRepository::persistRelatedModel()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 9
Ratio 100 %

Importance

Changes 0
Metric Value
dl 9
loc 9
rs 9.6666
c 0
b 0
f 0
cc 2
eloc 5
nc 2
nop 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Chubbyphp\Model\Doctrine\DBAL;
6
7
use Chubbyphp\Model\Cache\ModelCache;
8
use Chubbyphp\Model\Cache\ModelCacheInterface;
9
use Chubbyphp\Model\Collection\ModelCollectionInterface;
10
use Chubbyphp\Model\ModelInterface;
11
use Chubbyphp\Model\RepositoryInterface;
12
use Doctrine\DBAL\Connection;
13
use Doctrine\DBAL\Query\QueryBuilder;
14
use Psr\Log\LoggerInterface;
15
use Psr\Log\NullLogger;
16
17
abstract class AbstractDoctrineRepository implements RepositoryInterface
18
{
19
    /**
20
     * @var Connection
21
     */
22
    protected $connection;
23
24
    /**
25
     * @var RepositoryInterface[]
26
     */
27
    protected $relatedRepositories;
28
29
    /**
30
     * @var ModelCacheInterface
31
     */
32
    protected $cache;
33
34
    /**
35
     * @var LoggerInterface
36
     */
37
    protected $logger;
38
39
    /**
40
     * @param Connection               $connection
41
     * @param array                    $relatedRepositories
42
     * @param ModelCacheInterface|null $cache
43
     * @param LoggerInterface|null     $logger
44
     */
45
    public function __construct(
46
        Connection $connection,
47
        array $relatedRepositories = [],
48
        ModelCacheInterface $cache = null,
49
        LoggerInterface $logger = null
50
    ) {
51
        $this->connection = $connection;
52
        $this->relatedRepositories = $relatedRepositories;
53
        $this->cache = $cache ?? new ModelCache();
54
        $this->logger = $logger ?? new NullLogger();
55
    }
56
57
    /**
58
     * @param string $id
59
     *
60
     * @return ModelInterface|null
61
     */
62
    public function find(string $id)
63
    {
64
        $modelClass = $this->getModelClass();
65
66
        $this->logger->info('model: find model {model} with id {id}', ['model' => $modelClass, 'id' => $id]);
67
68
        if ($this->cache->has($id)) {
69
            return $this->fromPersistence($this->cache->get($id));
70
        }
71
72
        $qb = $this->connection->createQueryBuilder();
73
        $qb->select('*')->from($this->getTable())->where($qb->expr()->eq('id', ':id'))->setParameter('id', $id);
74
75
        $row = $qb->execute()->fetch(\PDO::FETCH_ASSOC);
76 View Code Duplication
        if (false === $row) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
77
            $this->logger->warning(
78
                'model: model {model} with id {id} not found',
79
                ['model' => $modelClass, 'id' => $id]
80
            );
81
82
            return null;
83
        }
84
85
        $this->cache->set($row['id'], $row);
86
87
        return $this->fromPersistence($row);
88
    }
89
90
    /**
91
     * @param array $criteria
92
     *
93
     * @return null|ModelInterface
94
     */
95
    public function findOneBy(array $criteria)
96
    {
97
        $modelClass = $this->getModelClass();
98
99
        $this->logger->info(
100
            'model: find model {model} with criteria {criteria}',
101
            ['model' => $modelClass, 'criteria' => $criteria]
102
        );
103
104
        $qb = $this->getFindByQueryBuilder($criteria)->setMaxResults(1);
105
106
        $row = $qb->execute()->fetch(\PDO::FETCH_ASSOC);
107 View Code Duplication
        if (false === $row) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
108
            $this->logger->warning(
109
                'model: model {model} with criteria {criteria} not found',
110
                ['model' => $modelClass, 'criteria' => $criteria]
111
            );
112
113
            return null;
114
        }
115
116
        $this->cache->set($row['id'], $row);
117
118
        return $this->fromPersistence($row);
119
    }
120
121
    /**
122
     * @param array $criteria
123
     *
124
     * @return ModelInterface[]|array
125
     */
126
    public function findBy(array $criteria, array $orderBy = null, int $limit = null, int $offset = null): array
127
    {
128
        $modelClass = $this->getModelClass();
129
130
        $this->logger->info(
131
            'model: find model {model} with criteria {criteria}',
132
            ['model' => $modelClass, 'criteria' => $criteria]
133
        );
134
135
        $qb = $this
136
            ->getFindByQueryBuilder($criteria)
137
            ->setFirstResult($offset)
138
            ->setMaxResults($limit)
139
        ;
140
141
        if (null !== $orderBy) {
142
            foreach ($orderBy as $field => $direction) {
143
                $qb->addOrderBy($field, $direction);
144
            }
145
        }
146
147
        $rows = $qb->execute()->fetchAll(\PDO::FETCH_ASSOC);
148
149
        if ([] === $rows) {
150
            return [];
151
        }
152
153
        $models = [];
154
        foreach ($rows as $row) {
155
            $this->cache->set($row['id'], $row);
156
            $models[] = $this->fromPersistence($row);
157
        }
158
159
        return $models;
160
    }
161
162
    /**
163
     * @param array $criteria
164
     *
165
     * @return QueryBuilder
166
     */
167
    private function getFindByQueryBuilder(array $criteria = []): QueryBuilder
168
    {
169
        $qb = $this->connection->createQueryBuilder();
170
        $qb->select('*')->from($this->getTable());
171
172
        foreach ($criteria as $field => $value) {
173
            $qb->andWhere($qb->expr()->eq($field, ':'.$field));
174
            $qb->setParameter($field, $value);
175
        }
176
177
        return $qb;
178
    }
179
180
    /**
181
     * @param ModelInterface $model
182
     */
183
    public function persist(ModelInterface $model)
184
    {
185
        $this->logger->info(
186
            'model: persist model {model} with id {id}',
187
            ['model' => get_class($model), 'id' => $model->getId()]
188
        );
189
190
        $row = $model->toPersistence();
191
        foreach ($row as $field => $value) {
192
            if ($value instanceof ModelCollectionInterface) {
193
                $this->persistRelatedModels($value);
194
                unset($row[$field]);
195
            } elseif ($value instanceof ModelInterface) {
196
                $this->persistRelatedModel($value);
197
                unset($row[$field]);
198
            }
199
        }
200
201
        if (null === $this->find($model->getId())) {
202
            $this->connection->insert($this->getTable(), $row);
203
        } else {
204
            $this->connection->update($this->getTable(), $row, ['id' => $model->getId()]);
205
        }
206
207
        $this->cache->set($model->getId(), $row);
208
    }
209
210
    /**
211
     * @param ModelCollectionInterface $modelCollection
212
     */
213
    private function persistRelatedModels(ModelCollectionInterface $modelCollection)
214
    {
215
        $initialModels = $modelCollection->getInitialModels();
216
        $models = $modelCollection->getModels();
217
        foreach ($models as $model) {
218
            $this->persistRelatedModel($model);
219
            if (isset($initialModels[$model->getId()])) {
220
                unset($initialModels[$model->getId()]);
221
            }
222
        }
223
224
        foreach ($initialModels as $initialModel) {
225
            $this->removeRelatedModel($initialModel);
226
        }
227
    }
228
229
    /**
230
     * @param ModelInterface $model
231
     */
232 View Code Duplication
    private function persistRelatedModel(ModelInterface $model)
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...
233
    {
234
        $modelClass = get_class($model);
235
        if (!isset($this->relatedRepositories[$modelClass])) {
236
            throw MissingRelatedRepositoryException::create($modelClass);
237
        }
238
239
        $this->relatedRepositories[$modelClass]->persist($model);
240
    }
241
242
    /**
243
     * @param ModelInterface $model
244
     */
245
    public function remove(ModelInterface $model)
246
    {
247
        $this->logger->info(
248
            'model: remove model {model} with id {id}',
249
            ['model' => get_class($model), 'id' => $model->getId()]
250
        );
251
252
        $row = $model->toPersistence();
253
        foreach ($row as $field => $value) {
254
            if ($value instanceof ModelCollectionInterface) {
255
                $this->removeRelatedModels($value);
256
            } elseif ($value instanceof ModelInterface) {
257
                $this->removeRelatedModel($value);
258
            }
259
        }
260
261
        $this->connection->delete($this->getTable(), ['id' => $model->getId()]);
262
263
        $this->cache->remove($model->getId());
264
    }
265
266
    /**
267
     * @paramModelCollectionInterface $modelCollection
268
     */
269
    private function removeRelatedModels(ModelCollectionInterface $modelCollection)
270
    {
271
        foreach ($modelCollection->getInitialModels() as $initialModel) {
272
            $this->removeRelatedModel($initialModel);
273
        }
274
    }
275
276
    /**
277
     * @param ModelInterface $model
278
     */
279 View Code Duplication
    private function removeRelatedModel(ModelInterface $model)
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...
280
    {
281
        $modelClass = get_class($model);
282
        if (!isset($this->relatedRepositories[$modelClass])) {
283
            throw MissingRelatedRepositoryException::create($modelClass);
284
        }
285
286
        $this->relatedRepositories[$modelClass]->remove($model);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Chubbyphp\Model\RepositoryInterface as the method remove() does only exist in the following implementations of said interface: Chubbyphp\Model\Doctrine...tractDoctrineRepository.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
287
    }
288
289
    /**
290
     * @param array $row
291
     *
292
     * @return ModelInterface
293
     */
294
    protected function fromPersistence(array $row): ModelInterface
295
    {
296
        /** @var ModelInterface $modelClass */
297
        $modelClass = $this->getModelClass();
298
299
        return $modelClass::fromPersistence($row);
300
    }
301
302
    /**
303
     * @return string
304
     */
305
    abstract protected function getTable(): string;
306
}
307