Completed
Pull Request — master (#16756)
by Vladimir
13:05
created

Component   F

Complexity

Total Complexity 97

Size/Duplication

Total Lines 665
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 8

Test Coverage

Coverage 99.01%

Importance

Changes 0
Metric Value
wmc 97
lcom 1
cbo 8
dl 0
loc 665
ccs 200
cts 202
cp 0.9901
rs 1.935
c 0
b 0
f 0

23 Methods

Rating   Name   Duplication   Size   Complexity  
A __get() 0 22 5
B __set() 0 36 8
A __isset() 0 17 4
A __call() 0 10 3
A __clone() 0 6 1
A hasProperty() 0 4 2
B canGetProperty() 0 15 7
B canSetProperty() 0 15 7
A hasMethod() 0 15 5
A behaviors() 0 4 1
A hasEventHandlers() 0 12 5
A on() 0 19 6
C off() 0 45 13
B trigger() 0 37 9
A getBehavior() 0 5 2
A getBehaviors() 0 5 1
A attachBehavior() 0 5 1
A attachBehaviors() 0 7 2
A detachBehavior() 0 12 2
A detachBehaviors() 0 7 2
A ensureBehaviors() 0 9 3
A attachBehaviorInternal() 0 18 4
A __unset() 0 19 4

How to fix   Complexity   

Complex Class

Complex classes like Component often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Component, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @link http://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license http://www.yiiframework.com/license/
6
 */
7
8
namespace yii\base;
9
10
use Yii;
11
use yii\helpers\StringHelper;
12
13
/**
14
 * Component is the base class that implements the *property*, *event* and *behavior* features.
15
 *
16
 * Component provides the *event* and *behavior* features, in addition to the *property* feature which is implemented in
17
 * its parent class [[\yii\base\BaseObject|BaseObject]].
18
 *
19
 * Event is a way to "inject" custom code into existing code at certain places. For example, a comment object can trigger
20
 * an "add" event when the user adds a comment. We can write custom code and attach it to this event so that when the event
21
 * is triggered (i.e. comment will be added), our custom code will be executed.
22
 *
23
 * An event is identified by a name that should be unique within the class it is defined at. Event names are *case-sensitive*.
24
 *
25
 * One or multiple PHP callbacks, called *event handlers*, can be attached to an event. You can call [[trigger()]] to
26
 * raise an event. When an event is raised, the event handlers will be invoked automatically in the order they were
27
 * attached.
28
 *
29
 * To attach an event handler to an event, call [[on()]]:
30
 *
31
 * ```php
32
 * $post->on('update', function ($event) {
33
 *     // send email notification
34
 * });
35
 * ```
36
 *
37
 * In the above, an anonymous function is attached to the "update" event of the post. You may attach
38
 * the following types of event handlers:
39
 *
40
 * - anonymous function: `function ($event) { ... }`
41
 * - object method: `[$object, 'handleAdd']`
42
 * - static class method: `['Page', 'handleAdd']`
43
 * - global function: `'handleAdd'`
44
 *
45
 * The signature of an event handler should be like the following:
46
 *
47
 * ```php
48
 * function foo($event)
49
 * ```
50
 *
51
 * where `$event` is an [[Event]] object which includes parameters associated with the event.
52
 *
53
 * You can also attach a handler to an event when configuring a component with a configuration array.
54
 * The syntax is like the following:
55
 *
56
 * ```php
57
 * [
58
 *     'on add' => function ($event) { ... }
59
 * ]
60
 * ```
61
 *
62
 * where `on add` stands for attaching an event to the `add` event.
63
 *
64
 * Sometimes, you may want to associate extra data with an event handler when you attach it to an event
65
 * and then access it when the handler is invoked. You may do so by
66
 *
67
 * ```php
68
 * $post->on('update', function ($event) {
69
 *     // the data can be accessed via $event->data
70
 * }, $data);
71
 * ```
72
 *
73
 * A behavior is an instance of [[Behavior]] or its child class. A component can be attached with one or multiple
74
 * behaviors. When a behavior is attached to a component, its public properties and methods can be accessed via the
75
 * component directly, as if the component owns those properties and methods.
76
 *
77
 * To attach a behavior to a component, declare it in [[behaviors()]], or explicitly call [[attachBehavior]]. Behaviors
78
 * declared in [[behaviors()]] are automatically attached to the corresponding component.
79
 *
80
 * One can also attach a behavior to a component when configuring it with a configuration array. The syntax is like the
81
 * following:
82
 *
83
 * ```php
84
 * [
85
 *     'as tree' => [
86
 *         'class' => 'Tree',
87
 *     ],
88
 * ]
89
 * ```
90
 *
91
 * where `as tree` stands for attaching a behavior named `tree`, and the array will be passed to [[\Yii::createObject()]]
92
 * to create the behavior object.
93
 *
94
 * For more details and usage information on Component, see the [guide article on components](guide:concept-components).
95
 *
96
 * @property Behavior[] $behaviors List of behaviors attached to this component. This property is read-only.
97
 *
98
 * @author Qiang Xue <[email protected]>
99
 * @since 2.0
100
 */
101
class Component extends BaseObject
102
{
103
    /**
104
     * @var array the attached event handlers (event name => handlers)
105
     */
106
    private $_events = [];
107
    /**
108
     * @var array the event handlers attached for wildcard patterns (event name wildcard => handlers)
109
     * @since 2.0.14
110
     */
111
    private $_eventWildcards = [];
112
    /**
113
     * @var Behavior[]|null the attached behaviors (behavior name => behavior). This is `null` when not initialized.
114
     */
115
    private $_behaviors;
116
117
118
    /**
119
     * Returns the value of a component property.
120
     *
121
     * This method will check in the following order and act accordingly:
122
     *
123
     *  - a property defined by a getter: return the getter result
124
     *  - a property of a behavior: return the behavior property value
125
     *
126
     * Do not call this method directly as it is a PHP magic method that
127
     * will be implicitly called when executing `$value = $component->property;`.
128
     * @param string $name the property name
129
     * @return mixed the property value or the value of a behavior's property
130
     * @throws UnknownPropertyException if the property is not defined
131
     * @throws InvalidCallException if the property is write-only.
132
     * @see __set()
133
     */
134 1592
    public function __get($name)
135
    {
136 1592
        $getter = 'get' . $name;
137 1592
        if (method_exists($this, $getter)) {
138
            // read property, e.g. getName()
139 1587
            return $this->$getter();
140
        }
141
142
        // behavior property
143 6
        $this->ensureBehaviors();
144 6
        foreach ($this->_behaviors as $behavior) {
145 3
            if ($behavior->canGetProperty($name)) {
146 3
                return $behavior->$name;
147
            }
148
        }
149
150 3
        if (method_exists($this, 'set' . $name)) {
151 1
            throw new InvalidCallException('Getting write-only property: ' . get_class($this) . '::' . $name);
152
        }
153
154 2
        throw new UnknownPropertyException('Getting unknown property: ' . get_class($this) . '::' . $name);
155
    }
156
157
    /**
158
     * Sets the value of a component property.
159
     *
160
     * This method will check in the following order and act accordingly:
161
     *
162
     *  - a property defined by a setter: set the property value
163
     *  - an event in the format of "on xyz": attach the handler to the event "xyz"
164
     *  - a behavior in the format of "as xyz": attach the behavior named as "xyz"
165
     *  - a property of a behavior: set the behavior property value
166
     *
167
     * Do not call this method directly as it is a PHP magic method that
168
     * will be implicitly called when executing `$component->property = $value;`.
169
     * @param string $name the property name or the event name
170
     * @param mixed $value the property value
171
     * @throws UnknownPropertyException if the property is not defined
172
     * @throws InvalidCallException if the property is read-only.
173
     * @see __get()
174
     */
175 3643
    public function __set($name, $value)
176
    {
177 3643
        $setter = 'set' . $name;
178 3643
        if (method_exists($this, $setter)) {
179
            // set property
180 3642
            $this->$setter($value);
181
182 3642
            return;
183 23
        } elseif (strncmp($name, 'on ', 3) === 0) {
184
            // on event: attach event handler
185 12
            $this->on(trim(substr($name, 3)), $value);
0 ignored issues
show
Documentation introduced by
$value is of type *, but the function expects a callable.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
186
187 12
            return;
188 11
        } elseif (strncmp($name, 'as ', 3) === 0) {
189
            // as behavior: attach behavior
190 3
            $name = trim(substr($name, 3));
191 3
            $this->attachBehavior($name, $value instanceof Behavior ? $value : Yii::createObject($value));
0 ignored issues
show
Documentation introduced by
$value is of type *, but the function expects a callable.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
192
193 3
            return;
194
        }
195
196
        // behavior property
197 8
        $this->ensureBehaviors();
198 8
        foreach ($this->_behaviors as $behavior) {
199 3
            if ($behavior->canSetProperty($name)) {
200 3
                $behavior->$name = $value;
201 3
                return;
202
            }
203
        }
204
205 5
        if (method_exists($this, 'get' . $name)) {
206 4
            throw new InvalidCallException('Setting read-only property: ' . get_class($this) . '::' . $name);
207
        }
208
209 1
        throw new UnknownPropertyException('Setting unknown property: ' . get_class($this) . '::' . $name);
210
    }
211
212
    /**
213
     * Checks if a property is set, i.e. defined and not null.
214
     *
215
     * This method will check in the following order and act accordingly:
216
     *
217
     *  - a property defined by a setter: return whether the property is set
218
     *  - a property of a behavior: return whether the property is set
219
     *  - return `false` for non existing properties
220
     *
221
     * Do not call this method directly as it is a PHP magic method that
222
     * will be implicitly called when executing `isset($component->property)`.
223
     * @param string $name the property name or the event name
224
     * @return bool whether the named property is set
225
     * @see http://php.net/manual/en/function.isset.php
226
     */
227 26
    public function __isset($name)
228
    {
229 26
        $getter = 'get' . $name;
230 26
        if (method_exists($this, $getter)) {
231 25
            return $this->$getter() !== null;
232
        }
233
234
        // behavior property
235 2
        $this->ensureBehaviors();
236 2
        foreach ($this->_behaviors as $behavior) {
237 1
            if ($behavior->canGetProperty($name)) {
238 1
                return $behavior->$name !== null;
239
            }
240
        }
241
242 2
        return false;
243
    }
244
245
    /**
246
     * Sets a component property to be null.
247
     *
248
     * This method will check in the following order and act accordingly:
249
     *
250
     *  - a property defined by a setter: set the property value to be null
251
     *  - a property of a behavior: set the property value to be null
252
     *
253
     * Do not call this method directly as it is a PHP magic method that
254
     * will be implicitly called when executing `unset($component->property)`.
255
     * @param string $name the property name
256
     * @throws InvalidCallException if the property is read only.
257
     * @see http://php.net/manual/en/function.unset.php
258
     */
259 2
    public function __unset($name)
260
    {
261 2
        $setter = 'set' . $name;
262 2
        if (method_exists($this, $setter)) {
263 1
            $this->$setter(null);
264 1
            return;
265
        }
266
267
        // behavior property
268 2
        $this->ensureBehaviors();
269 2
        foreach ($this->_behaviors as $behavior) {
270 1
            if ($behavior->canSetProperty($name)) {
271 1
                $behavior->$name = null;
272
                return;
273
            }
274
        }
275
276 1
        throw new InvalidCallException('Unsetting an unknown or read-only property: ' . get_class($this) . '::' . $name);
277
    }
278
279
    /**
280
     * Calls the named method which is not a class method.
281
     *
282
     * This method will check if any attached behavior has
283
     * the named method and will execute it if available.
284
     *
285
     * Do not call this method directly as it is a PHP magic method that
286
     * will be implicitly called when an unknown method is being invoked.
287
     * @param string $name the method name
288
     * @param array $params method parameters
289
     * @return mixed the method return value
290
     * @throws UnknownMethodException when calling unknown method
291
     */
292 14
    public function __call($name, $params)
293
    {
294 14
        $this->ensureBehaviors();
295 14
        foreach ($this->_behaviors as $object) {
296 13
            if ($object->hasMethod($name)) {
297 13
                return call_user_func_array([$object, $name], $params);
298
            }
299
        }
300 3
        throw new UnknownMethodException('Calling unknown method: ' . get_class($this) . "::$name()");
301
    }
302
303
    /**
304
     * This method is called after the object is created by cloning an existing one.
305
     * It removes all behaviors because they are attached to the old object.
306
     */
307 37
    public function __clone()
308
    {
309 37
        $this->_events = [];
310 37
        $this->_eventWildcards = [];
311 37
        $this->_behaviors = null;
312 37
    }
313
314
    /**
315
     * Returns a value indicating whether a property is defined for this component.
316
     *
317
     * A property is defined if:
318
     *
319
     * - the class has a getter or setter method associated with the specified name
320
     *   (in this case, property name is case-insensitive);
321
     * - the class has a member variable with the specified name (when `$checkVars` is true);
322
     * - an attached behavior has a property of the given name (when `$checkBehaviors` is true).
323
     *
324
     * @param string $name the property name
325
     * @param bool $checkVars whether to treat member variables as properties
326
     * @param bool $checkBehaviors whether to treat behaviors' properties as properties of this component
327
     * @return bool whether the property is defined
328
     * @see canGetProperty()
329
     * @see canSetProperty()
330
     */
331 5
    public function hasProperty($name, $checkVars = true, $checkBehaviors = true)
332
    {
333 5
        return $this->canGetProperty($name, $checkVars, $checkBehaviors) || $this->canSetProperty($name, false, $checkBehaviors);
334
    }
335
336
    /**
337
     * Returns a value indicating whether a property can be read.
338
     *
339
     * A property can be read if:
340
     *
341
     * - the class has a getter method associated with the specified name
342
     *   (in this case, property name is case-insensitive);
343
     * - the class has a member variable with the specified name (when `$checkVars` is true);
344
     * - an attached behavior has a readable property of the given name (when `$checkBehaviors` is true).
345
     *
346
     * @param string $name the property name
347
     * @param bool $checkVars whether to treat member variables as properties
348
     * @param bool $checkBehaviors whether to treat behaviors' properties as properties of this component
349
     * @return bool whether the property can be read
350
     * @see canSetProperty()
351
     */
352 7
    public function canGetProperty($name, $checkVars = true, $checkBehaviors = true)
353
    {
354 7
        if (method_exists($this, 'get' . $name) || $checkVars && property_exists($this, $name)) {
355 5
            return true;
356 7
        } elseif ($checkBehaviors) {
357 7
            $this->ensureBehaviors();
358 7
            foreach ($this->_behaviors as $behavior) {
359 1
                if ($behavior->canGetProperty($name, $checkVars)) {
360 1
                    return true;
361
                }
362
            }
363
        }
364
365 7
        return false;
366
    }
367
368
    /**
369
     * Returns a value indicating whether a property can be set.
370
     *
371
     * A property can be written if:
372
     *
373
     * - the class has a setter method associated with the specified name
374
     *   (in this case, property name is case-insensitive);
375
     * - the class has a member variable with the specified name (when `$checkVars` is true);
376
     * - an attached behavior has a writable property of the given name (when `$checkBehaviors` is true).
377
     *
378
     * @param string $name the property name
379
     * @param bool $checkVars whether to treat member variables as properties
380
     * @param bool $checkBehaviors whether to treat behaviors' properties as properties of this component
381
     * @return bool whether the property can be written
382
     * @see canGetProperty()
383
     */
384 18
    public function canSetProperty($name, $checkVars = true, $checkBehaviors = true)
385
    {
386 18
        if (method_exists($this, 'set' . $name) || $checkVars && property_exists($this, $name)) {
387 12
            return true;
388 7
        } elseif ($checkBehaviors) {
389 7
            $this->ensureBehaviors();
390 7
            foreach ($this->_behaviors as $behavior) {
391 1
                if ($behavior->canSetProperty($name, $checkVars)) {
392 1
                    return true;
393
                }
394
            }
395
        }
396
397 7
        return false;
398
    }
399
400
    /**
401
     * Returns a value indicating whether a method is defined.
402
     *
403
     * A method is defined if:
404
     *
405
     * - the class has a method with the specified name
406
     * - an attached behavior has a method with the given name (when `$checkBehaviors` is true).
407
     *
408
     * @param string $name the property name
409
     * @param bool $checkBehaviors whether to treat behaviors' methods as methods of this component
410
     * @return bool whether the method is defined
411
     */
412 100
    public function hasMethod($name, $checkBehaviors = true)
413
    {
414 100
        if (method_exists($this, $name)) {
415 40
            return true;
416 86
        } elseif ($checkBehaviors) {
417 86
            $this->ensureBehaviors();
418 86
            foreach ($this->_behaviors as $behavior) {
419 3
                if ($behavior->hasMethod($name)) {
420 3
                    return true;
421
                }
422
            }
423
        }
424
425 86
        return false;
426
    }
427
428
    /**
429
     * Returns a list of behaviors that this component should behave as.
430
     *
431
     * Child classes may override this method to specify the behaviors they want to behave as.
432
     *
433
     * The return value of this method should be an array of behavior objects or configurations
434
     * indexed by behavior names. A behavior configuration can be either a string specifying
435
     * the behavior class or an array of the following structure:
436
     *
437
     * ```php
438
     * 'behaviorName' => [
439
     *     'class' => 'BehaviorClass',
440
     *     'property1' => 'value1',
441
     *     'property2' => 'value2',
442
     * ]
443
     * ```
444
     *
445
     * Note that a behavior class must extend from [[Behavior]]. Behaviors can be attached using a name or anonymously.
446
     * When a name is used as the array key, using this name, the behavior can later be retrieved using [[getBehavior()]]
447
     * or be detached using [[detachBehavior()]]. Anonymous behaviors can not be retrieved or detached.
448
     *
449
     * Behaviors declared in this method will be attached to the component automatically (on demand).
450
     *
451
     * @return array the behavior configurations.
452
     */
453 2020
    public function behaviors()
454
    {
455 2020
        return [];
456
    }
457
458
    /**
459
     * Returns a value indicating whether there is any handler attached to the named event.
460
     * @param string $name the event name
461
     * @return bool whether there is any handler attached to the event.
462
     */
463 98
    public function hasEventHandlers($name)
464
    {
465 98
        $this->ensureBehaviors();
466
467 98
        foreach ($this->_eventWildcards as $wildcard => $handlers) {
468 4
            if (!empty($handlers) && StringHelper::matchWildcard($wildcard, $name)) {
469 4
                return true;
470
            }
471
        }
472
473 98
        return !empty($this->_events[$name]) || Event::hasHandlers($this, $name);
474
    }
475
476
    /**
477
     * Attaches an event handler to an event.
478
     *
479
     * The event handler must be a valid PHP callback. The following are
480
     * some examples:
481
     *
482
     * ```
483
     * function ($event) { ... }         // anonymous function
484
     * [$object, 'handleClick']          // $object->handleClick()
485
     * ['Page', 'handleClick']           // Page::handleClick()
486
     * 'handleClick'                     // global function handleClick()
487
     * ```
488
     *
489
     * The event handler must be defined with the following signature,
490
     *
491
     * ```
492
     * function ($event)
493
     * ```
494
     *
495
     * where `$event` is an [[Event]] object which includes parameters associated with the event.
496
     *
497
     * Since 2.0.14 you can specify event name as a wildcard pattern:
498
     *
499
     * ```php
500
     * $component->on('event.group.*', function ($event) {
501
     *     Yii::trace($event->name . ' is triggered.');
502
     * });
503
     * ```
504
     *
505
     * @param string $name the event name
506
     * @param callable $handler the event handler
507
     * @param mixed $data the data to be passed to the event handler when the event is triggered.
508
     * When the event handler is invoked, this data can be accessed via [[Event::data]].
509
     * @param bool $append whether to append new event handler to the end of the existing
510
     * handler list. If false, the new handler will be inserted at the beginning of the existing
511
     * handler list.
512
     * @see off()
513
     */
514 148
    public function on($name, $handler, $data = null, $append = true)
515
    {
516 148
        $this->ensureBehaviors();
517
518 148
        if (strpos($name, '*') !== false) {
519 5
            if ($append || empty($this->_eventWildcards[$name])) {
520 5
                $this->_eventWildcards[$name][] = [$handler, $data];
521
            } else {
522
                array_unshift($this->_eventWildcards[$name], [$handler, $data]);
523
            }
524 5
            return;
525
        }
526
527 145
        if ($append || empty($this->_events[$name])) {
528 145
            $this->_events[$name][] = [$handler, $data];
529
        } else {
530 4
            array_unshift($this->_events[$name], [$handler, $data]);
531
        }
532 145
    }
533
534
    /**
535
     * Detaches an existing event handler from this component.
536
     *
537
     * This method is the opposite of [[on()]].
538
     *
539
     * Note: in case wildcard pattern is passed for event name, only the handlers registered with this
540
     * wildcard will be removed, while handlers registered with plain names matching this wildcard will remain.
541
     *
542
     * @param string $name event name
543
     * @param callable $handler the event handler to be removed.
544
     * If it is null, all handlers attached to the named event will be removed.
545
     * @return bool if a handler is found and detached
546
     * @see on()
547
     */
548 66
    public function off($name, $handler = null)
549
    {
550 66
        $this->ensureBehaviors();
551 66
        if (empty($this->_events[$name]) && empty($this->_eventWildcards[$name])) {
552 5
            return false;
553
        }
554 65
        if ($handler === null) {
555 2
            unset($this->_events[$name], $this->_eventWildcards[$name]);
556 2
            return true;
557
        }
558
559 65
        $removed = false;
560
        // plain event names
561 65
        if (isset($this->_events[$name])) {
562 64
            foreach ($this->_events[$name] as $i => $event) {
563 64
                if ($event[0] === $handler) {
564 64
                    unset($this->_events[$name][$i]);
565 64
                    $removed = true;
566
                }
567
            }
568 64
            if ($removed) {
569 64
                $this->_events[$name] = array_values($this->_events[$name]);
570 64
                return $removed;
571
            }
572
        }
573
574
        // wildcard event names
575 2
        if (isset($this->_eventWildcards[$name])) {
576 1
            foreach ($this->_eventWildcards[$name] as $i => $event) {
577 1
                if ($event[0] === $handler) {
578 1
                    unset($this->_eventWildcards[$name][$i]);
579 1
                    $removed = true;
580
                }
581
            }
582 1
            if ($removed) {
583 1
                $this->_eventWildcards[$name] = array_values($this->_eventWildcards[$name]);
584
                // remove empty wildcards to save future redundant regex checks:
585 1
                if (empty($this->_eventWildcards[$name])) {
586 1
                    unset($this->_eventWildcards[$name]);
587
                }
588
            }
589
        }
590
591 2
        return $removed;
592
    }
593
594
    /**
595
     * Triggers an event.
596
     * This method represents the happening of an event. It invokes
597
     * all attached handlers for the event including class-level handlers.
598
     * @param string $name the event name
599
     * @param Event $event the event parameter. If not set, a default [[Event]] object will be created.
600
     */
601 1942
    public function trigger($name, Event $event = null)
602
    {
603 1942
        $this->ensureBehaviors();
604
605 1942
        $eventHandlers = [];
606 1942
        foreach ($this->_eventWildcards as $wildcard => $handlers) {
607 1
            if (StringHelper::matchWildcard($wildcard, $name)) {
608 1
                $eventHandlers = array_merge($eventHandlers, $handlers);
609
            }
610
        }
611
612 1942
        if (!empty($this->_events[$name])) {
613 136
            $eventHandlers = array_merge($eventHandlers, $this->_events[$name]);
614
        }
615
616 1942
        if (!empty($eventHandlers)) {
617 137
            if ($event === null) {
618 27
                $event = new Event();
619
            }
620 137
            if ($event->sender === null) {
621 137
                $event->sender = $this;
622
            }
623 137
            $event->handled = false;
624 137
            $event->name = $name;
625 137
            foreach ($eventHandlers as $handler) {
626 137
                $event->data = $handler[1];
627 137
                call_user_func($handler[0], $event);
628
                // stop further handling if the event is handled
629 135
                if ($event->handled) {
630 135
                    return;
631
                }
632
            }
633
        }
634
635
        // invoke class-level attached handlers
636 1941
        Event::trigger($this, $name, $event);
637 1941
    }
638
639
    /**
640
     * Returns the named behavior object.
641
     * @param string $name the behavior name
642
     * @return null|Behavior the behavior object, or null if the behavior does not exist
643
     */
644 29
    public function getBehavior($name)
645
    {
646 29
        $this->ensureBehaviors();
647 29
        return isset($this->_behaviors[$name]) ? $this->_behaviors[$name] : null;
648
    }
649
650
    /**
651
     * Returns all behaviors attached to this component.
652
     * @return Behavior[] list of behaviors attached to this component
653
     */
654 1
    public function getBehaviors()
655
    {
656 1
        $this->ensureBehaviors();
657 1
        return $this->_behaviors;
658
    }
659
660
    /**
661
     * Attaches a behavior to this component.
662
     * This method will create the behavior object based on the given
663
     * configuration. After that, the behavior object will be attached to
664
     * this component by calling the [[Behavior::attach()]] method.
665
     * @param string $name the name of the behavior.
666
     * @param string|array|Behavior $behavior the behavior configuration. This can be one of the following:
667
     *
668
     *  - a [[Behavior]] object
669
     *  - a string specifying the behavior class
670
     *  - an object configuration array that will be passed to [[Yii::createObject()]] to create the behavior object.
671
     *
672
     * @return Behavior the behavior object
673
     * @see detachBehavior()
674
     */
675 13
    public function attachBehavior($name, $behavior)
676
    {
677 13
        $this->ensureBehaviors();
678 13
        return $this->attachBehaviorInternal($name, $behavior);
679
    }
680
681
    /**
682
     * Attaches a list of behaviors to the component.
683
     * Each behavior is indexed by its name and should be a [[Behavior]] object,
684
     * a string specifying the behavior class, or an configuration array for creating the behavior.
685
     * @param array $behaviors list of behaviors to be attached to the component
686
     * @see attachBehavior()
687
     */
688 3
    public function attachBehaviors($behaviors)
689
    {
690 3
        $this->ensureBehaviors();
691 3
        foreach ($behaviors as $name => $behavior) {
692 3
            $this->attachBehaviorInternal($name, $behavior);
693
        }
694 3
    }
695
696
    /**
697
     * Detaches a behavior from the component.
698
     * The behavior's [[Behavior::detach()]] method will be invoked.
699
     * @param string $name the behavior's name.
700
     * @return null|Behavior the detached behavior. Null if the behavior does not exist.
701
     */
702 4
    public function detachBehavior($name)
703
    {
704 4
        $this->ensureBehaviors();
705 4
        if (isset($this->_behaviors[$name])) {
706 4
            $behavior = $this->_behaviors[$name];
707 4
            unset($this->_behaviors[$name]);
708 4
            $behavior->detach();
709 4
            return $behavior;
710
        }
711
712 1
        return null;
713
    }
714
715
    /**
716
     * Detaches all behaviors from the component.
717
     */
718 1
    public function detachBehaviors()
719
    {
720 1
        $this->ensureBehaviors();
721 1
        foreach ($this->_behaviors as $name => $behavior) {
722 1
            $this->detachBehavior($name);
723
        }
724 1
    }
725
726
    /**
727
     * Makes sure that the behaviors declared in [[behaviors()]] are attached to this component.
728
     */
729 2021
    public function ensureBehaviors()
730
    {
731 2021
        if ($this->_behaviors === null) {
732 2021
            $this->_behaviors = [];
733 2021
            foreach ($this->behaviors() as $name => $behavior) {
734 116
                $this->attachBehaviorInternal($name, $behavior);
735
            }
736
        }
737 2021
    }
738
739
    /**
740
     * Attaches a behavior to this component.
741
     * @param string|int $name the name of the behavior. If this is an integer, it means the behavior
742
     * is an anonymous one. Otherwise, the behavior is a named one and any existing behavior with the same name
743
     * will be detached first.
744
     * @param string|array|Behavior $behavior the behavior to be attached
745
     * @return Behavior the attached behavior.
746
     */
747 130
    private function attachBehaviorInternal($name, $behavior)
748
    {
749 130
        if (!($behavior instanceof Behavior)) {
750 116
            $behavior = Yii::createObject($behavior);
751
        }
752 130
        if (is_int($name)) {
753 9
            $behavior->attach($this);
754 9
            $this->_behaviors[] = $behavior;
755
        } else {
756 121
            if (isset($this->_behaviors[$name])) {
757 3
                $this->_behaviors[$name]->detach();
758
            }
759 121
            $behavior->attach($this);
760 121
            $this->_behaviors[$name] = $behavior;
761
        }
762
763 130
        return $behavior;
764
    }
765
}
766