Completed
Push — master ( 22ceeb...0c5df6 )
by Michael
09:51
created

AbstractContainerMediator   B

Complexity

Total Complexity 53

Size/Duplication

Total Lines 337
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 2

Test Coverage

Coverage 98.25%

Importance

Changes 0
Metric Value
wmc 53
lcom 2
cbo 2
dl 0
loc 337
ccs 112
cts 114
cp 0.9825
rs 7.4757
c 0
b 0
f 0

15 Methods

Rating   Name   Duplication   Size   Complexity  
A addServiceListenersByEventList() 0 5 1
A addServiceListener() 0 14 4
A addServiceSubscriber() 0 4 1
A getListeners() 0 11 4
getServiceByName() 0 1 ?
A getServiceListeners() 0 8 3
D removeServiceListener() 0 36 9
A removeServiceListenersByEventList() 0 5 1
A removeServiceSubscriber() 0 4 1
setServiceContainer() 0 1 ?
B getActualPriority() 0 19 8
A bubbleUpUnsetServiceListener() 0 14 3
C checkAllowedServiceListener() 0 27 8
B lazyLoadServices() 0 21 5
B sortServiceListeners() 0 19 5

How to fix   Complexity   

Complex Class

Complex classes like AbstractContainerMediator often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AbstractContainerMediator, and based on these observations, apply Extract Interface, too.

1
<?php
2
declare(strict_types = 1);
3
/**
4
 * Contains AbstractContainerMediator class.
5
 *
6
 * PHP version 7.0
7
 *
8
 * LICENSE:
9
 * This file is part of Event Mediator - A general event mediator (dispatcher)
10
 * which has minimal dependencies so it is easy to drop in and use.
11
 * Copyright (C) 2015-2016 Michael Cummings
12
 *
13
 * This program is free software; you can redistribute it and/or modify it
14
 * under the terms of the GNU General Public License as published by the Free
15
 * Software Foundation; version 2 of the License.
16
 *
17
 * This program is distributed in the hope that it will be useful, but WITHOUT
18
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
19
 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
20
 * details.
21
 *
22
 * You should have received a copy of the GNU General Public License along with
23
 * this program; if not, you may write to
24
 *
25
 * Free Software Foundation, Inc.
26
 * 59 Temple Place, Suite 330
27
 * Boston, MA 02111-1307 USA
28
 *
29
 * or find a electronic copy at
30
 * <http://spdx.org/licenses/GPL-2.0.html>.
31
 *
32
 * You should also be able to find a copy of this license in the included
33
 * LICENSE file.
34
 *
35
 * @author    Michael Cummings <[email protected]>
36
 * @copyright 2015-2016 Michael Cummings
37
 * @license   GPL-2.0
38
 */
39
namespace EventMediator;
40
41
/**
42
 * Class AbstractContainerMediator
43
 */
44
abstract class AbstractContainerMediator extends Mediator implements ContainerMediatorInterface
45
{
46
    /**
47
     * Add a service as an event listener.
48
     *
49
     * @param string     $eventName Name of the event the listener is being added for.
50
     * @param array      $listener  Listener to be added. ['containerID', 'method']
51
     * @param int|string $priority  Priority level for the added listener.
52
     *
53
     * @return ContainerMediatorInterface Fluent interface.
54
     * @throws \DomainException
55
     * @throws \InvalidArgumentException
56
     */
57 19
    public function addServiceListener(string $eventName, array $listener, $priority = 0): ContainerMediatorInterface
58
    {
59 19
        $this->checkEventName($eventName);
60 18
        $this->checkAllowedServiceListener($listener);
61 13
        $priority = $this->getActualPriority($eventName, $priority);
62 12
        if (array_key_exists($eventName, $this->serviceListeners)
63 12
            && array_key_exists($priority, $this->serviceListeners[$eventName])
64 12
            && in_array($listener, $this->serviceListeners[$eventName][$priority], true)
65
        ) {
66 1
            return $this;
67
        }
68 12
        $this->serviceListeners[$eventName][$priority][] = $listener;
69 12
        return $this;
70
    }
71
    /**
72
     * @param array $events
73
     *
74
     * @return ContainerMediatorInterface
75
     * @throws \DomainException
76
     * @throws \InvalidArgumentException
77
     * @throws \LengthException
78
     */
79 6
    public function addServiceListenersByEventList(array $events): ContainerMediatorInterface
80
    {
81 6
        $this->walkEventList($events, [$this, 'addServiceListener']);
82 6
        return $this;
83
    }
84
    /**
85
     * Add a service as a subscriber to event(s).
86
     *
87
     * @param ServiceSubscriberInterface $sub Service subscriber to be added.
88
     *
89
     * @return ContainerMediatorInterface Fluent interface.
90
     * @throws \DomainException
91
     * @throws \InvalidArgumentException
92
     * @throws \LengthException
93
     */
94 6
    public function addServiceSubscriber(ServiceSubscriberInterface $sub): ContainerMediatorInterface
95
    {
96 6
        return $this->addServiceListenersByEventList($sub->getServiceSubscribedEvents());
97
    }
98
    /**
99
     * @param string $eventName
100
     *
101
     * @return array
102
     * @throws \DomainException
103
     * @throws \InvalidArgumentException
104
     */
105 5
    public function getListeners(string $eventName = ''): array
106
    {
107 5
        if (0 !== count($this->serviceListeners)) {
108 3
            if ('' === $eventName) {
109
                $this->lazyLoadServices(array_keys($this->serviceListeners));
110 3
            } elseif (array_key_exists($eventName, $this->serviceListeners)) {
111 3
                $this->lazyLoadServices([$eventName]);
112
            }
113
        }
114 5
        return parent::getListeners($eventName);
115
    }
116
    /** @noinspection GenericObjectTypeUsageInspection */
117
    /**
118
     * This method is used any time the mediator need to get the actual instance
119
     * of the class for an event.
120
     *
121
     * Normal will only be called during actual trigger of an event since lazy
122
     * loading is used.
123
     *
124
     * @param string $serviceName
125
     *
126
     * @return object
127
     */
128
    abstract public function getServiceByName(string $serviceName);
129
    /**
130
     * Get a list of service listeners for an event.
131
     *
132
     * Note that if event name is empty all listeners will be returned. Any event subscribers are also included in the
133
     * list.
134
     *
135
     * @param string $eventName Name of the event the list of service listeners is needed for.
136
     *
137
     * @return array List of event service listeners or empty array if event is unknown or has no listeners or
138
     *               subscribers.
139
     * @throws \InvalidArgumentException
140
     */
141 9
    public function getServiceListeners(string $eventName = ''): array
142
    {
143 9
        $this->sortServiceListeners($eventName);
144 9
        if ('' !== $eventName) {
145 3
            return array_key_exists($eventName, $this->serviceListeners) ? $this->serviceListeners[$eventName] : [];
146
        }
147 7
        return $this->serviceListeners;
148
    }
149
    /**
150
     * Remove a service as an event listener.
151
     *
152
     * @param string     $eventName Event name that listener is being removed from.
153
     * @param array      $listener  Service listener to be removed.
154
     * @param int|string $priority  Priority level for the to be removed listener.
155
     *
156
     * @return ContainerMediatorInterface Fluent interface.
157
     * @throws \DomainException
158
     * @throws \InvalidArgumentException
159
     */
160 7
    public function removeServiceListener(string $eventName, array $listener, $priority = 0): ContainerMediatorInterface
161
    {
162 7
        $this->checkEventName($eventName);
163 6
        if (!array_key_exists($eventName, $this->serviceListeners)) {
164 1
            return $this;
165
        }
166 5
        $this->checkAllowedServiceListener($listener);
167 4
        if (in_array($eventName, $this->loadedServices, true)) {
168 1
            $this->removeListener($eventName, [$this->getServiceByName($listener[0]), $listener[1]], $priority);
169
        }
170
        /**
171
         * @var array      $priorities
172
         * @var int|string $atPriority
173
         * @var array      $listeners
174
         */
175 4
        if ('last' !== $priority) {
176 4
            $priorities = $this->serviceListeners[$eventName];
177
        } else {
178 1
            $priorities = array_reverse($this->serviceListeners[$eventName], true);
179 1
            $priority = 'first';
180
        }
181 4
        $isIntPriority = is_int($priority);
182 4
        foreach ($priorities as $atPriority => $listeners) {
183 4
            if ($isIntPriority && $priority !== $atPriority) {
184
                continue;
185
            }
186 4
            $key = array_search($listener, $listeners, true);
187 4
            if (false !== $key) {
188 4
                $this->bubbleUpUnsetServiceListener($eventName, $atPriority, $key);
189 4
                if ('first' === $priority) {
190 4
                    break;
191
                }
192
            }
193
        }
194 4
        return $this;
195
    }
196
    /**
197
     * @param array $events
198
     *
199
     * @return ContainerMediatorInterface
200
     * @throws \DomainException
201
     * @throws \InvalidArgumentException
202
     * @throws \LengthException
203
     */
204 3
    public function removeServiceListenersByEventList(array $events): ContainerMediatorInterface
205
    {
206 3
        $this->walkEventList($events, [$this, 'removeServiceListener']);
207 3
        return $this;
208
    }
209
    /**
210
     * Remove a service subscriber from event(s).
211
     *
212
     * @param ServiceSubscriberInterface $sub Subscriber to be removed.
213
     *
214
     * @return ContainerMediatorInterface Fluent interface.
215
     * @throws \DomainException
216
     * @throws \InvalidArgumentException
217
     * @throws \LengthException
218
     */
219 3
    public function removeServiceSubscriber(ServiceSubscriberInterface $sub): ContainerMediatorInterface
220
    {
221 3
        return $this->removeServiceListenersByEventList($sub->getServiceSubscribedEvents());
222
    }
223
    /**
224
     * This is used to bring in the service container that will be used.
225
     *
226
     * Though not required it would be considered best practice for this method
227
     * to create a new instance of the container when given null. Another good
228
     * practice is to call this method from the class constructor to allow
229
     * easier testing. For examples of both have a look at
230
     * PimpleContainerMediator.
231
     *
232
     * @see PimpleContainerMediator Container mediator implemented using Pimple.
233
     *
234
     * @param mixed $value The service container to be used.
235
     *
236
     * @return ContainerMediatorInterface Fluent interface.
237
     */
238
    abstract public function setServiceContainer($value = null): ContainerMediatorInterface;
239
    /**
240
     * @param string     $eventName
241
     * @param string|int $priority
242
     *
243
     * @return int
244
     * @throws \InvalidArgumentException
245
     */
246 15
    protected function getActualPriority(string $eventName, $priority): int
247
    {
248 15
        if (is_int($priority)) {
249 14
            return $priority;
250
        }
251 4
        if (!in_array($priority, ['first', 'last'], true)) {
252 1
            $mess = 'Unknown priority was given only "first", "last", or integer may be used';
253 1
            throw new \InvalidArgumentException($mess);
254
        }
255 3
        $listenerM = parent::getActualPriority($eventName, $priority);
256 3
        if ($priority === 'first') {
257 2
            $serviceM = array_key_exists($eventName, $this->serviceListeners)
258 2
                ? max(array_keys($this->serviceListeners[$eventName])) + 1 : 1;
259 2
            return ($listenerM > $serviceM) ? $listenerM : $serviceM;
260
        }
261 1
        $serviceM = array_key_exists($eventName, $this->serviceListeners)
262 1
            ? min(array_keys($this->serviceListeners[$eventName])) - 1 : -1;
263 1
        return ($listenerM < $serviceM) ? $listenerM : $serviceM;
264
    }
265
    /**
266
     * @param string $eventName
267
     * @param int    $priority
268
     * @param int    $key
269
     */
270 4
    private function bubbleUpUnsetServiceListener(string $eventName, int $priority, int $key)
271
    {
272 4
        unset($this->serviceListeners[$eventName][$priority][$key]);
273
        // Remove empty priorities.
274 4
        if (0 === count($this->serviceListeners[$eventName][$priority])) {
275 4
            unset($this->serviceListeners[$eventName][$priority]);
276
        }
277
        // Remove empty events.
278 4
        if (0 === count($this->serviceListeners[$eventName])) {
279 4
            unset($this->serviceListeners[$eventName]);
280 4
            $key = array_search($eventName, $this->loadedServices, true);
281 4
            unset($this->loadedServices[$key]);
282
        }
283
    }
284
    /**
285
     * @param $listener
286
     *
287
     * @throws \InvalidArgumentException
288
     */
289 18
    private function checkAllowedServiceListener($listener)
290
    {
291 18
        if (is_array($listener) && 2 === count($listener)) {
292 17
            list($containerID, $method) = $listener;
293 17
            if (!is_string($method)) {
294 2
                $mess = sprintf('Service listener method name MUST be a string, but was given %s', gettype($method));
295 2
                throw new \InvalidArgumentException($mess);
296
            }
297 16
            if (!is_string($containerID)) {
298 1
                $mess = sprintf('Service listener container ID MUST be a string, but was given %s',
299
                    gettype($containerID));
300 1
                throw new \InvalidArgumentException($mess);
301
            }
302 15
            if (!ctype_print($method) || false === preg_match('%\w{1,}%', $method)) {
303 1
                $mess = 'Service listener method name format is invalid, was given ' . $method;
304 1
                throw new \InvalidArgumentException($mess);
305
            }
306
            // Also catches empty string.
307 14
            if (!ctype_print($containerID)) {
308 1
                $mess = 'Using any non-printable characters in the container ID is NOT allowed';
309 1
                throw new \InvalidArgumentException($mess);
310
            }
311 13
            return;
312
        }
313 1
        $mess = 'Service listener form MUST be ["containerID", "methodName"]';
314 1
        throw new \InvalidArgumentException($mess);
315
    }
316
    /**
317
     * @param array $eventNames
318
     *
319
     * @return ContainerMediatorInterface Fluent interface
320
     * @throws \DomainException
321
     * @throws \InvalidArgumentException
322
     */
323 3
    private function lazyLoadServices(array $eventNames): ContainerMediatorInterface
324
    {
325 3
        foreach ($eventNames as $event) {
326 3
            if (!in_array($event, $this->loadedServices, true)) {
327 3
                $this->loadedServices[] = $event;
328
            }
329
            /**
330
             * @var array $priorities
331
             * @var int   $priority
332
             * @var array $listeners
333
             * @var array $listener
334
             */
335 3
            $priorities = $this->serviceListeners[$event];
336 3
            foreach ($priorities as $priority => $listeners) {
337 3
                foreach ($listeners as $listener) {
338 3
                    $this->addListener($event, [$this->getServiceByName($listener[0]), $listener[1]], $priority);
339
                }
340
            }
341
        }
342 3
        return $this;
343
    }
344
    /**
345
     * @param string $eventName
346
     *
347
     * @return ContainerMediatorInterface Fluent Interface
348
     * @throws \InvalidArgumentException
349
     */
350 9
    private function sortServiceListeners(string $eventName): ContainerMediatorInterface
351
    {
352 9
        if (0 === count($this->serviceListeners)) {
353 4
            return $this;
354
        }
355 8
        if ('' !== $eventName) {
356 3
            if (!array_key_exists($eventName, $this->serviceListeners)) {
357 2
                return $this;
358
            }
359 2
            $eventNames = [$eventName];
360
        } else {
361 6
            ksort($this->serviceListeners);
362 6
            $eventNames = array_keys(parent::getListeners(''));
363
        }
364 7
        foreach ($eventNames as $anEvent) {
365 7
            krsort($this->serviceListeners[$anEvent], SORT_NUMERIC);
366
        }
367 7
        return $this;
368
    }
369
    /**
370
     * List of already loaded services.
371
     *
372
     * @var string[] $loadedServices
373
     */
374
    private $loadedServices = [];
375
    /**
376
     * @var array $serviceListeners Holds the list of service listeners that will be lazy loaded when events are
377
     * triggered.
378
     */
379
    private $serviceListeners = [];
380
}
381