Completed
Push — master ( cbc691...72a5fc )
by Eric
02:18
created

Jarvis::broadcast()   C

Complexity

Conditions 8
Paths 8

Size

Total Lines 26
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
c 2
b 0
f 1
dl 0
loc 26
rs 5.3846
cc 8
eloc 14
nc 8
nop 2
1
<?php
0 ignored issues
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 30 and the first side effect is on line 3.

The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.

The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.

To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.

Loading history...
2
3
declare(strict_types = 1);
4
5
namespace Jarvis;
6
7
use FastRoute\Dispatcher;
8
use Jarvis\Skill\DependencyInjection\{
9
    Container,
10
    ContainerProvider,
11
    ContainerProviderInterface
12
};
13
use Jarvis\Skill\EventBroadcaster\{
14
    AnalyzeEvent,
15
    ControllerEvent,
16
    EventInterface,
17
    ExceptionEvent,
18
    JarvisEvents,
19
    PermanentEventInterface,
20
    ResponseEvent,
21
    SimpleEvent
22
};
23
use Symfony\Component\HttpFoundation\{ParameterBag, Request, Response};
24
25
/**
26
 * Jarvis. Minimalist dependency injection container.
27
 *
28
 * @author Eric Chau <[email protected]>
29
 */
30
class Jarvis extends Container
31
{
32
    const DEFAULT_DEBUG = false;
33
    const CONTAINER_PROVIDER_FQCN = ContainerProvider::class;
34
    const DEFAULT_SCOPE = 'default';
35
36
    const RECEIVER_HIGH_PRIORITY = 2;
37
    const RECEIVER_NORMAL_PRIORITY = 1;
38
    const RECEIVER_LOW_PRIORITY = 0;
39
40
    private $receivers = [];
41
    private $permanentEvents = [];
42
    private $computedReceivers = [];
43
    private $masterEmitter = false;
44
    private $masterSet = false;
45
46
    /**
47
     * Creates an instance of Jarvis. It can take settings as first argument.
48
     * List of accepted options:
49
     *   - container_provider (type: string|array): fqcn of your container provider
50
     *
51
     * @param  array $settings Your own settings to modify Jarvis behavior
52
     */
53
    public function __construct(array $settings = [])
54
    {
55
        parent::__construct();
56
57
        $this['settings'] = new ParameterBag($settings);
58
        $this->lock('settings');
59
60
        $this['debug'] = $this->settings->getBoolean('debug', static::DEFAULT_DEBUG);
61
        $this->lock('debug');
62
63
        if (!$this->settings->has('container_provider')) {
64
            $this->settings->set('container_provider', [static::CONTAINER_PROVIDER_FQCN]);
65
        } else {
66
            $containerProvider = (array) $this->settings->get('container_provider');
67
            array_unshift($containerProvider, static::CONTAINER_PROVIDER_FQCN);
68
            $this->settings->set('container_provider', $containerProvider);
69
        }
70
71
        foreach ($this->settings->get('container_provider') as $classname) {
72
            $this->hydrate(new $classname());
73
        }
74
    }
75
76
    public function __destruct()
77
    {
78
        $this->masterBroadcast(JarvisEvents::TERMINATE_EVENT);
79
    }
80
81
    /**
82
     * This method is an another way to get a locked value.
83
     *
84
     * Example: $this['foo'] is equal to $this->foo, but it ONLY works for locked values.
85
     *
86
     * @param  string $key The key of the locked value
87
     * @return mixed
88
     * @throws \InvalidArgumentException if the requested key is not associated to a locked service
89
     */
90
    public function __get(string $key)
91
    {
92
        if (!isset($this->locked[$key])) {
93
            throw new \InvalidArgumentException(sprintf('"%s" is not a key of a locked value.', $key));
94
        }
95
96
        $this->masterSetter($key, $this[$key]);
97
98
        return $this->$key;
99
    }
100
101
    /**
102
     * Sets new attributes to Jarvis. Note that this method is reserved to Jarvis itself only.
103
     *
104
     * @param string $key   The key name of the new attribute
105
     * @param mixed  $value The value to associate to provided key
106
     * @throws \LogicException if this method is not called by Jarvis itself
107
     */
108
    public function __set(string $key, $value)
109
    {
110
        if (false === $this->masterSet) {
111
            throw new \LogicException('You are not allowed to set new attribute into Jarvis.');
112
        }
113
114
        $this->$key = $value;
115
    }
116
117
    /**
118
     * @param  Request|null $request
119
     * @return Response
120
     */
121
    public function analyze(Request $request = null) : Response
122
    {
123
        $request = $request ?? $this->request;
0 ignored issues
show
Documentation introduced by
The property request does not exist on object<Jarvis\Jarvis>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
124
        $response = null;
125
126
        try {
127
            $this->masterBroadcast(JarvisEvents::ANALYZE_EVENT, $analyzeEvent = new AnalyzeEvent($request));
128
129
            if ($response = $analyzeEvent->response()) {
130
                return $response;
131
            }
132
133
            $routeInfo = $this->router->match($request->getMethod(), $request->getPathInfo());
0 ignored issues
show
Documentation introduced by
The property router does not exist on object<Jarvis\Jarvis>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
134
            if (Dispatcher::FOUND === $routeInfo[0]) {
135
                $callback = $this->callback_resolver->resolve($routeInfo[1]);
0 ignored issues
show
Documentation introduced by
The property callback_resolver does not exist on object<Jarvis\Jarvis>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
136
137
                $event = new ControllerEvent($callback, $routeInfo[2]);
138
                $this->masterBroadcast(JarvisEvents::CONTROLLER_EVENT, $event);
139
140
                $response = call_user_func_array($event->callback(), $event->arguments());
141
142
                if (is_string($response)) {
143
                    $response = new Response($response);
144
                }
145
            } elseif (Dispatcher::NOT_FOUND === $routeInfo[0] || Dispatcher::METHOD_NOT_ALLOWED === $routeInfo[0]) {
146
                $response = new Response(null, Dispatcher::NOT_FOUND === $routeInfo[0]
147
                    ? Response::HTTP_NOT_FOUND
148
                    : Response::HTTP_METHOD_NOT_ALLOWED
149
                );
150
            }
151
152
            $this->masterBroadcast(JarvisEvents::RESPONSE_EVENT, new ResponseEvent($request, $response));
153
        } catch (\Exception $exception) {
154
            $this->masterBroadcast(JarvisEvents::EXCEPTION_EVENT, $exceptionEvent = new ExceptionEvent($exception));
155
            $response = $exceptionEvent->response();
156
        }
157
158
        return $response;
159
    }
160
161
    /**
162
     * @param  string  $eventName
163
     * @param  mixed   $receiver
164
     * @param  integer $priority
165
     * @return self
166
     */
167
    public function addReceiver(string $eventName, $receiver, int $priority = self::RECEIVER_NORMAL_PRIORITY) : Jarvis
168
    {
169
        if (!isset($this->receivers[$eventName])) {
170
            $this->receivers[$eventName] = [
171
                self::RECEIVER_LOW_PRIORITY    => [],
172
                self::RECEIVER_NORMAL_PRIORITY => [],
173
                self::RECEIVER_HIGH_PRIORITY   => [],
174
            ];
175
        }
176
177
        $this->receivers[$eventName][$priority][] = $receiver;
178
        $this->computedReceivers[$eventName] = null;
179
180
        if (isset($this->permanentEvents[$eventName])) {
181
            $event = $this->permanentEvents[$eventName];
182
183
            call_user_func_array($this->callback_resolver->resolve($receiver), [$event]);
0 ignored issues
show
Documentation introduced by
The property callback_resolver does not exist on object<Jarvis\Jarvis>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
184
        }
185
186
        return $this;
187
    }
188
189
    /**
190
     * @param  string              $eventName
191
     * @param  EventInterface|null $event
192
     * @return self
193
     */
194
    public function broadcast(string $eventName, EventInterface $event = null) : Jarvis
195
    {
196
        if (!$this->masterEmitter && in_array($eventName, JarvisEvents::RESERVED_EVENT_NAMES)) {
197
            throw new \LogicException(sprintf(
198
                'You\'re trying to broadcast "$eventName" but "%s" are reserved event names.',
199
                implode('|', JarvisEvents::RESERVED_EVENT_NAMES)
200
            ));
201
        }
202
203
        if (isset($this->receivers[$eventName])) {
204
            $event = $event ?? new SimpleEvent();
205
            if ($event instanceof PermanentEventInterface && $event->isPermanent()) {
206
                $this->permanentEvents[$eventName] = $event;
207
            }
208
209
            foreach ($this->buildEventReceivers($eventName) as $receiver) {
210
                call_user_func_array($this->callback_resolver->resolve($receiver), [$event]);
0 ignored issues
show
Documentation introduced by
The property callback_resolver does not exist on object<Jarvis\Jarvis>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
211
212
                if ($event->isPropagationStopped()) {
213
                    break;
214
                }
215
            }
216
        }
217
218
        return $this;
219
    }
220
221
    /**
222
     * @param  ContainerProviderInterface $provider
223
     * @return self
224
     */
225
    public function hydrate(ContainerProviderInterface $provider) : Jarvis
226
    {
227
        $provider->hydrate($this);
228
229
        return $this;
230
    }
231
232
    /**
233
     * Enables master emitter mode.
234
     *
235
     * @return self
236
     */
237
    private function masterBroadcast(string $eventName, EventInterface $event = null) : Jarvis
238
    {
239
        $this->masterEmitter = true;
240
        $this->broadcast($eventName, $event);
241
        $this->masterEmitter = false;
242
243
        return $this;
244
    }
245
246
    /**
247
     * Sets new attribute into Jarvis.
248
     *
249
     * @param  string $key   The name of the new attribute
250
     * @param  mixed  $value The value of the new attribute
251
     * @return self
252
     */
253
    private function masterSetter(string $key, $value) : Jarvis
254
    {
255
        $this->masterSet = true;
256
        $this->$key = $value;
257
        $this->masterSet = false;
258
259
        return $this;
260
    }
261
262
    /**
263
     * Builds and returns well ordered receivers collection that match with provided event name.
264
     *
265
     * @param  string $eventName The event name we want to get its receivers
266
     * @return array
267
     */
268
    private function buildEventReceivers(string $eventName) : array
269
    {
270
        return $this->computedReceivers[$eventName] = $this->computedReceivers[$eventName] ?? array_merge(
271
            $this->receivers[$eventName][self::RECEIVER_HIGH_PRIORITY],
272
            $this->receivers[$eventName][self::RECEIVER_NORMAL_PRIORITY],
273
            $this->receivers[$eventName][self::RECEIVER_LOW_PRIORITY]
274
        );
275
    }
276
}
277