Passed
Push — master ( 30451f...036a09 )
by Fabio
07:34 queued 02:23
created

TEventHandler::offsetExists()   A

Complexity

Conditions 5
Paths 7

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 1
nc 7
nop 1
dl 0
loc 3
rs 9.6111
c 1
b 0
f 0
1
<?php
2
/**
3
 * TEventHandler class
4
 *
5
 * @link https://github.com/pradosoft/prado
6
 * @license https://github.com/pradosoft/prado/blob/master/LICENSE
7
 */
8
9
namespace Prado;
10
11
use Prado\Collections\IWeakRetainable;
12
use Prado\Exceptions\TApplicationException;
13
use Prado\Exceptions\TInvalidDataTypeException;
14
use Prado\Exceptions\TInvalidDataValueException;
15
use Prado\Exceptions\TInvalidOperationException;
16
17
use Closure;
18
use WeakReference;
19
20
/**
21
 * TEventHandler class
22
 *
23
 * This class is a helper class for passing specific data along with an event handler.
24
 * Normal event handlers only have a sender and parameter, but under a TEventHandler
25
 * there is a third parameter in the event handler method for data.  This class is
26
 * invokable and will pass invoked method arguments forward to the managed handler
27
 * with the specified data.
28
 *
29
 * A TEventHandler handler method would be implemented as such:
30
 * <code>
31
 *    $handler = new TEventHandler([$object, 'myHandler'], ['key' => 'data']);
32
 *    $handler($sender, $param); // <- invokable
33
 *    $component->attachEventHandler('onMyEvent', $handler, $priority);
34
 *
35
 *  // In the $object class:
36
 *    public function myHandler(object $sender, mixed $param, mixed $data = null): mixed
37
 *    {
38
 *	 	// $data === ['key' => 'data']
39
 *       ....
40
 *    }
41
 * </code>
42
 * In this instance, $data is default to null so it can be called on a raised
43
 * attached event without TEventHandler.  If you will only have a your handler use
44
 * TEventHandler, then $data can be required (without the null default).
45
 *
46
 * There are several ways to access the event handler (callable).  {@link getHandler}
47
 * will return the event handler and can be requested to return the callable as
48
 * WeakReference.
49
 *
50
 * The event handler can be accessed by ArrayAccess as well.  For example:
51
 * <code>
52
 *		$handler = new TEventHandler('TMyClass::myStaticHandler', ['data' => 2, ...]);
53
 *		$handler[null] === 'TMyClass::myStaticHandler' === $handler->getHandler();
54
 *		$handler[0] === 'TMyClass::myStaticHandler';
55
 *		$handler[1] === null
56
 *		$handler[2] === ['data' => 2, ...] === $handler->getData();
57
 *
58
 *		$handler = new TEventHandler([$behavior, 'myHandler'], ['data' => 3, ...]);
59
 *		$handler[null] === [$behavior, 'myHandler'] === $handler->getHandler();
60
 *		$handler[0] === $behavior;
61
 *		$handler[1] === 'myHandler';
62
 *		$handler[2] === ['data' => 3, ...] === $handler->getData();
63
 *
64
 *		// Add the handler to the event at priority 12 (the default is 10)
65
 *		$component->attachEventHandler('onMyEvent', $handler, 12);
66
 * </code>
67
 *
68
 * PRADO event handler objects are stored as WeakReference to improve PHP garbage
69
 * collection.  To enable this functionality, TEventHandler holds its callable object
70
 * as WeakReference and re-references the callable handler object when used.
71
 *
72
 * The only exceptions to conversion into WeakReference are Closure and {@link IWeakRetainable}.
73
 * Closure and IWeakRetainable are retained without WeakReference conversion because
74
 * they might be the only instance in the application.  Holding these instances
75
 * directly will properly increment their PHP use counter to be retained.
76
 *
77
 * {@link hasWeakObject} returns if there is an object being held as WeakReference.
78
 *
79
 * When the TEventHandler {@link getData data} is an array, and the 3rd parameter
80
 * $data of {@link __invoke invoke} is also an array, an `array_replace` (with the
81
 * function parameter array taking precedence) will combine the data.
82
 *
83
 * In nesting TEventHandlers, a base TEventHandler, pointing to a callable, can be
84
 * instanced with core data.  Children TEventHandler[s] can point to the parent
85
 * TEventHandler and override specific data items in the {@link getData Data} array
86
 * with its own (array) data items.  This only works if the Data is an array otherwise
87
 * the children will override the parent TEventHandler data.
88
 *
89
 * @author Brad Anderson <[email protected]>
90
 * @since 4.2.3
91
 */
92
class TEventHandler implements IWeakRetainable, \ArrayAccess, \Countable
93
{
94
	/** @var mixed The callable event handler being managed. */
95
	private mixed $_handler;
96
97
	/** @var mixed The data to feed the handler when invoked. */
98
	private mixed $_data;
99
100
	/** @var bool Is the callable object converted into WeakReference? */
101
	private bool $_weakObject = false;
102
103
	/**
104
	 * Constructs a new TEventHandler and initializes the Event Handler and Data.
105
	 * @param mixed $handler The event handler being managed.
106
	 * @param mixed $data The data to feed the event handler.
107
	 */
108
	public function __construct(mixed $handler, mixed $data = null)
109
	{
110
		if(!is_callable($handler)) {
111
			throw new TInvalidDataTypeException('eventhandler_not_callable');
112
		}
113
114
		if (is_array($handler) && is_object($handler[0])) {
115
			$handler[0] = WeakReference::create($handler[0]);
116
			$this->_weakObject = true;
117
		} elseif (is_object($handler) && !($handler instanceof Closure) && !($handler instanceof IWeakRetainable)) {
118
			$handler = WeakReference::create($handler);
119
			$this->_weakObject = true;
120
		}
121
122
		$this->_handler = $handler;
123
		$this->_data = $data;
124
	}
125
126
	/**
127
	 * This calls the handler with the specified data.  If no $data is specified then
128
	 * TEventHandler injects its own data for the event handler.  When $data is an array
129
	 * and the TEventHandler data is an array, the data is combined by `array_replace`,
130
	 * with the function input array data  taking precedence over TEventHandler data.
131
	 * This allows TEventHandlers to be nested, with children taking precedence.
132
	 * @param mixed $sender The sender that raised the event.
133
	 * @param mixed $param The parameter that goes with the raised event.
134
	 * @param null|mixed $data The data for the managed event handler. default null
135
	 *   for data from the TEventHandler.
136
	 * @param array $argv Any additional function arguments.
137
	 * @throws TApplicationException
138
	 * @return mixed The result of the event handler.
139
	 */
140
	public function __invoke(mixed $sender = null, mixed $param = null, mixed $data = null, ...$argv): mixed
141
	{
142
		$handler = $this->getHandler();
143
		if (!$handler) {
144
			throw new TApplicationException('eventhandler_lost_weak_ref');
145
		}
146
		if (is_array($data) && is_array($this->_data)) {
147
			$data = array_replace($this->_data, $data);
148
		} elseif ($data === null) {
149
			$data = $this->_data;
150
		}
151
		return $handler($sender, $param, $data, ...$argv);
152
	}
153
154
	/**
155
	 * Gets the handler being managed. The WeakReference version of the handler can
156
	 * be retrieved by passing true to the $weak parameter.  When there are nested
157
	 * TEventHandlers, this will return the next TEventHandler as an invokable and will
158
	 * not link to the last and actual callable handler.
159
	 * @param bool $weak Return the handler with WeakReference instead of objects. Default false.
160
	 * @return null|array|object|string The callable event handler.
161
	 */
162
	public function getHandler(bool $weak = false): null|string|object|array
163
	{
164
		$handler = $this->_handler;
165
		if (!$weak && $this->_weakObject) {
166
			if (is_array($handler) && is_object($handler[0]) && ($handler[0] instanceof WeakReference)) {
167
				if ($obj = $handler[0]->get()) {
168
					$handler[0] = $obj;
169
				} else {
170
					$handler = null;
171
				}
172
			} elseif (is_object($handler) && ($handler instanceof WeakReference)) {
173
				$handler = $handler->get();
174
			}
175
		}
176
		return $handler;
177
	}
178
179
	/**
180
	 * This methods checks if the item is the same as the handler.  When TEventHandler
181
	 * are nested, we check the nested TEventHandler for isSameHandler.
182
	 * @param mixed $item
183
	 * @param bool $weak
184
	 * @return bool Is the $item the same as the managed callable.
185
	 */
186
	public function isSameHandler(mixed $item, bool $weak = false): bool
187
	{
188
		$handler = $this->getHandler($weak);
189
		if ($item === $handler) {
190
			return true;
191
		}
192
		if ($handler instanceof TEventHandler) {
193
			return $handler->isSameHandler($item, $weak);
194
		}
195
		return false;
196
	}
197
198
	/**
199
	 * If there is an object referenced in the callable, this method returns the object.
200
	 * By default, the WeakReference is re-referenced.  The WeakReference can be returned
201
	 * by passing true for $weak.  This will return Closure and IWeakRetainable objects
202
	 * without any WeakReference because they are exempt from conversion into WeakReference.
203
	 * When TEventHandlers are nested, this returns the last and actual callable object.
204
	 * @param bool $weak Return the callable with WeakReference instead of objects.
205
	 * @return ?object The object of the event handler if there is one.
206
	 */
207
	public function getHandlerObject(bool $weak = false): ?object
208
	{
209
		$handler = null;
210
		if (is_array($this->_handler) && is_object($this->_handler[0])) {
211
			$handler = $this->_handler[0];
212
		} elseif($this->_handler instanceof TEventHandler) {
213
			return $this->_handler->getHandlerObject($weak);
214
		} elseif(is_object($this->_handler)) {
215
			$handler = $this->_handler;
216
217
		}
218
		if(!$weak && $this->_weakObject) {
219
			$handler = $handler->get();
220
		}
221
		return $handler;
222
	}
223
224
	/**
225
	 * This checks if the managed handler is still valid.  When TEventHandler are nested,
226
	 * this returns if the last and actual handler is still valid (from WeakReference).
227
	 * @return bool Does the managed event Handler exist and is callable.
228
	 */
229
	public function hasHandler(): bool
230
	{
231
		if($this->_handler instanceof TEventHandler) {
232
			return $this->_handler->hasHandler();
233
		}
234
		return $this->getHandler() !== null;
235
	}
236
237
	/**
238
	 * Returns the data associated with the managed event handler. By passing true to
239
	 * $withHandlerData, this will combine the data from nested TEventHandlers when the
240
	 * data is in array format.  The children data take precedence in the combining
241
	 * of data.
242
	 * @param bool $withHandlerData
243
	 * @return mixed The data associated with the event handler.
244
	 */
245
	public function getData(bool $withHandlerData = false): mixed
246
	{
247
		if ($withHandlerData && ($this->_handler instanceof TEventHandler) && is_array($this->_data) && is_array($data = $this->_handler->getData(true))) {
248
			return array_replace($data, $this->_data);
249
		}
250
		return $this->_data;
251
	}
252
253
	/**
254
	 * @param mixed $data The data associated with the event handler.
255
	 */
256
	public function setData(mixed $data): void
257
	{
258
		$this->_data = $data;
259
	}
260
261
	/**
262
	 * Does the object contain any WeakReference objects?  When there are nested TEventHandler
263
	 * this will return the last and actual data from the actual callable.
264
	 * @return bool If there are objects as WeakReferences.
265
	 */
266
	public function hasWeakObject(): bool
267
	{
268
		if($this->_handler instanceof TEventHandler) {
269
			return $this->_handler->hasWeakObject();
270
		}
271
		return $this->_weakObject;
272
	}
273
274
	/**
275
	 * Returns the number of items in the managed event handler (with data).
276
	 * This method is required by \Countable interface.
277
	 * @return int The number of items in the managed event handler.
278
	 */
279
	public function count(): int
280
	{
281
		return $this->getCount();
282
	}
283
284
	/**
285
	 * There are 3 items when the handler is an array, and 2 items when the handler
286
	 * is an invokable object or string.
287
	 * @return int The number of items in the managed event handler (with data).
288
	 */
289
	public function getCount(): int
290
	{
291
		return is_array($this->_handler) ? 3 : 2;
292
	}
293
294
	/**
295
	 * These values exist: null (the handler), 0, 2, and conditionally 1 on the handler
296
	 * being an array.
297
	 * @param mixed $offset The offset to check existence.
298
	 * @return bool Does the property exist for the managed event handler (with data).
299
	 */
300
	public function offsetExists(mixed $offset): bool
301
	{
302
		return $offset === null || $offset === 0 || $offset === 2 || (is_array($this->_handler) ? $offset === 1 : 0);
303
	}
304
305
	/**
306
	 * This is a convenience method for getting the data of the TEventHandler.
307
	 * - Index null will return the {@link getHandler Handler},
308
	 * - Index '0' will return the handler if its a string or object, or the first element
309
	 *   of the callable array;
310
	 * - Index '1' will return the second element of the callable array or null if not
311
	 *   an array; and
312
	 * - Index '2' will return the data associated with the managed event handler.
313
	 * If the WeakReference object of the managed event handler is invalid, this will return null
314
	 * for all handler offsets (null, 0 and 1).  Data will still return properly even
315
	 * when the handler is invalid.
316
	 * @param mixed $offset Which property of the managed event handler to retrieve.
317
	 * @throws TInvalidDataValueException When $offset is not a property of the managed event handler.
318
	 * @return mixed The value of the handler, handler elements, or data.
319
	 */
320
	public function offsetGet(mixed $offset): mixed
321
	{
322
		if ($offset === null) {
323
			return $this->getHandler();
324
		}
325
		if (is_numeric($offset)) {
326
			$offset = (int) $offset;
327
			if ($offset === 2) {
328
				return $this->_data;
329
			} elseif(is_array($this->_handler)) {
330
				if ($offset === 0) {
331
					if ($this->_weakObject) {
332
						return $this->_handler[$offset]->get();
333
					}
334
					return $this->_handler[$offset];
335
				} elseif ($offset === 1) {
336
					if ($this->_weakObject && !$this->_handler[0]->get()) {
337
						return null;
338
					}
339
					return $this->_handler[$offset];
340
				}
341
			} elseif ($offset === 0) {
342
				if ($this->_weakObject) {
343
					return $this->_handler->get();
344
				}
345
				return $this->_handler;
346
			} elseif ($offset === 1) {
347
				return null;
348
			}
349
		}
350
		throw new TInvalidDataValueException('eventhandler_bad_offset', $offset);
351
	}
352
353
	/**
354
	 * This is a convenience method for setting the data of the managed event handler.
355
	 * This cannot set the event handler but can set the data.
356
	 * - Index '2' will set the data associated with the managed event handler.
357
	 * @param mixed $offset Only accepts '2' to set the data associated with the event handler.
358
	 * @param mixed $value The data being set.
359
	 * @throws TInvalidOperationException When trying to set the handler elements.
360
	 * @throws TInvalidDataValueException When $offset is not a property of the managed event handler.
361
	 */
362
	public function offsetSet(mixed $offset, mixed $value): void
363
	{
364
		if (is_numeric($offset)) {
365
			$offset = (int) $offset;
366
			if ($offset === 2) {
367
				$this->setData($value);
368
				return;
369
			}
370
		}
371
		if ($offset === null || is_numeric($offset) && ($offset == 0 || $offset == 1)) {
372
			throw new TInvalidOperationException('eventhandler_no_set_handler', $offset);
373
		}
374
		throw new TInvalidDataValueException('eventhandler_bad_offset', $offset);
375
	}
376
377
	/**
378
	 * This is a convenience method for resetting the data to null.  The Handler cannot
379
	 * be unset.  However, the data can be unset.  The only valid index for unsetting
380
	 * the handler data is '2'.
381
	 * @param mixed $offset Only accepts '2' for the data element.
382
	 * @throws TInvalidOperationException When trying to set the handler elements.
383
	 * @throws TInvalidDataValueException When $offset is not a property of the managed event handler.
384
	 */
385
	public function offsetUnset(mixed $offset): void
386
	{
387
		if (is_numeric($offset)) {
388
			$offset = (int) $offset;
389
			if ($offset === 2) {
390
				$this->setData(null);
391
				return;
392
			}
393
		}
394
		if ($offset === null || is_numeric($offset) && ($offset == 0 || $offset == 1)) {
395
			throw new TInvalidOperationException('eventhandler_no_set_handler', $offset);
396
		}
397
		throw new TInvalidDataValueException('eventhandler_bad_offset', $offset);
398
	}
399
}
400