Completed
Push — master ( b96d89...96506e )
by max
03:28
created

InMemoryRepository::remove()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 15
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 9
nc 2
nop 1
1
<?php
2
3
namespace T4webInfrastructure;
4
5
use ArrayObject;
6
use Zend\EventManager\EventManagerInterface;
7
use Zend\EventManager\Event;
8
use T4webDomainInterface\Infrastructure\CriteriaInterface;
9
use T4webDomainInterface\Infrastructure\RepositoryInterface;
10
use T4webDomainInterface\EntityInterface;
11
use T4webDomainInterface\EntityFactoryInterface;
12
use T4webInfrastructure\Event\EntityChangedEvent;
13
14
class InMemoryRepository implements RepositoryInterface
15
{
16
    /**
17
     * @var string
18
     */
19
    protected $entityName;
20
21
    /**
22
     * @var CriteriaFactory
23
     */
24
    protected $criteriaFactory;
25
26
    /**
27
     * @var EntityFactoryInterface
28
     */
29
    protected $entityFactory;
30
31
    /**
32
     * @var ArrayObject
33
     */
34
    protected $identityMap;
35
36
    /**
37
     * @var ArrayObject
38
     */
39
    protected $identityMapOriginal;
40
41
    /**
42
     * @var EventManagerInterface
43
     */
44
    protected $eventManager;
45
46
    /**
47
     * @var EntityChangedEvent
48
     */
49
    protected $event;
50
51
    protected $primaryKey = 1;
52
53
    /**
54
     * @param string $entityName
55
     * @param CriteriaFactory $criteriaFactory
56
     * @param EntityFactoryInterface $entityFactory
57
     * @param EventManagerInterface $eventManager
58
     */
59
    public function __construct(
60
        $entityName,
61
        CriteriaFactory $criteriaFactory,
62
        EntityFactoryInterface $entityFactory,
63
        EventManagerInterface $eventManager
64
    ) {
65
        $this->entityName = $entityName;
66
        $this->criteriaFactory = $criteriaFactory;
67
        $this->entityFactory = $entityFactory;
68
        $this->identityMap = new ArrayObject();
69
        $this->identityMapOriginal = new ArrayObject();
70
        $this->eventManager = $eventManager;
71
    }
72
73
    /**
74
     * @param EntityInterface $entity
75
     * @return EntityInterface|int|null
76
     */
77
    public function add(EntityInterface $entity)
78
    {
79
        $id = $entity->getId();
80
81
        if ($this->identityMap->offsetExists((int)$id)) {
82
            $e = $this->getEvent();
83
            $originalEntity = $this->identityMapOriginal->offsetGet($entity->getId());
84
            $e->setOriginalEntity($originalEntity);
85
            $e->setChangedEntity($entity);
86
87
            $this->triggerPreChanges($e);
88
89
            $this->toIdentityMap($entity);
90
91
            $this->triggerChanges($e);
92
            $this->triggerAttributesChange($e);
93
94
            return 1;
95
        } else {
96
            if (empty($id)) {
97
                $id = $this->primaryKey++;
98
                $entity->populate(compact('id'));
99
            }
100
101
            $this->toIdentityMap($entity);
102
103
            $this->triggerCreate($entity);
104
        }
105
106
        return $entity;
107
    }
108
109
    /**
110
     * @param EntityInterface $entity
111
     * @return int|null
112
     */
113
    public function remove(EntityInterface $entity)
114
    {
115
        $id = $entity->getId();
116
117
        if ($this->identityMap->offsetExists((int)$id)) {
118
            $this->identityMap->offsetUnset($id);
119
            $result = 1;
120
        } else {
121
            $result = 0;
122
        }
123
124
        $this->triggerDelete($entity);
125
126
        return $result;
127
    }
128
129
    /**
130
     * @param CriteriaInterface|array $criteria
131
     * @return EntityInterface|null
132
     */
133
    public function find($criteria)
134
    {
135
        if (is_array($criteria)) {
136
            $criteria = $this->createCriteria($criteria);
137
        }
138
139
        $callback = $criteria->getQuery();
140
141
        $result = null;
142
        /** @var EntityInterface $entity */
143
        foreach ($this->identityMap as $entity) {
144
            if ($callback($entity->extract())) {
145
                $result = $entity;
146
            }
147
        }
148
149
        if (is_null($result)) {
150
            return;
151
        }
152
153
        $entity = $result;
154
155
        $this->toIdentityMap($entity);
156
157
        return $entity;
158
    }
159
160
    /**
161
     * @param mixed $id
162
     * @return EntityInterface|null
163
     */
164
    public function findById($id)
165
    {
166
        $criteria = $this->createCriteria();
167
        $criteria->equalTo('id', $id);
168
169
        return $this->find($criteria);
170
    }
171
172
    /**
173
     * @param CriteriaInterface|array $criteria
174
     * @return EntityInterface[]
175
     */
176
    public function findMany($criteria)
177
    {
178
        if (is_array($criteria)) {
179
            $criteria = $this->createCriteria($criteria);
180
        }
181
182
        $callback = $criteria->getQuery();
183
184
        $entities = new ArrayObject();
185
        /** @var EntityInterface $entity */
186
        foreach ($this->identityMap as $entity) {
187
            if ($callback($entity->extract())) {
188
                $entities->offsetSet($entity->getId(), $entity);
189
            }
190
        }
191
192
        foreach ($entities as $entity) {
193
            $this->toIdentityMap($entity);
194
        }
195
196
        return $entities;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $entities; (ArrayObject) is incompatible with the return type declared by the interface T4webDomainInterface\Inf...toryInterface::findMany of type T4webDomainInterface\EntityInterface[].

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
197
    }
198
199
    /**
200
     * @param CriteriaInterface|array $criteria
201
     * @return int
202
     */
203
    public function count($criteria)
204
    {
205
        if (is_array($criteria)) {
206
            $criteria = $this->createCriteria($criteria);
207
        }
208
209
        /** @var ArrayObject $entities */
210
        $entities = $this->findMany($criteria);
211
212
        return $entities->count();
213
    }
214
215
    /**
216
     * @param array $filter
217
     * @return CriteriaInterface
218
     */
219
    public function createCriteria(array $filter = [])
220
    {
221
        return $this->criteriaFactory->buildInMemory($this->entityName, $filter);
222
    }
223
224
    /**
225
     * @param EntityInterface $entity
226
     */
227
    protected function toIdentityMap(EntityInterface $entity)
228
    {
229
        $this->identityMap->offsetSet($entity->getId(), $entity);
230
        $this->identityMapOriginal->offsetSet($entity->getId(), clone $entity);
231
    }
232
233
    /**
234
     * @param EntityInterface $changedEntity
235
     * @return bool
236
     */
237
    protected function isEntityChanged(EntityInterface $changedEntity)
238
    {
239
        $originalEntity = $this->identityMapOriginal->offsetGet($changedEntity->getId());
240
        return $changedEntity != $originalEntity;
241
    }
242
243
    /**
244
     * @return EntityChangedEvent
245
     */
246 View Code Duplication
    protected function getEvent()
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...
247
    {
248
        if (null === $this->event) {
249
            $this->event = new EntityChangedEvent();
250
            $this->event->setTarget($this);
251
        }
252
253
        return $this->event;
254
    }
255
256
    /**
257
     * @param EntityInterface $createdEntity
258
     */
259 View Code Duplication
    protected function triggerCreate(EntityInterface &$createdEntity)
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...
260
    {
261
        $this->eventManager->addIdentifiers(get_class($createdEntity));
262
263
        $event = new Event(
264
            sprintf('entity:%s:created', get_class($createdEntity)),
265
            $this,
266
            ['entity' => $createdEntity]
267
        );
268
        $this->eventManager->trigger($event);
269
270
        if ($event->getParam('entity') && $event->getParam('entity') instanceof EntityInterface) {
271
            $createdEntity = $event->getParam('entity');
272
        }
273
    }
274
275
    /**
276
     * @param EntityInterface $deletedEntity
277
     */
278 View Code Duplication
    protected function triggerDelete(EntityInterface $deletedEntity)
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...
279
    {
280
        $this->eventManager->addIdentifiers(get_class($deletedEntity));
281
282
        $event = new Event(
283
            sprintf('entity:%s:deleted', get_class($deletedEntity)),
284
            $this,
285
            ['entity' => $deletedEntity]
286
        );
287
        $this->eventManager->trigger($event);
288
    }
289
290
    /**
291
     * @param EntityChangedEvent $e
292
     */
293
    protected function triggerChanges(EntityChangedEvent $e)
294
    {
295
        $changedEntity = $e->getChangedEntity();
296
        $this->eventManager->trigger($this->getEntityChangeEventName($changedEntity), $this, $e);
297
    }
298
299
    /**
300
     * @param EntityChangedEvent $e
301
     */
302
    protected function triggerPreChanges(EntityChangedEvent $e)
303
    {
304
        $changedEntity = $e->getChangedEntity();
305
        $this->eventManager->trigger($this->getEntityChangeEventName($changedEntity).':pre', $this, $e);
306
    }
307
308
    /**
309
     * @param EntityChangedEvent $e
310
     */
311 View Code Duplication
    protected function triggerAttributesChange(EntityChangedEvent $e)
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...
312
    {
313
        $changedEntity = $e->getChangedEntity();
314
315
        $originalAttrs = $e->getOriginalEntity()->extract();
316
        $changedAttrs = $changedEntity->extract();
317
318
        foreach (array_keys(array_diff_assoc($originalAttrs, $changedAttrs)) as $attribute) {
319
            $this->eventManager->trigger($this->getAttributeChangeEventName($changedEntity, $attribute), $this, $e);
320
        }
321
    }
322
323
    /**
324
     * @param EntityInterface $changedEntity
325
     * @return string
326
     */
327
    protected function getEntityChangeEventName(EntityInterface $changedEntity)
328
    {
329
        return sprintf('entity:%s:changed', get_class($changedEntity));
330
    }
331
332
    /**
333
     * @param EntityInterface $changedEntity
334
     * @param $attributeName
335
     * @return string
336
     */
337
    protected function getAttributeChangeEventName(EntityInterface $changedEntity, $attributeName)
338
    {
339
        return sprintf('attribute:%s:%s:changed', get_class($changedEntity), $attributeName);
340
    }
341
}
342