1 <?php
2
3 /*
4 * This file is part of the ICanBoogie package.
5 *
6 * (c) Olivier Laviale <olivier.laviale@gmail.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12 namespace ICanBoogie;
13
14 /**
15 * An event.
16 *
17 * @property-read $stopped bool `true` when the event was stopped, `false` otherwise.
18 * @property-read $used int The number of event hooks that were invoked while dispatching the event.
19 * @property-read $used_by array Event hooks that were invoked while dispatching the event.
20 * @property-read $target mixed The object the event is dispatched on.
21 */
22 class Event
23 {
24 /**
25 * The reserved properties that cannot be used to provide event properties.
26 *
27 * @var array[string]bool
28 */
29 static private $reserved = array('chain' => true, 'stopped' => true, 'target' => true, 'used' => true, 'used_by' => true);
30
31 /**
32 * Profiling information about events.
33 *
34 * @var array
35 */
36 static public $profiling = array
37 (
38 'hooks' => array(),
39 'unused' => array()
40 );
41
42 /**
43 * `true` when the event was stopped, `false` otherwise.
44 *
45 * @var bool
46 */
47 private $stopped = false;
48
49 /**
50 * Event hooks that were invoked while dispatching the event.
51 *
52 * @var array
53 */
54 private $used_by = array();
55
56 /**
57 * The object the event is dispatched on.
58 *
59 * @var mixed
60 */
61 private $target;
62
63 /**
64 * Chain of hooks to execute once the event has been fired.
65 *
66 * @var array
67 */
68 private $chain = array();
69
70 /**
71 * Creates an event and fires it immediately.
72 *
73 * If the event's target is specified its class is used to prefix the event type. For example,
74 * if the event's target is an instance of `ICanBoogie\Operation` and the event type is
75 * `process` the final event type will be `ICanBoogie\Operation::process`.
76 *
77 * @param mixed $target The target of the event.
78 * @param string $type The event type.
79 * @param array $payload Event payload.
80 *
81 * @throws PropertyIsReserved in attempt to specify a reserved property with the payload.
82 */
83 public function __construct($target, $type, array $payload=array())
84 {
85 if ($target)
86 {
87 $class = get_class($target);
88 $type = $class . '::' . $type;
89 }
90
91 $events = Events::get();
92
93 if ($events->is_skippable($type))
94 {
95 return;
96 }
97
98 $hooks = $events->get_hooks($type);
99
100 if (!$hooks)
101 {
102 self::$profiling['unused'][] = array(microtime(true), $type);
103
104 $events->skip($type);
105
106 return;
107 }
108
109 $this->target = $target;
110
111 #
112 # copy payload to the event's properties.
113 #
114
115 foreach ($payload as $property => &$value)
116 {
117 if (isset(self::$reserved[$property]))
118 {
119 throw new PropertyIsReserved($property);
120 }
121
122 #
123 # we need to set the property to null before we set its value by reference
124 # otherwise if the property doesn't exists the magic method `__get()` is
125 # invoked and throws an exception because we try to get the value of a
126 # property that do not exists.
127 #
128
129 $this->$property = null;
130 $this->$property = &$value;
131 }
132
133 #
134 # process event hooks chain
135 #
136
137 foreach ($hooks as $hook)
138 {
139 $this->used_by[] = $hook;
140
141 $time = microtime(true);
142
143 call_user_func($hook, $this, $target);
144
145 self::$profiling['hooks'][] = array($time, $type, $hook, microtime(true) - $time);
146
147 if ($this->stopped)
148 {
149 return;
150 }
151 }
152
153 #
154 # process finish chain hooks
155 #
156
157 foreach ($this->chain as $hook)
158 {
159 $this->used_by[] = $hook;
160
161 $time = microtime(true);
162
163 call_user_func($hook, $this, $target);
164
165 self::$profiling['hooks'][] = array($time, $type, $hook, microtime(true) - $time);
166
167 if ($this->stopped)
168 {
169 return;
170 }
171 }
172 }
173
174 /**
175 * Handles the read-only properties {@link $stopped}, {@link $used}, {@link $used_by}
176 * and {@link $target}.
177 *
178 * @param string $property The read-only property to return.
179 *
180 * @throws PropertyNotReadable if the property exists but is not readable.
181 * @throws PropertyNotDefined if the property doesn't exists.
182 *
183 * @return mixed
184 */
185 public function __get($property)
186 {
187 static $readers = array('stopped', 'used_by', 'target');
188
189 if ($property === 'used')
190 {
191 return count($this->used_by);
192 }
193 else if (in_array($property, $readers))
194 {
195 return $this->$property;
196 }
197
198 $properties = get_object_vars($this);
199
200 if (array_key_exists($property, $properties))
201 {
202 throw new PropertyNotReadable(array($property, $this));
203 }
204
205 throw new PropertyNotDefined(array($property, $this));
206 }
207
208 /**
209 * Stops the hooks chain.
210 *
211 * After the `stop()` method is called the hooks chain is broken and no other hook is called.
212 */
213 public function stop()
214 {
215 $this->stopped = true;
216 }
217
218 /**
219 * Add an event hook to the finish chain.
220 *
221 * The finish chain is executed after the event chain was traversed without being stopped.
222 *
223 * @param callable $hook
224 *
225 * @return \ICanBoogie\Event
226 */
227 public function chain($hook)
228 {
229 $this->chain[] = $hook;
230
231 return $this;
232 }
233 }