1 <?php
2
3 4 5 6 7 8 9 10
11
12 namespace ICanBoogie;
13
14 15 16
17 class Events implements \IteratorAggregate
18 {
19 static private $jumptable = array
20 (
21 'get' => array(__CLASS__, 'get')
22 );
23
24 25 26 27 28 29 30 31
32 static public function __callstatic($name, array $arguments)
33 {
34 return call_user_func_array(self::$jumptable[$name], $arguments);
35 }
36
37 38 39 40 41 42 43 44
45 static public function patch($name, $callback)
46 {
47 if (empty(self::$jumptable[$name]))
48 {
49 throw new \RuntimeException("Undefined patchable: $name.");
50 }
51
52 self::$jumptable[$name] = $callback;
53 }
54
55 56 57 58 59
60 static private function get()
61 {
62 static $events;
63
64 if (!$events)
65 {
66 $events = new static;
67 }
68
69 return $events;
70 }
71
72 73 74 75 76
77 protected $hooks = array();
78
79 80 81 82 83
84 protected $consolidated_hooks = array();
85
86 87 88 89 90
91 protected $skippable = array();
92
93 public function __construct(array $hooks=array())
94 {
95 $this->hooks = $hooks;
96 }
97
98 99 100
101 public function getIterator()
102 {
103 return new \ArrayIterator($this->hooks);
104 }
105
106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130
131 public function attach($name, $hook=null)
132 {
133 if ($hook === null)
134 {
135 $hook = $name;
136 $name = null;
137 }
138
139 if (!is_callable($hook))
140 {
141 throw new \InvalidArgumentException(format
142 (
143 'The event hook must be a callable, %type given: :hook', array
144 (
145 'type' => gettype($hook),
146 'hook' => $hook
147 )
148 ));
149 }
150
151 if ($name === null)
152 {
153 $name = self::resolve_event_type_from_hook($hook);
154 }
155
156 if (!isset($this->hooks[$name]))
157 {
158 $this->hooks[$name] = array();
159 }
160
161 array_unshift($this->hooks[$name], $hook);
162
163
164
165
166
167 $this->skippable = array();
168
169 if (strpos($name, '::') !== false)
170 {
171 $this->consolidated_hooks = array();
172 }
173
174 return new EventHook($this, $name, $hook);
175 }
176
177 178 179 180 181 182 183
184 static private function resolve_event_type_from_hook($hook)
185 {
186 if (is_array($hook))
187 {
188 $reflection = new \ReflectionMethod($hook[0], $hook[1]);
189 }
190 else if (is_string($hook) && strpos($hook, '::'))
191 {
192 list($class, $method) = explode('::', $hook);
193
194 $reflection = new \ReflectionMethod($class, $method);
195 }
196 else
197 {
198 $reflection = new \ReflectionFunction($hook);
199 }
200
201 list($event, $target) = $reflection->getParameters();
202
203 $event_class = self::get_parameter_class($event);
204 $target_class = self::get_parameter_class($target);
205
206 $event_class_base = basename('/' . strtr($event_class, '\\', '/'));
207 $type = substr($event_class_base, 0, -5);
208
209 if (strpos($event_class_base, 'Before') === 0)
210 {
211 $type = hyphenate(substr($type, 6)) . ':before';
212 }
213 else
214 {
215 $type = hyphenate($type);
216 }
217
218 $type = strtr($type, '-', '_');
219
220 return $target_class . '::' . $type;
221 }
222
223 224 225 226 227 228 229 230 231 232
233 static private function get_parameter_class(\ReflectionParameter $param)
234 {
235 if (!preg_match('#\[\s*(<[^>]+>)?\s*([^\s]+)#', $param, $matches))
236 {
237 return;
238 }
239
240 return $matches[2];
241 }
242
243 public function batch_attach(array $definitions)
244 {
245 $this->hooks = \array_merge_recursive($this->hooks, $definitions);
246 $this->skippable = array();
247 $this->consolidated_hooks = array();
248 }
249
250 251 252 253 254 255 256 257 258 259
260 public function detach($name, $hook)
261 {
262 $hooks = &$this->hooks;
263
264 if (isset($hooks[$name]))
265 {
266 foreach ($hooks[$name] as $key => $h)
267 {
268 if ($h != $hook)
269 {
270 continue;
271 }
272
273 unset($hooks[$name][$key]);
274
275 if (!count($hooks[$name]))
276 {
277 unset($hooks[$name]);
278 }
279
280 if (strpos($name, '::') !== false)
281 {
282 $this->consolidated_hooks = array();
283 }
284
285 return;
286 }
287 }
288
289 throw new \Exception("The specified event hook is not attached to `{$name}`.");
290 }
291
292 293 294 295 296
297 public function skip($name)
298 {
299 $this->skippable[$name] = true;
300 }
301
302 303 304 305 306 307 308
309 public function is_skippable($name)
310 {
311 return isset($this->skippable[$name]);
312 }
313
314 315 316 317 318 319 320 321 322 323
324 public function get_hooks($name)
325 {
326 if (!strpos($name, '::'))
327 {
328 return isset($this->hooks[$name]) ? $this->hooks[$name] : array();
329 }
330
331 if (isset($this->consolidated_hooks[$name]))
332 {
333 return $this->consolidated_hooks[$name];
334 }
335
336 list($class, $type) = explode('::', $name);
337
338 $hooks = array();
339 $c = $class;
340
341 while ($c)
342 {
343 if (isset($this->hooks[$c . '::' . $type]))
344 {
345 $hooks = array_merge($hooks, $this->hooks[$c . '::' . $type]);
346 }
347
348 $c = get_parent_class($c);
349 }
350
351 $this->consolidated_hooks[$name] = $hooks;
352
353 return $hooks;
354 }
355 }
356
357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380
381 class EventHook
382 {
383 private $type;
384 private $hook;
385 private $events;
386
387 public function __construct(Events $events, $type, $hook)
388 {
389 $this->events = $events;
390 $this->type = $type;
391 $this->hook = $hook;
392 }
393
394 public function __get($property)
395 {
396 static $readers = array('events', 'type', 'hook');
397
398 if (in_array($property, $readers))
399 {
400 return $this->$property;
401 }
402
403 throw new PropertyNotDefined(array($property, $this));
404 }
405
406 407 408
409 public function detach()
410 {
411 $this->events->detach($this->type, $this->hook);
412 }
413 }