Passed
Push — master ( e1e446...1b5c12 )
by Fabio
06:00
created

TBaseBehavior::detach()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 3
rs 10
1
<?php
2
/**
3
 * TBaseBehavior class file.
4
 *
5
 * @author Brad Anderson <[email protected]>
6
 * @link https://github.com/pradosoft/prado
7
 * @license https://github.com/pradosoft/prado/blob/master/LICENSE
8
 */
9
10
namespace Prado\Util;
11
12
use Prado\Collections\TPriorityItemTrait;
13
use Prado\Exceptions\TInvalidOperationException;
14
use Prado\TComponent;
15
use Prado\TPropertyValue;
16
17
/**
18
 * TBaseBehavior is the base implementing class for both PRADO behaviors types
19
 * {@link TClassBehavior} and {@link @TBehavior}.
20
 *
21
 * This provides an {@link init} stub, {@link events} for attaching the behaviors'
22
 * handlers (value) to events (keys), an {@link getEnabled Enabled} flag, the {@link
23
 * getName Name} of the behavior, a {@link getRetainDisabledHandlers RetainDisabledHandlers}
24
 * flag to retain event handlers on the behavior being disabled, and {@link attach}ing
25
 * and {@link detach}ing from an owner.  Attaching and detaching call methods {@link
26
 * syncEventHandlers} and {@link detachEventHandlers}, respectively, to manage the
27
 * behaviors' handlers in the owner[s] events.
28
 *
29
 * Behaviors use the {@link TPriorityItemTrait} to receive priority information
30
 * from insertion into the owner's {@link TPriorityMap} of behaviors.  When attaching
31
 * events to an owner, the event handlers receive the same priority as the behavior
32
 * in the owner.
33
 *
34
 * Changing the {@link setEnabled Enabled} flag can automatically attach or detach
35
 * events from the owner. If the behavior events should be preserved in the owner
36
 * when disabled, set {@link setRetainDisabledHandlers RetainDisabledHandlers} to
37
 * true.  To force detach event handlers, give this property the value null.  When
38
 * false, the default attachment logic applies where the behavior event handlers
39
 * are attached where the owner has behaviors enabled and the behavior is enabled.
40
 *
41
 * Event Handlers from {@link events} are cached and available by the method {@link
42
 * eventsLog}.  The eventsLog retains Closures across event handler management
43
 * and multiple TClassBehavior owners.
44
 *
45
 * @author Brad Anderson <[email protected]>
46
 * @since 4.2.3
47
 */
48
abstract class TBaseBehavior extends TComponent implements IBaseBehavior
49
{
50
	use TPriorityItemTrait;
51
52
	/** @var ?string The name of the behavior in the owner[s] */
53
	protected ?string $_name = null;
54
55
	/** @var bool Is the behavior enabled or not. Default true */
56
	private bool $_enabled = true;
57
58
	/** @var null|bool Indicates how to maintain the event handlers. Default: false
59
	 *   for default installation logic. True for always attached and null for always
60
	 *   detached.
61
	 */
62
	private $_retainDisabledHandlers = false;
63
64
	/** @var null|array|false The cached events of the behavior, for Closures. */
65
	private $_eventsLog = false;
66
67
	/**
68
	 * Cloning a new instance clears the behavior name.
69
	 */
70
	public function __clone()
71
	{
72
		$this->_name = null;
73
		parent::__clone();
74
	}
75
76
	/**
77
	 * This processes behavior configuration elements.  This is usually called before
78
	 * attach. This is only needed for complex behavior configurations.
79
	 * @param array|\Prado\Xml\TXmlElement $config The innards to the behavior
80
	 *   configuration.
81
	 */
82
	public function init($config)
83
	{
84
	}
85
86
	/**
87
	 * Declares events and the corresponding event handler methods.
88
	 * The events are defined by the {@link owner} component, while the handler
89
	 * methods defined in the behavior class.  These events will be attached to the
90
	 * owner depending on the enable status and {@link getRetainDisabledHandlers}.
91
	 * The format of events is as follows:
92
	 * e.g. return ["onEvent" => function($sender, $param) {...}, "onInit" => "myInitHandler",
93
	 * 'onAnEvent' => [$this, 'methodHandler'], 'onOtherEvent' => [[$this, 'handlerMethod'],
94
	 * "behaviorMethod", function($sender, $param) {...}];
95
	 *
96
	 * Subclasses should use {@link mergeHandlers} to combine event handlers with the
97
	 * parent's event handlers.  For example:
98
	 * <code>
99
	 *	return self::mergeHandlers(parent::events(), ['onEvent' => 'myHandler', ...]]);
100
	 * </code>
101
	 *
102
	 * Acceptable array values are string name of behavior method, callable, or
103
	 * an array of string behavior method names or callables.
104
	 * @return array events (array keys) and the corresponding event handler methods
105
	 *   (array values).
106
	 */
107
	public function events()
108
	{
109
		return [];
110
	}
111
112
	/**
113
	 * Returns the cached events of the behavior where Closures are retained.  TClassBehavior
114
	 * owners will all have the same Closure instances.
115
	 * @return array events (keys) and the array of corresponding event handler methods (values).
116
	 *   e.g. return results look like: ['onMyEvent' => ['objectMethod', $callable, $closure],
117
	 *   'onEvent' => ['behaviorMethod', $callable2, $closure2]]
118
	 */
119
	public function eventsLog()
120
	{
121
		if ($this->_eventsLog === false) {
122
			$this->_eventsLog = self::mergeHandlers($this->events());
123
		}
124
		return $this->_eventsLog;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->_eventsLog also could return the type true which is incompatible with the documented return type array.
Loading history...
125
	}
126
127
	/**
128
	 * By default, all events with handlers in {@link eventsLog} are attached or there
129
	 * will be an error.  When this is false, attaching behavior event handlers will
130
	 * not fail if the owner is missing events and handlers are not attached.  Put
131
	 * another way, when false, behavior event handlers are optional rather than made
132
	 * mandatory.
133
	 * @return bool Strictly enforce attaching behavior event handlers. Default true.
134
	 */
135
	public function getStrictEvents(): bool
136
	{
137
		return true;
138
	}
139
140
	/**
141
	 * Merges the handlers of an event into an array of keys (as event names) and values
142
	 * as an array of behavior method names (strings) and callable.
143
	 * @param array $args The array of events (keys) and values of either behavior method
144
	 *   names (strings), callable, or an array of the former.  Multiple handlers can be
145
	 *   set on a single event to allow subclasses to have their own handlers.
146
	 * @return array The array of combined events (keys) and an array (values) of handlers
147
	 *   for each.
148
	 */
149
	public static function mergeHandlers(...$args): array
150
	{
151
		if(empty($args)) {
152
			return [];
153
		}
154
		$combined = [];
155
		foreach ($args as $events) {
156
			foreach ($events as $name => $handler) {
157
				if (!isset($combined[$name])) {
158
					$combined[$name] = [];
159
				}
160
				if (is_string($handler) || is_callable($handler)) {
161
					$combined[$name][] = $handler;
162
				} elseif (is_array($handler)) {
163
					$combined[$name] += $handler;
164
				}
165
			}
166
		}
167
		return $combined;
168
	}
169
170
	/**
171
	 * Attaches the behavior object to a new owner component.
172
	 * The default implementation will synchronize attachment of event handlers declared
173
	 * in {@link eventsLog}.
174
	 * Make sure you call the parent implementation if you override this method.
175
	 * @param TComponent $component the component that this behavior is being attached to.
176
	 */
177
	public function attach($component)
178
	{
179
		$this->syncEventHandlers($component);
180
	}
181
182
	/**
183
	 * Detaches the behavior object from an owner component.
184
	 * The default implementation will detach event handlers declared in {@link eventsLog}.
185
	 * Make sure you call this parent implementation if you override this method.
186
	 * @param TComponent $component the component that this behavior is being detached from.
187
	 */
188
	public function detach($component)
189
	{
190
		$this->detachEventHandlers($component);
191
	}
192
193
	/**
194
	 * @return ?string The name of the behavior in the owner[s].
195
	 */
196
	public function getName(): ?string
197
	{
198
		return $this->_name;
199
	}
200
201
	/**
202
	 * @param ?string $value The name of the behavior in the owner[s].
203
	 * @throws TInvalidOperationException When there is an owner and the new name is
204
	 *   not the same as the given name.
205
	 */
206
	public function setName($value)
207
	{
208
		if (!$this->hasOwner()) {
209
			$this->_name = TPropertyValue::ensureString($value);
210
		} elseif ($value !== $this->_name) {
211
			throw new TInvalidOperationException('basebehavior_cannot_setname_with_owner', $this->_name, $value);
212
		}
213
	}
214
215
	/**
216
	 * @return bool Whether this behavior is enabled, Default true.
217
	 */
218
	public function getEnabled(): bool
219
	{
220
		return $this->_enabled;
221
	}
222
223
	/**
224
	 * This method sets the enabled flag and synchronizes the behavior's handlers with
225
	 * its owner[s].
226
	 * @param bool|string $value Whether this behavior is enabled.
227
	 */
228
	public function setEnabled($value)
229
	{
230
		$value = TPropertyValue::ensureBoolean($value);
231
		if ($this->_enabled !== $value) {
232
			$this->_enabled = $value;
233
			$this->syncEventHandlers();
234
		}
235
	}
236
237
	/**
238
	 * RetainDisabledHandlers has three states:
239
	 *   1) "true" is to always install the event handlers regardless of behavior enabled
240
	 *      or owner behaviors enabled status.
241
	 *   2) "false" is the default attachment logic to remove the event handlers when
242
	 *      the behavior or owner behaviors are disabled. (default)
243
	 *   3) "null" is to always remove the event handlers.
244
	 * @return null|bool Does the behavior retain the handlers when disabled (true),
245
	 *   remove the handlers when the behavior/owner is disabled (false), or always
246
	 *   remove the handlers (null).
247
	 */
248
	public function getRetainDisabledHandlers()
249
	{
250
		return $this->_retainDisabledHandlers;
251
	}
252
253
	/**
254
	 * This changes the retaining of disabled behavior handlers and then synchronizes
255
	 * the behavior's handlers with its owner[s].  There are three acceptable values:
256
	 *   1) "true" is to always install the event handlers regardless of behavior enabled
257
	 *      or owner behaviors enabled status.
258
	 *   2) "false" is the default attachment logic to remove the event handlers when
259
	 *      the behavior or owner behaviors are disabled. (default)
260
	 *   3) "null" is to always remove the event handlers.
261
	 * @param null|bool $value Does the behavior retain the handlers when disabled (true),
262
	 *   remove the handlers when the behavior/owner is disabled (false), or always
263
	 *   remove handlers (null).
264
	 */
265
	public function setRetainDisabledHandlers($value)
266
	{
267
		if ($value !== 0 && $value !== null && (!is_string($value) || strtolower($value) !== 'null' && $value !== '0')) {
268
			$value = TPropertyValue::ensureBoolean($value);
269
		} else {
270
			$value = null;
271
		}
272
		if ($this->_retainDisabledHandlers !== $value) {
273
			$this->_retainDisabledHandlers = $value;
274
			$this->syncEventHandlers();
275
		}
276
	}
277
278
	/**
279
	 * This synchronizes an owner's events of the behavior event handlers by attaching
280
	 * or detaching where needed.  A behaviors handlers are attached depending on whether
281
	 * {@link getRetainDisabledHandlers} is true (or null) or both the owner and behavior are
282
	 * [Behavior] enabled.  The $attachOverride will set RetainDisabledHandlers when not
283
	 * its default value 0 and thus can act like {@link setRetainDisabledHandlers}.
284
	 * @param ?object $component The component to manage the behaviors handlers on. Default
285
	 *   is null for synchronizing all owners.
286
	 * @param null|bool|int $attachOverride Overrides the default attachment logic or whether
287
	 *   to install and forcibly attach or detach the handlers when true or null.  false resets
288
	 *   RetainDisabledHandlers to the default attachment logic (of when enabled). Default is 0
289
	 *   for normal action and no override (true/null) or reset (false).
290
	 * @throws TInvalidOperationException When synchronizing a component without owners
291
	 *   or the component that isn't an owner.
292
	 * @since 4.2.3
293
	 */
294
	public function syncEventHandlers(?object $component = null, $attachOverride = 0)
295
	{
296
		$hasOwner = $this->hasOwner();
297
		if ($component && !$hasOwner) {
298
			throw new TInvalidOperationException('basebehavior_sync_no_owner', $this->getName());
299
		}
300
		if (!$hasOwner) {
301
			return;
302
		}
303
		if ($component && !$this->isOwner($component)) {
304
			throw new TInvalidOperationException('basebehavior_sync_not_owner', $this->getName());
305
		}
306
		foreach ($component ? [$component] : $this->getOwners() as $component) {
307
			if (is_bool($attachOverride) || $attachOverride === null) {
308
				$this->_retainDisabledHandlers = $attachOverride;
309
			}
310
			if ($this->_retainDisabledHandlers !== false) {
311
				$install = (bool) $this->_retainDisabledHandlers;
312
			} else {
313
				$install = $this->getEnabled() && $component->getBehaviorsEnabled();
314
			}
315
			if ($this->getHandlersStatus($component) ^ $install) {
316
				if ($install) {
317
					$this->attachEventHandlers($component);
318
				} else {
319
					$this->detachEventHandlers($component);
320
				}
321
			}
322
		}
323
	}
324
325
	/*
326
	 * Attaches the behavior event handlers to an owner component. This tracks of the
327
	 * attachment status of handlers on the owner components.
328
	 * @param TComponent $component The component to attach the behavior event handlers to.
329
	 * @since 4.2.3
330
	 */
331
	protected function attachEventHandlers(TComponent $component): void
332
	{
333
		if ($this->setHandlersStatus($component, true)) {
334
			$priority = $this->getPriority();
335
			$strict = $this->getStrictEvents();
336
			foreach ($this->eventsLog() as $event => $handlers) {
337
				if ($strict || $this->hasEvent($event)) {
338
					foreach($handlers as $handler) {
339
						$component->attachEventHandler($event, is_string($handler) ? [$this, $handler] : $handler, $priority);
0 ignored issues
show
Bug introduced by
It seems like $priority can also be of type double; however, parameter $priority of Prado\TComponent::attachEventHandler() does only seem to accept Prado\numeric|null, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

339
						$component->attachEventHandler($event, is_string($handler) ? [$this, $handler] : $handler, /** @scrutinizer ignore-type */ $priority);
Loading history...
340
					}
341
				}
342
			}
343
		}
344
	}
345
346
	/*
347
	 * Detaches the behavior event handlers from an owner component. This tracks of the
348
	 * attachment status of handlers on the owner components.
349
	 * @param TComponent $component The component to detach the behavior event handlers from.
350
	 * @since 4.2.3
351
	 */
352
	protected function detachEventHandlers(TComponent $component): void
353
	{
354
		if ($this->setHandlersStatus($component, false)) {
355
			$strict = $this->getStrictEvents();
356
			foreach ($this->eventsLog() as $event => $handlers) {
357
				if ($strict || $this->hasEvent($event)) {
358
					foreach($handlers as $handler) {
359
						$component->detachEventHandler($event, is_string($handler) ? [$this, $handler] : $handler);
360
					}
361
				}
362
			}
363
		}
364
	}
365
366
	/**
367
	 * This gets the attachment status of the behavior event handlers on the given
368
	 * component.
369
	 * @param ?TComponent $component The component to check the status of the handlers.
370
	 *   Null only works for IBehavior and returns the status of the single owner.
371
	 * @return bool Are the behavior handlers attached to the given owner events.
372
	 * @since 4.2.3
373
	 */
374
	abstract protected function getHandlersStatus(?TComponent $component = null): ?bool;
375
376
	/**
377
	 * This sets the attachment status of the behavior event handlers on the given
378
	 * component.
379
	 * @param TComponent $component The component to set the status of.
380
	 * @param bool $attach "true" to attach the handlers or "false" detach them.
381
	 * @return bool Is there a change in the attachment status for the given owner
382
	 *   component.
383
	 * @since 4.2.3
384
	 */
385
	abstract protected function setHandlersStatus(TComponent $component, bool $attach): bool;
386
387
	/**
388
	 * Returns an array with the names of all variables of this object that should NOT be serialized
389
	 * because their value is the default one or useless to be cached for the next page loads.
390
	 * Reimplement in derived classes to add new variables, but remember to  also to call the parent
391
	 * implementation first.
392
	 * @param array &$exprops Properties of the object to exclude.
393
	 */
394
	protected function _getZappableSleepProps(&$exprops)
395
	{
396
		parent::_getZappableSleepProps($exprops);
397
		$exprops[] = "\0*\0_name";
398
		if ($this->_enabled === true) {
399
			$exprops[] = "\0" . __CLASS__ . "\0_enabled";
400
		}
401
		if ($this->_retainDisabledHandlers === null) {
402
			$exprops[] = "\0" . __CLASS__ . "\0_retainDisabledHandlers";
403
		}
404
		$exprops[] = "\0" . __CLASS__ . "\0_eventsLog";
405
		$this->_priorityItemZappableSleepProps($exprops);
406
	}
407
}
408