Completed
Push — master ( 6ec6af...7fd515 )
by Jaap
14s
created

AbstractEntityService::getRepository()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
crap 1
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 105
    public function __construct(EntityRepositoryInterface $repository, $entityClassName)
65
    {
66 105
        $this->repository = $repository;
67 105
        $this->listeners = [];
68
69 105
        $this->getEvent()->setEntityClassName($entityClassName);
70 105
    }
71
72
    /**
73
     * Gets the repository that is used by the service.
74
     *
75
     * @return EntityRepositoryInterface
76
     */
77 90
    protected function getRepository()
78
    {
79 90
        return $this->repository;
80
    }
81
82
    /**
83
     * Get the pre initialized event object
84
     *
85
     * @return EntityEvent
86
     */
87 105
    protected function getEvent()
88
    {
89 105
        if (null === $this->event) {
90 105
            $this->event = $event = new EntityEvent;
91
92 105
            $event->setTarget($this);
93
        }
94
95 105
        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 60
    public function getEventManager()
105
    {
106 60
        if (null === $this->eventManager) {
107 51
            $this->setEventManager(new EventManager);
108
        }
109
110 60
        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 60
    public function setEventManager(EventManagerInterface $eventManager)
121
    {
122 60
        if ($this->eventManager === $eventManager || $eventManager === null) {
123 3
            return;
124
        }
125
126 60
        if ($this->eventManager !== null) {
127 3
            $this->detach($this->eventManager);
128
        }
129
130 60
        $this->eventManager = $eventManager;
131 60
        $this->eventManager->addIdentifiers([
132 60
            'EntityService',
133 60
            'PolderKnowledge\EntityService\Service\EntityService',
134 60
            $this->getEntityServiceName(),
135 60
            trim($this->getEntityServiceName(), '\\'),
136
        ]);
137
138 60
        $this->attach($this->eventManager);
139 60
    }
140
141
    /**
142
     * {@inheritDoc}
143
     */
144
    public function attach(EventManagerInterface $events, $priority = 1)
145
    {
146 60 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 36
            $repository = $event->getTarget()->getRepository();
148
149 36
            $event->setResult(call_user_func_array(
150 36
                [$repository, $event->getName()],
151 36
                $event->getParams()
152
            ));
153 60
        };
154
155 60
        $this->listeners[] = $events->attach('countBy', $callback, 0);
156 60
        $this->listeners[] = $events->attach('delete', $callback, 0);
157 60
        $this->listeners[] = $events->attach('deleteBy', $callback, 0);
158 60
        $this->listeners[] = $events->attach('find', $callback, 0);
159 60
        $this->listeners[] = $events->attach('findAll', $callback, 0);
160 60
        $this->listeners[] = $events->attach('findBy', $callback, 0);
161 60
        $this->listeners[] = $events->attach('findOneBy', $callback, 0);
162 60
        $this->listeners[] = $events->attach(
163 60
            'persist',
164 60 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 60
            },
173
            0
174
        );
175 60
        $this->listeners[] = $events->attach(
176 60
            'multiPersist',
177 60
            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 60
            },
190
            0
191
        );
192
193 60
        $this->listeners[] = $events->attach('*', function (EntityEvent $event) {
194 42
            $event->disableStoppingOfPropagation();
195 60
        }, -1);
196 60
    }
197
198
    /**
199
     * Returns the FQCN of the entity handled by this service.
200
     *
201
     * @return string
202
     */
203 96
    protected function getEntityServiceName()
204
    {
205 96
        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 array|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)
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 6
    public function findBy($criteria)
309
    {
310 6
        if (!$this->isRepositoryReadable()) {
311 3
            throw $this->createNotReadableException();
312
        }
313
314 3
        return $this->trigger(__FUNCTION__, [
315 3
            '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 45
    protected function trigger($name, array $params)
385
    {
386 45
        $event = clone $this->getEvent();
387 45
        $event->setName($name);
388 45
        $event->setParams($params);
389
390 45
        $responseCollection = $this->getEventManager()->triggerEvent($event);
391
392 45
        if ($responseCollection->stopped() && $event->isError()) {
393 3
            throw new RuntimeException($event->getError(), $event->getErrorNr());
394
        }
395
396 42
        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 48
    protected function isRepositoryReadable()
474
    {
475 48
        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