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