Completed
Pull Request — master (#6)
by
unknown
02:11
created

AbstractEntityService::setEventManager()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 20
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 20
ccs 13
cts 13
cp 1
rs 9.2
c 0
b 0
f 0
cc 4
eloc 12
nc 3
nop 1
crap 4
1
<?php
2
/**
3
 * Polder Knowledge / entityservice (https://polderknowledge.com)
4
 *
5
 * @link https://github.com/polderknowledge/entityservice for the canonical source repository
6
 * @copyright Copyright (c) 2016 Polder Knowledge (https://polderknowledge.com)
7
 * @license https://github.com/polderknowledge/entityservice/blob/master/LICENSE.md MIT
8
 */
9
10
namespace PolderKnowledge\EntityService;
11
12
use Doctrine\Common\Collections\Collection;
13
use Doctrine\Common\Collections\Criteria;
14
use PolderKnowledge\EntityService\Event\EntityEvent;
15
use PolderKnowledge\EntityService\Exception\RuntimeException;
16
use PolderKnowledge\EntityService\Exception\ServiceException;
17
use PolderKnowledge\EntityService\Feature\TransactionAwareInterface;
18
use PolderKnowledge\EntityService\Repository\EntityRepositoryInterface;
19
use PolderKnowledge\EntityService\Repository\Feature\DeletableInterface;
20
use PolderKnowledge\EntityService\Repository\Feature\FlushableInterface;
21
use PolderKnowledge\EntityService\Repository\Feature\ReadableInterface;
22
use PolderKnowledge\EntityService\Repository\Feature\WritableInterface;
23
use Traversable;
24
use Zend\EventManager\AbstractListenerAggregate;
25
use Zend\EventManager\EventManager;
26
use Zend\EventManager\EventManagerInterface;
27
28
/**
29
 * Base class for application specific EntityServices. This is a fully event driven class.
30
 * Each method trigger a method for extendability.
31
 */
32
abstract class AbstractEntityService extends AbstractListenerAggregate implements
33
    EntityServiceInterface,
34
    TransactionAwareInterface
35
{
36
    /**
37
     * The repository that is used for this entity service.
38
     *
39
     * @var EntityRepositoryInterface
40
     */
41
    private $repository;
42
43
    /**
44
     * EventManager handling all events triggered by this service
45
     *
46
     * @var EventManagerInterface
47
     */
48
    private $eventManager;
49
50
    /**
51
     * Initialized Event
52
     *
53
     * @var EntityEvent
54
     */
55
    private $event;
56
57
    /**
58
     * Initializes a new instance of this class.
59
     *
60
     * @param EntityRepositoryInterface $repository The repository that is used to communicate with.
61
     * @param string $entityClassName The FQCN of the entity.
62
     * @throws \PolderKnowledge\EntityService\Exception\RuntimeException
63
     */
64 108
    public function __construct(EntityRepositoryInterface $repository, $entityClassName)
65
    {
66 108
        $this->repository = $repository;
67 108
        $this->listeners = [];
68
69 108
        $this->getEvent()->setEntityClassName($entityClassName);
70 108
    }
71
72
    /**
73
     * Gets the repository that is used by the service.
74
     *
75
     * @return EntityRepositoryInterface
76
     */
77 93
    protected function getRepository()
78
    {
79 93
        return $this->repository;
80
    }
81
82
    /**
83
     * Get the pre initialized event object
84
     *
85
     * @return EntityEvent
86
     */
87 108
    protected function getEvent()
88
    {
89 108
        if (null === $this->event) {
90 108
            $this->event = $event = new EntityEvent;
91
92 108
            $event->setTarget($this);
93
        }
94
95 108
        return $this->event;
96
    }
97
98
    /**
99
     * Will create an EventManager when no EventManager was provided.
100
     * The returned EventManager is used to handle events triggered by this service instance.
101
     *
102
     * @return EventManagerInterface
103
     */
104 63
    public function getEventManager()
105
    {
106 63
        if (null === $this->eventManager) {
107 54
            $this->setEventManager(new EventManager);
108
        }
109
110 63
        return $this->eventManager;
111
    }
112
113
    /**
114
     * Set the EventManager used by this service instance to handle its events.
115
     * It will take care of disabling the old EventManager and will subscribe the internal
116
     * listeners to the new EventManager
117
     *
118
     * @param EventManagerInterface $eventManager
119
     */
120 63
    public function setEventManager(EventManagerInterface $eventManager)
121
    {
122 63
        if ($this->eventManager === $eventManager || $eventManager === null) {
123 3
            return;
124
        }
125
126 63
        if ($this->eventManager !== null) {
127 3
            $this->detach($this->eventManager);
128
        }
129
130 63
        $this->eventManager = $eventManager;
131 63
        $this->eventManager->addIdentifiers([
132 63
            'EntityService',
133 63
            'PolderKnowledge\EntityService\Service\EntityService',
134 63
            $this->getEntityServiceName(),
135 63
            trim($this->getEntityServiceName(), '\\'),
136
        ]);
137
138 63
        $this->attach($this->eventManager);
139 63
    }
140
141
    /**
142
     * {@inheritDoc}
143
     */
144
    public function attach(EventManagerInterface $events, $priority = 1)
145
    {
146 63 View Code Duplication
        $callback = function (EntityEvent $event) {
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...
147 39
            $repository = $event->getTarget()->getRepository();
148
149 39
            $event->setResult(call_user_func_array(
150 39
                [$repository, $event->getName()],
151 39
                $event->getParams()
152
            ));
153 63
        };
154
155 63
        $this->listeners[] = $events->attach('countBy', $callback, 0);
156 63
        $this->listeners[] = $events->attach('delete', $callback, 0);
157 63
        $this->listeners[] = $events->attach('deleteBy', $callback, 0);
158 63
        $this->listeners[] = $events->attach('find', $callback, 0);
159 63
        $this->listeners[] = $events->attach('findAll', $callback, 0);
160 63
        $this->listeners[] = $events->attach('findBy', $callback, 0);
161 63
        $this->listeners[] = $events->attach('findOneBy', $callback, 0);
162 63
        $this->listeners[] = $events->attach(
163 63
            'persist',
164 63 View Code Duplication
            function (EntityEvent $event) {
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...
165 15
                $repository = $event->getTarget()->getRepository();
166
167 15
                call_user_func_array([$repository, 'persist'], $event->getParams());
168
169 15
                if ($repository instanceof FlushableInterface) {
170 3
                    $repository->flush();
171
                }
172 63
            },
173
            0
174
        );
175 63
        $this->listeners[] = $events->attach(
176 63
            'multiPersist',
177 63
            function (EntityEvent $event) {
178 3
                $repository = $event->getTarget()->getRepository();
179
180 3
                $entities = current($event->getParams());
181
182 3
                foreach ($entities as $entity) {
183 3
                    call_user_func_array([$repository, 'persist'], [$entity]);
184
                }
185
186 3
                if ($repository instanceof FlushableInterface) {
187 3
                    $repository->flush();
188
                }
189 63
            },
190
            0
191
        );
192
193 63
        $this->listeners[] = $events->attach('*', function (EntityEvent $event) {
194 45
            $event->disableStoppingOfPropagation();
195 63
        }, -1);
196 63
    }
197
198
    /**
199
     * Returns the FQCN of the entity handled by this service.
200
     *
201
     * @return string
202
     */
203 99
    protected function getEntityServiceName()
204
    {
205 99
        return $this->getEvent()->getEntityClassName();
206
    }
207
208
    /**
209
     * Deletes the given object from the repository
210
     *
211
     * @param object $entity The entity to delete.
212
     * @return mixed
213
     * @throws \Zend\EventManager\Exception\InvalidArgumentException
214
     * @throws \PolderKnowledge\EntityService\Exception\RuntimeException
215
     */
216 6
    public function delete($entity)
217
    {
218 6
        if (!$this->isRepositoryDeletable()) {
219 3
            throw $this->createNotDeletableException();
220
        }
221
222 3
        return $this->trigger(__FUNCTION__, [
223 3
            'entity' => $entity,
224
        ]);
225
    }
226
227
    /**
228
     * Deletes all objects matching the criteria from the repository
229
     *
230
     * @param Criteria $criteria The criteria values to match on.
231
     * @return mixed
232
     * @throws \Zend\EventManager\Exception\InvalidArgumentException
233
     * @throws \PolderKnowledge\EntityService\Exception\RuntimeException
234
     */
235 6
    public function deleteBy(Criteria $criteria)
236
    {
237 6
        if (!$this->isRepositoryDeletable()) {
238 3
            throw $this->createNotDeletableException();
239
        }
240
241 3
        return $this->trigger(__FUNCTION__, [
242 3
            'criteria' => $criteria,
243
        ]);
244
    }
245
246
    /**
247
     * Count the objects matching the criteria respecting the order, limit and offset.
248
     *
249
     * @param array|Criteria $criteria The criteria values to match on.
250
     * @return int
251
     * @throws \Zend\EventManager\Exception\InvalidArgumentException
252
     * @throws \PolderKnowledge\EntityService\Exception\RuntimeException
253
     */
254 12
    public function countBy($criteria)
255
    {
256 12
        if (!$this->isRepositoryReadable()) {
257 3
            throw $this->createNotReadableException();
258
        }
259
260 9
        return $this->trigger(__FUNCTION__, [
261 9
            'criteria' => $criteria,
262
        ]);
263
    }
264
265
    /**
266
     * Find one object in the repository matching the $id
267
     *
268
     * @param mixed $id The id of the entity.
269
     * @return object|null
270
     * @throws \Zend\EventManager\Exception\InvalidArgumentException
271
     * @throws \PolderKnowledge\EntityService\Exception\RuntimeException
272
     */
273 12
    public function find($id)
274
    {
275 12
        if (!$this->isRepositoryReadable()) {
276 3
            throw $this->createNotReadableException();
277
        }
278
279 9
        return $this->trigger(__FUNCTION__, [
280 9
            'id' => $id,
281
        ]);
282
    }
283
284
    /**
285
     * Finds all entities in the repository.
286
     *
287
     * @return array Returns the entities that exist.
288
     * @throws \Zend\EventManager\Exception\InvalidArgumentException
289
     * @throws \PolderKnowledge\EntityService\Exception\RuntimeException
290
     */
291 15
    public function findAll()
292
    {
293 15
        if (!$this->isRepositoryReadable()) {
294 3
            throw $this->createNotReadableException();
295
        }
296
297 12
        return $this->trigger(__FUNCTION__, []);
298
    }
299
300
    /**
301
     * Find one or more objects in the repository matching the criteria respecting the order, limit and offset
302
     *
303
     * @param array|Criteria $criteria The array with criteria to search on.
304
     * @return array
305
     * @throws \Zend\EventManager\Exception\InvalidArgumentException
306
     * @throws \PolderKnowledge\EntityService\Exception\RuntimeException
307
     */
308 9
    public function findBy($criteria)
309
    {
310 9
        if (!$this->isRepositoryReadable()) {
311 3
            throw $this->createNotReadableException();
312
        }
313
314 6
        return $this->trigger(__FUNCTION__, [
315 6
            'criteria' => $criteria,
316
        ]);
317
    }
318
319
    /**
320
     * Find one object in the repository matching the criteria
321
     *
322
     * @param array|Criteria $criteria The criteria values to match on.
323
     * @return object|null
324
     * @throws \Zend\EventManager\Exception\InvalidArgumentException
325
     * @throws \PolderKnowledge\EntityService\Exception\RuntimeException
326
     */
327 6
    public function findOneBy($criteria)
328
    {
329 6
        if (!$this->isRepositoryReadable()) {
330 3
            throw $this->createNotReadableException();
331
        }
332
333 3
        return $this->trigger(__FUNCTION__, [
334 3
            'criteria' => $criteria,
335
        ]);
336
    }
337
338
    /**
339
     * Persist the given entity
340
     *
341
     * @param object $entity
342
     * @return mixed
343
     * @throws \Zend\EventManager\Exception\InvalidArgumentException
344
     * @throws RuntimeException
345
     */
346 18
    public function persist($entity)
347
    {
348 18
        if (!$this->isRepositoryWritable()) {
349 3
            throw $this->createNotWritableException();
350
        }
351
352 15
        return $this->trigger(__FUNCTION__, [
353 15
            'entity' => $entity,
354
        ]);
355
    }
356
357
    /**
358
     * Persist the given object and flushes it to the storage device.
359
     *
360
     * @param array|Collection|Traversable $entities The entities to persist.
361
     * @return mixed
362
     * @throws RuntimeException
363
     */
364 6
    public function multiPersist($entities)
365
    {
366 6
        if (!$this->isRepositoryWritable()) {
367 3
            throw $this->createNotWritableException();
368
        }
369
370 3
        return $this->trigger(__FUNCTION__, [
371 3
            'entities' => $entities,
372
        ]);
373
    }
374
375
    /**
376
     * will prepare the event object and trigger the event using the internal EventManager
377
     *
378
     * @param  string $name
379
     * @param  array $params
380
     * @return mixed
381
     * @throws \Zend\EventManager\Exception\InvalidArgumentException
382
     * @throws \PolderKnowledge\EntityService\Exception\RuntimeException
383
     */
384 48
    protected function trigger($name, array $params)
385
    {
386 48
        $event = clone $this->getEvent();
387 48
        $event->setName($name);
388 48
        $event->setParams($params);
389
390 48
        $responseCollection = $this->getEventManager()->triggerEvent($event);
391
392 48
        if ($responseCollection->stopped() && $event->isError()) {
393 3
            throw new RuntimeException($event->getError(), $event->getErrorNr());
394
        }
395
396 45
        return $event->getResult();
397
    }
398
399
    /**
400
     * Starts a new transaction.
401
     *
402
     * @throws ServiceException
403
     */
404 6
    public function beginTransaction()
405
    {
406 6
        if ($this->isTransactionEnabled() === false) {
407 3
            throw new ServiceException(sprintf(
408 3
                'The repository for %s doesn\'t support Transactions',
409 3
                $this->getEntityServiceName()
410
            ));
411
        }
412
413 3
        $this->getRepository()->beginTransaction();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface PolderKnowledge\EntitySe...tityRepositoryInterface as the method beginTransaction() does only exist in the following implementations of said interface: PolderKnowledge\EntitySe...\Doctrine\ORMRepository.

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...
414 3
    }
415
416
    /**
417
     * Commits a started transaction.
418
     *
419
     * @throws ServiceException
420
     */
421 6
    public function commitTransaction()
422
    {
423 6
        if ($this->isTransactionEnabled() === false) {
424 3
            throw new ServiceException(sprintf(
425 3
                'The repository for %s doesn\'t support Transactions',
426 3
                $this->getEntityServiceName()
427
            ));
428
        }
429
430 3
        $this->getRepository()->commitTransaction();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface PolderKnowledge\EntitySe...tityRepositoryInterface as the method commitTransaction() does only exist in the following implementations of said interface: PolderKnowledge\EntitySe...\Doctrine\ORMRepository.

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...
431 3
    }
432
433
    /**
434
     * Rolls back a started transaction.
435
     *
436
     * @throws ServiceException
437
     */
438 6
    public function rollbackTransaction()
439
    {
440 6
        if ($this->isTransactionEnabled() === false) {
441 3
            throw new ServiceException(sprintf(
442 3
                'The repository for %s doesn\'t support Transactions',
443 3
                $this->getEntityServiceName()
444
            ));
445
        }
446
447 3
        $this->getRepository()->rollbackTransaction();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface PolderKnowledge\EntitySe...tityRepositoryInterface as the method rollbackTransaction() does only exist in the following implementations of said interface: PolderKnowledge\EntitySe...\Doctrine\ORMRepository.

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...
448 3
    }
449
450
    /**
451
     * Returns true when possible to start an transaction
452
     */
453 18
    public function isTransactionEnabled()
454
    {
455 18
        return $this->getRepository() instanceof TransactionAwareInterface;
456
    }
457
458
    /**
459
     * Returns true when the repository for $entityName is writable
460
     *
461
     * @return bool
462
     */
463 24
    protected function isRepositoryWritable()
464
    {
465 24
        return $this->getRepository() instanceof WritableInterface;
466
    }
467
468
    /**
469
     * Returns true when the repository for $entityName is readable
470
     *
471
     * @return bool
472
     */
473 51
    protected function isRepositoryReadable()
474
    {
475 51
        return $this->getRepository() instanceof ReadableInterface;
476
    }
477
478
    /**
479
     * Returns true when the repository for $entityName has delete behavior
480
     *
481
     * @return bool
482
     */
483 12
    protected function isRepositoryDeletable()
484
    {
485 12
        return $this->getRepository() instanceof DeletableInterface;
486
    }
487
488
    /**
489
     * Throws an exception for cases where it's not possible to delete from the repository.
490
     *
491
     * @throws RuntimeException
492
     */
493 6
    private function createNotDeletableException()
494
    {
495 6
        throw new RuntimeException(sprintf(
496 6
            'The entities of type "%s" cannot be deleted from its repository.',
497 6
            $this->getEntityServiceName()
498
        ));
499
    }
500
501
    /**
502
     * Throws an exception for cases where it's not possible to read from the repository.
503
     *
504
     * @throws RuntimeException
505
     */
506 15
    private function createNotReadableException()
507
    {
508 15
        throw new RuntimeException(sprintf(
509 15
            'It is not possible to read entities of type "%s" from its repository.',
510 15
            $this->getEntityServiceName()
511
        ));
512
    }
513
514
    /**
515
     * Throws an exception for cases where it's not possible to write to the repository.
516
     *
517
     * @throws RuntimeException
518
     */
519 6
    private function createNotWritableException()
520
    {
521 6
        throw new RuntimeException(sprintf(
522 6
            'The entities of type "%s" cannot be written to its repository.',
523 6
            $this->getEntityServiceName()
524
        ));
525
    }
526
}
527