Completed
Push — develop ( 6e756f...bdea0a )
by
unknown
15:13 queued 07:47
created

canCreateServiceWithName()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
c 2
b 0
f 1
dl 0
loc 9
rs 9.6666
cc 1
eloc 2
nc 1
nop 3
1
<?php
2
/**
3
 * YAWIK
4
 *
5
 * @filesource
6
 * @license MIT
7
 * @copyright  2013 - 2016 Cross Solution <http://cross-solution.de>
8
 */
9
  
10
/** */
11
namespace Core\Factory\EventManager;
12
13
use Core\EventManager\EventProviderInterface;
14
use Zend\EventManager\ListenerAggregateInterface;
15
use Zend\ServiceManager\AbstractFactoryInterface;
16
use Zend\ServiceManager\ServiceLocatorInterface;
17
use Zend\Stdlib\ArrayUtils;
18
19
/**
20
 * Creates event manager instances.
21
 *
22
 * Optionally configures these instances with options set in array with the key "event_manager" in the main config
23
 * array. Also creates listeners which are preconfigured in the options and automatically attaches them.
24
 *
25
 * The options array:
26
 * [
27
 *      'event_manager' => [
28
 *          'Meaningful/Service.Name/Events' => [
29
 *              'service' => string: Service name or class name of the event manager to create.
30
 *              'event'   => string: Service name or class name of the event class to be used.
31
 *              'configure' => bool: Wether or not to configure the service manager through THIS factory.
32
 *              'identifiers' => array: list of identifiers for the event manager.
33
 *              'listeners' => array: preconfigured listeners which will ONLY be created, when the
34
 *                                    event manager is created. (lazy loading)
35
 *      ]
36
 * ]
37
 *
38
 * The listeners array:
39
 *
40
 * [
41
 *      string:listener => string:event, // creates the listener with the key as name (or class) and attaches it
42
 *                                       // to the event manager on the provided event.
43
 *
44
 *      // If you need more options or need to attach to multiple events, you can use following syntax: //
45
 *      string:listener => [ string|array:event{, string:methodName}{, int:priority}{, bool:lazy }],
46
 *                      // First string item or any array item is used as event(s)
47
 *                      // Any string item (if event is already set) and any following string items set (and override previous) method names
48
 *                      // (the method name is the name of the method to be called upon the listener when the event happens.)
49
 *                      // Any int item set and override previously set priority.
50
 *                      // Any boolean item set and override previously set lazy option.
51
 *                      // (lazy option does provide even more lazy loading. The listener is only created, if
52
 *                      //  the event it listens to is actually triggered. (accomplished by \Core\Listener\DeferredListenerAggregate)
53
 *
54
 *      string:aggregate, // Creates an ListenerAggregate and call its attach method with the instance of the event manager
55
 *      string:aggregate => int:priority // Same as above, but passes the priority value along.
56
 * ]
57
 *
58
 * If you need to attach more than one method or two events with different priorities you can do so using the
59
 * verbose style in the listener array:
60
 *
61
 * [
62
 *      string:listener => [
63
 *          'events' => [
64
 *              string:eventName,
65
 *              string:eventName => int:priority,
66
 *              string:eventName' => string:method,
67
 *              string:eventName => [
68
 *                  'method' => string:method,
69
 *                  'method' => [ string:method, string:method, ... ],
70
 *                  'method' => [ string:method => int:priority, string:method' => int:priority ],
71
 *                  'priority' => int:priority,
72
 *              ],
73
 *          ],
74
 *          'method' => string:defaultMethod,
75
 *          'priority' => int:defaultPriority,
76
 *          'lazy' => bool
77
 *      ],
78
 * ]
79
 *
80
 * Example:
81
 * Attach one listener to two methods on one event:
82
 * [
83
 *      'Listener' => [ 'events' => [ 'eventName' => [ 'method' => ['method1', 'method2'] ] ] ],
84
 * ]
85
 *
86
 * Use default priority on method1 and different priority on method2:
87
 * [
88
 *      'Listener' => [ 'events' => [ 'eventName' => [ 'method' => ['method1', 'method2' => 12] ] ] ], 'priority' => 5 ],
89
 * ]
90
 *
91
 * Attach one listener to two events with different priority:
92
 * [
93
 *      'Listener' => [ 'events' => [ 'event1' => 1, 'event2' => 2 ] ],
94
 * ]
95
 *
96
 * Each specific event -> method -> priority triple will be attached searately to the event manager using the
97
 * same listener instance.
98
 *
99
 * 
100
 * @author Mathias Gelhausen <[email protected]>
101
 * @since 0.25
102
 */
103
class EventManagerAbstractFactory implements AbstractFactoryInterface
104
{
105
    public function canCreateServiceWithName(ServiceLocatorInterface $serviceLocator, $name, $requestedName)
106
    {
107
        /* We check, if $requestedName ends with the string '/Events'.
108
         * Instead of parsing the string with regular expressions (eg. ~/Events$~),
109
         * it's more efficient to just check with strpos, if the reversed string starts
110
         * with the reverted '/Events' string.
111
         */
112
        return 0 === strpos(strrev($requestedName), 'stnevE/');
113
    }
114
115
    /**
116
     * Creates an event manager and attaches preconfigured listeners.
117
     *
118
     * @param ServiceLocatorInterface $serviceLocator
119
     * @param string                  $name
120
     * @param string                  $requestedName
121
     *
122
     * @return \Zend\EventManager\EventManagerInterface
123
     * @throws \UnexpectedValueException
124
     */
125
    public function createServiceWithName(ServiceLocatorInterface $serviceLocator, $name, $requestedName)
126
    {
127
        $config = $this->getConfig($serviceLocator, $requestedName);
128
        $events = $this->createEventManager($serviceLocator, $config);
129
130
        $this->attachListeners($serviceLocator, $events, $config['listeners']);
131
132
        return $events;
133
    }
134
135
    /**
136
     * Gets configuration for an event manager.
137
     *
138
     * Merges the default config with configuration from the main config,
139
     * if a key $name exists in the array under the "event_manager" array in the main config.
140
     *
141
     * @param ServiceLocatorInterface $services
142
     * @param string $name
143
     *
144
     * @return array
145
     */
146
    protected function getConfig($services, $name)
147
    {
148
        $defaults = [
149
            'service' => 'EventManager',
150
            'configure' => true,
151
            'identifiers' => [ $name ],
152
            'event' => '\Zend\EventManager\Event',
153
            'listeners' => [],
154
        ];
155
156
        $config = $services->get('Config');
157
        $config = isset($config['event_manager'][$name]) ? $config['event_manager'][$name] : [];
158
159
        /*
160
         * array_merge does not work, because the default values for 'identifiers' and 'listeners'
161
         * are arrays and array_merge breaks the structure.
162
         */
163
        $config = array_replace_recursive($defaults, $config);
164
165
        return $config;
166
    }
167
168
    /**
169
     * Creates an event manager instance.
170
     *
171
     * Fetches from the service manager or tries to instantiate direct, if no service
172
     * exists in the service manager.
173
     *
174
     * If the key 'configure' in the config array has the value TRUE (default),
175
     * the event manager instance will get configured. Which means, the event prototype
176
     * will be set (after it is fetched from the service manager or instatiated),
177
     * and the shared event manager will be injected.
178
     *
179
     * @param ServiceLocatorInterface $services
180
     * @param array $config
181
     *
182
     * @return \Zend\EventManager\EventManagerInterface
183
     * @throws \UnexpectedValueException if neither a service exists, nor could a class be found.
184
     */
185
    protected function createEventManager($services, $config)
186
    {
187
        /* @var \Zend\EventManager\EventManagerInterface|\Core\EventManager\EventProviderInterface $events */
188
189 View Code Duplication
        if ($services->has($config['service'])) {
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...
190
            $events = $services->get($config['service']);
191
192
        } else {
193
            if (!class_exists($config['service'], true)) {
194
                throw new \UnexpectedValueException(sprintf(
195
                    'Class or service %s does not exists. Cannot create event manager instance.', $config['service']
196
                ));
197
            }
198
199
            $events = new $config['service']();
200
        }
201
202
        if (false === $config['configure']) {
203
            return $events;
204
        }
205
206
        $events->setIdentifiers($config['identifiers']);
207
208
        if ($events instanceOf EventProviderInterface || method_exists($events, 'setEventPrototype')) {
209
            /* @var \Zend\EventManager\EventInterface $event */
210
            $event = $services->has($config['event']) ? $services->get($config['event']) : new $config['event']();
211
            $events->setEventPrototype($event);
212
        }
213
        else {
214
            $events->setEventClass($config['event']);
215
        }
216
217
        if ('EventManager' != $config['service'] && method_exists($events, 'setSharedManager') && $services->has('SharedEventManager')) {
218
            /* @var \Zend\EventManager\SharedEventManagerInterface $sharedEvents */
219
            $sharedEvents = $services->get('SharedEventManager');
220
            $events->setSharedManager($sharedEvents);
221
        }
222
223
        return $events;
224
    }
225
226
    /**
227
     * Attaches listeners provided in the config to the event manager instance.
228
     *
229
     * @param ServiceLocatorInterface $services
230
     * @param \Zend\EventManager\EventManagerInterface $eventManager
231
     * @param array $listeners
232
     *
233
     * @throws \UnexpectedValueException if a listener name cannot be fetched as service or be instantiated.
234
     */
235
    protected function attachListeners($services, $eventManager, $listeners)
236
    {
237
        $lazyListeners = [];
238
239
        foreach ($listeners as $name => $options) {
240
            $options = $this->normalizeListenerOptions($name, $options);
241
242
            if ($options['lazy'] && null !== $options['attach'] ) {
243
                foreach ($options['attach'] as $spec) {
244
                    $lazyListeners[] = [
245
                        'service' => $options['service'],
246
                        'event' => $spec['events'],
247
                        'method' => $spec['method'],
248
                        'priority' => $spec['priority'],
249
                    ];
250
                }
251
                continue;
252
            }
253
254 View Code Duplication
            if ($services->has($options['service'])) {
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...
255
                $listener = $services->get($options['service']);
256
257
            } else if (class_exists($options['service'], true)) {
258
                $listener = new $options['service']();
259
260
            } else {
261
                throw new \UnexpectedValueException(sprintf(
262
                                                        'Class or service %s does not exists. Cannot create listener instance.', $options['service']
263
                                                    ));
264
            }
265
266
            if ($listener instanceOf ListenerAggregateInterface) {
267
                $listener->attach($eventManager, $options['priority']);
0 ignored issues
show
Unused Code introduced by
The call to ListenerAggregateInterface::attach() has too many arguments starting with $options['priority'].

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
268
                continue;
269
            }
270
271
            foreach ($options['attach'] as $spec) {
272
                $callback = $spec['method'] ? [ $listener, $spec['method'] ] : $listener;
273
                $eventManager->attach($spec['events'], $callback, $spec['priority']);
274
            }
275
        }
276
277
        if (!empty($lazyListeners)) {
278
            /* @var \Core\Listener\DeferredListenerAggregate $aggregate */
279
            $aggregate = $services->get('Core/Listener/DeferredListenerAggregate');
280
            $aggregate->setListeners($lazyListeners)
281
                      ->attach($eventManager);
282
        }
283
    }
284
285
    /**
286
     * Normalizes the listener configuration.
287
     *
288
     * Converts the options given in the main config file to an array
289
     * containing key => value pairs for easier consumption in
290
     * {@link attachListeners()}
291
     *
292
     * @param int|string $name Service or class name of the listener. (if int, we have config for an aggregate)
293
     * @param string|array $options String is either event name or aggregate name (when name is int).
294
     *                              Array are the options from config. [ [event,..], method, priority, lazy]
295
     *
296
     * @return array
297
     */
298
    protected function normalizeListenerOptions($name, $options)
299
    {
300
301
        /*
0 ignored issues
show
Unused Code Comprehensibility introduced by
37% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
302
         * $options is an array with following meta-syntax:
303
         *
304
         *  $options = [
305
         *      string:listener => string:event,
306
         *      string:listener => [ string|array:event{, string:methodName}{, int:priority}{, bool:lazy }],
307
         *      string:aggregate, // implies integer value as $name
308
         *      string:aggregate => int:priority,
309
         *      string:listener => [
310
         *          'events' => [ 'event', 'event' => priority, 'event' => 'method',
311
         *                        'event' => [ 'method' => method, 'priority' => priority ],
312
         *                        'event' => [ 'method' => [ 'method', 'method' => priority ], 'priority' => priority ]
313
         *                     ],
314
         *          'method' => method,
315
         *          'priority' => priority,
316
         *          'lazy' => bool
317
         * ]
318
         */
319
320
        $normalized = [
321
            'service' => $name,
322
            'attach' => null,
323
            'priority' => 0,
324
            'lazy' => false,
325
        ];
326
327
        if (is_int($name)) {
328
            /* $options must be the name of an aggregate service or class. */
329
            $normalized['service'] = $options;
330
            return $normalized;
331
332
        }
333
334
        if (is_int($options)) {
335
            /* $name must be the name of an aggregate and the priority is passed. */
336
            $normalized['priority'] = $options;
337
            return $normalized;
338
339
        }
340
341
        if (is_string($options)) {
342
            /* Only an event name is provided in config */
343
            $normalized['attach'] = [ [ 'events' => [ $options ], 'method' => null, 'priority' => 0 ] ];
344
            return $normalized;
345
346
        }
347
348
        if (ArrayUtils::isHashTable($options)) {
349
            $normalized['attach'] = $this->normalizeEventsSpec($options);
350
351
            if (isset($options['lazy'])) {
352
                $normalized['lazy'] = $options['lazy'];
353
            }
354
355
            return $normalized;
356
357
        }
358
359
        $event = $method = null;
360
        $priority = 0;
361
        $lazy = false;
362
363
        foreach ($options as $opt) {
364
365
            if (is_array($opt)) {
366
                /* Must be event names */
367
                $event = $opt;
368
369
            } else if (is_string($opt)) {
370
                if (null === $event) {
371
                    /* first string found is assumed to be the event name */
372
                    $event = [ $opt ];
373
                } else {
374
                    /* second string found must be a method name. */
375
                    $method = $opt;
376
                }
377
378
            } else if (is_int($opt)) {
379
                /* Integer values must be priority */
380
                $priority = $opt;
381
382
            } else if (is_bool($opt)) {
383
                /* Lazy option is passed. */
384
                $lazy = $opt;
385
            }
386
        }
387
388
        $normalized['attach'] = [ [ 'events' => $event, 'method' => $method, 'priority' => $priority ] ];
389
        $normalized['lazy']   = $lazy;
390
391
        return $normalized;
392
    }
393
394
    protected function normalizeEventsSpec($options)
395
    {
396
        $listenerPriority = isset($options['priority']) ? $options['priority'] : 0;
397
        $listenerMethod   = isset($options['method'])   ? $options['method']   : '__none__';
398
        $events = [];
399
400
        foreach ($options['events'] as $event => $spec) {
401
402
403
            $eventPriority = isset($spec['priority']) ? $spec['priority'] : $listenerPriority;
404
405
            if (is_int($event)) {
406
                $events[$listenerMethod][$eventPriority][] = $spec;
407
408
            } else if (is_int($spec)) {
409
                $events[$listenerMethod][$spec][] = $event;
410
411
            } else if (is_string($spec)) {
412
                $events[$spec][$eventPriority][] = $event;
413
414
            } else if (is_array($spec)) {
415
                if (isset($spec['method'])) {
416
                    if (!is_array($spec['method'])) {
417
                        $spec['method'] = [ $spec['method'] ];
418
                    }
419
420
                    foreach ($spec['method'] as $method => $methodPriority) {
421
                        if (is_int($method)) {
422
                            $events[$methodPriority][$eventPriority][] = $event;
423
424
                        } else if (is_int($methodPriority)) {
425
                            $events[$method][$methodPriority][] = $event;
426
                        }
427
                    }
428
                }
429
            }
430
        }
431
432
        $eventsSpec = [];
433
        foreach ($events as $method => $priorities) {
434
            foreach ($priorities as $priority => $event) {
435
                $eventsSpec[] = [
436
                    'events' => $event,
437
                    'method' => '__none__' == $method ? null : $method,
438
                    'priority' => $priority,
439
                ];
440
            }
441
        }
442
443
        return $eventsSpec;
444
    }
445
}