Completed
Pull Request — master (#12929)
by Robert
48:37 queued 12:35
created

ComponentTrait::__isset()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 16
c 0
b 0
f 0
rs 9.2
cc 4
eloc 10
nc 4
nop 1
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
12
/**
13
 *
14
 * @author Robert Korulczyk <[email protected]>
15
 * @since 2.0.11
16
 */
17
trait ComponentTrait
18
{
19
    /**
20
     * @var array the attached event handlers (event name => handlers)
21
     */
22
    private $_events = [];
23
    /**
24
     * @var Behavior[]|null the attached behaviors (behavior name => behavior). This is `null` when not initialized.
25
     */
26
    private $_behaviors;
27
28
29
    /**
30
     * Returns the value of a component property.
31
     * This method will check in the following order and act accordingly:
32
     *
33
     *  - a property defined by a getter: return the getter result
34
     *  - a property of a behavior: return the behavior property value
35
     *
36
     * Do not call this method directly as it is a PHP magic method that
37
     * will be implicitly called when executing `$value = $component->property;`.
38
     * @param string $name the property name
39
     * @return mixed the property value or the value of a behavior's property
40
     * @throws UnknownPropertyException if the property is not defined
41
     * @throws InvalidCallException if the property is write-only.
42
     * @see __set()
43
     */
44
    public function __get($name)
45
    {
46
        $getter = 'get' . $name;
47
        if (method_exists($this, $getter)) {
48
            // read property, e.g. getName()
49
            return $this->$getter();
50
        } else {
51
            // behavior property
52
            $this->ensureBehaviors();
53
            foreach ($this->_behaviors as $behavior) {
54
                if ($behavior->canGetProperty($name)) {
55
                    return $behavior->$name;
56
                }
57
            }
58
        }
59
        if (method_exists($this, 'set' . $name)) {
60
            throw new InvalidCallException('Getting write-only property: ' . get_class($this) . '::' . $name);
61
        } else {
62
            throw new UnknownPropertyException('Getting unknown property: ' . get_class($this) . '::' . $name);
63
        }
64
    }
65
66
    /**
67
     * Sets the value of a component property.
68
     * This method will check in the following order and act accordingly:
69
     *
70
     *  - a property defined by a setter: set the property value
71
     *  - an event in the format of "on xyz": attach the handler to the event "xyz"
72
     *  - a behavior in the format of "as xyz": attach the behavior named as "xyz"
73
     *  - a property of a behavior: set the behavior property value
74
     *
75
     * Do not call this method directly as it is a PHP magic method that
76
     * will be implicitly called when executing `$component->property = $value;`.
77
     * @param string $name the property name or the event name
78
     * @param mixed $value the property value
79
     * @throws UnknownPropertyException if the property is not defined
80
     * @throws InvalidCallException if the property is read-only.
81
     * @see __get()
82
     */
83
    public function __set($name, $value)
84
    {
85
        $setter = 'set' . $name;
86
        if (method_exists($this, $setter)) {
87
            // set property
88
            $this->$setter($value);
89
90
            return;
91
        } elseif (strncmp($name, 'on ', 3) === 0) {
92
            // on event: attach event handler
93
            $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...
94
95
            return;
96
        } elseif (strncmp($name, 'as ', 3) === 0) {
97
            // as behavior: attach behavior
98
            $name = trim(substr($name, 3));
99
            $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...
100
101
            return;
102
        } else {
103
            // behavior property
104
            $this->ensureBehaviors();
105
            foreach ($this->_behaviors as $behavior) {
106
                if ($behavior->canSetProperty($name)) {
107
                    $behavior->$name = $value;
108
109
                    return;
110
                }
111
            }
112
        }
113
        if (method_exists($this, 'get' . $name)) {
114
            throw new InvalidCallException('Setting read-only property: ' . get_class($this) . '::' . $name);
115
        } else {
116
            throw new UnknownPropertyException('Setting unknown property: ' . get_class($this) . '::' . $name);
117
        }
118
    }
119
120
    /**
121
     * Checks if a property is set, i.e. defined and not null.
122
     * This method will check in the following order and act accordingly:
123
     *
124
     *  - a property defined by a setter: return whether the property is set
125
     *  - a property of a behavior: return whether the property is set
126
     *  - return `false` for non existing properties
127
     *
128
     * Do not call this method directly as it is a PHP magic method that
129
     * will be implicitly called when executing `isset($component->property)`.
130
     * @param string $name the property name or the event name
131
     * @return boolean whether the named property is set
132
     * @see http://php.net/manual/en/function.isset.php
133
     */
134
    public function __isset($name)
135
    {
136
        $getter = 'get' . $name;
137
        if (method_exists($this, $getter)) {
138
            return $this->$getter() !== null;
139
        } else {
140
            // behavior property
141
            $this->ensureBehaviors();
142
            foreach ($this->_behaviors as $behavior) {
143
                if ($behavior->canGetProperty($name)) {
144
                    return $behavior->$name !== null;
145
                }
146
            }
147
        }
148
        return false;
149
    }
150
151
    /**
152
     * Sets a component property to be null.
153
     * This method will check in the following order and act accordingly:
154
     *
155
     *  - a property defined by a setter: set the property value to be null
156
     *  - a property of a behavior: set the property value to be null
157
     *
158
     * Do not call this method directly as it is a PHP magic method that
159
     * will be implicitly called when executing `unset($component->property)`.
160
     * @param string $name the property name
161
     * @throws InvalidCallException if the property is read only.
162
     * @see http://php.net/manual/en/function.unset.php
163
     */
164
    public function __unset($name)
165
    {
166
        $setter = 'set' . $name;
167
        if (method_exists($this, $setter)) {
168
            $this->$setter(null);
169
            return;
170
        } else {
171
            // behavior property
172
            $this->ensureBehaviors();
173
            foreach ($this->_behaviors as $behavior) {
174
                if ($behavior->canSetProperty($name)) {
175
                    $behavior->$name = null;
176
                    return;
177
                }
178
            }
179
        }
180
        throw new InvalidCallException('Unsetting an unknown or read-only property: ' . get_class($this) . '::' . $name);
181
    }
182
183
    /**
184
     * Calls the named method which is not a class method.
185
     *
186
     * This method will check if any attached behavior has
187
     * the named method and will execute it if available.
188
     *
189
     * Do not call this method directly as it is a PHP magic method that
190
     * will be implicitly called when an unknown method is being invoked.
191
     * @param string $name the method name
192
     * @param array $params method parameters
193
     * @return mixed the method return value
194
     * @throws UnknownMethodException when calling unknown method
195
     */
196
    public function __call($name, $params)
197
    {
198
        $this->ensureBehaviors();
199
        foreach ($this->_behaviors as $object) {
200
            if ($object->hasMethod($name)) {
201
                return call_user_func_array([$object, $name], $params);
202
            }
203
        }
204
        throw new UnknownMethodException('Calling unknown method: ' . get_class($this) . "::$name()");
205
    }
206
207
    /**
208
     * This method is called after the object is created by cloning an existing one.
209
     * It removes all behaviors because they are attached to the old object.
210
     */
211
    public function __clone()
212
    {
213
        $this->_events = [];
214
        $this->_behaviors = null;
215
    }
216
217
    /**
218
     * Returns a value indicating whether a property is defined for this component.
219
     * A property is defined if:
220
     *
221
     * - the class has a getter or setter method associated with the specified name
222
     *   (in this case, property name is case-insensitive);
223
     * - the class has a member variable with the specified name (when `$checkVars` is true);
224
     * - an attached behavior has a property of the given name (when `$checkBehaviors` is true).
225
     *
226
     * @param string $name the property name
227
     * @param boolean $checkVars whether to treat member variables as properties
228
     * @param boolean $checkBehaviors whether to treat behaviors' properties as properties of this component
229
     * @return boolean whether the property is defined
230
     * @see canGetProperty()
231
     * @see canSetProperty()
232
     */
233
    public function hasProperty($name, $checkVars = true, $checkBehaviors = true)
234
    {
235
        return $this->canGetProperty($name, $checkVars, $checkBehaviors) || $this->canSetProperty($name, false, $checkBehaviors);
236
    }
237
238
    /**
239
     * Returns a value indicating whether a property can be read.
240
     * A property can be read if:
241
     *
242
     * - the class has a getter method associated with the specified name
243
     *   (in this case, property name is case-insensitive);
244
     * - the class has a member variable with the specified name (when `$checkVars` is true);
245
     * - an attached behavior has a readable property of the given name (when `$checkBehaviors` is true).
246
     *
247
     * @param string $name the property name
248
     * @param boolean $checkVars whether to treat member variables as properties
249
     * @param boolean $checkBehaviors whether to treat behaviors' properties as properties of this component
250
     * @return boolean whether the property can be read
251
     * @see canSetProperty()
252
     */
253
    public function canGetProperty($name, $checkVars = true, $checkBehaviors = true)
254
    {
255
        if (method_exists($this, 'get' . $name) || $checkVars && property_exists($this, $name)) {
256
            return true;
257
        } elseif ($checkBehaviors) {
258
            $this->ensureBehaviors();
259
            foreach ($this->_behaviors as $behavior) {
260
                if ($behavior->canGetProperty($name, $checkVars)) {
261
                    return true;
262
                }
263
            }
264
        }
265
        return false;
266
    }
267
268
    /**
269
     * Returns a value indicating whether a property can be set.
270
     * A property can be written if:
271
     *
272
     * - the class has a setter method associated with the specified name
273
     *   (in this case, property name is case-insensitive);
274
     * - the class has a member variable with the specified name (when `$checkVars` is true);
275
     * - an attached behavior has a writable property of the given name (when `$checkBehaviors` is true).
276
     *
277
     * @param string $name the property name
278
     * @param boolean $checkVars whether to treat member variables as properties
279
     * @param boolean $checkBehaviors whether to treat behaviors' properties as properties of this component
280
     * @return boolean whether the property can be written
281
     * @see canGetProperty()
282
     */
283
    public function canSetProperty($name, $checkVars = true, $checkBehaviors = true)
284
    {
285
        if (method_exists($this, 'set' . $name) || $checkVars && property_exists($this, $name)) {
286
            return true;
287
        } elseif ($checkBehaviors) {
288
            $this->ensureBehaviors();
289
            foreach ($this->_behaviors as $behavior) {
290
                if ($behavior->canSetProperty($name, $checkVars)) {
291
                    return true;
292
                }
293
            }
294
        }
295
        return false;
296
    }
297
298
    /**
299
     * Returns a value indicating whether a method is defined.
300
     * A method is defined if:
301
     *
302
     * - the class has a method with the specified name
303
     * - an attached behavior has a method with the given name (when `$checkBehaviors` is true).
304
     *
305
     * @param string $name the property name
306
     * @param boolean $checkBehaviors whether to treat behaviors' methods as methods of this component
307
     * @return boolean whether the property is defined
308
     */
309
    public function hasMethod($name, $checkBehaviors = true)
310
    {
311
        if (method_exists($this, $name)) {
312
            return true;
313
        } elseif ($checkBehaviors) {
314
            $this->ensureBehaviors();
315
            foreach ($this->_behaviors as $behavior) {
316
                if ($behavior->hasMethod($name)) {
317
                    return true;
318
                }
319
            }
320
        }
321
        return false;
322
    }
323
324
    /**
325
     * Returns a list of behaviors that this component should behave as.
326
     *
327
     * Child classes may override this method to specify the behaviors they want to behave as.
328
     *
329
     * The return value of this method should be an array of behavior objects or configurations
330
     * indexed by behavior names. A behavior configuration can be either a string specifying
331
     * the behavior class or an array of the following structure:
332
     *
333
     * ```php
334
     * 'behaviorName' => [
335
     *     'class' => 'BehaviorClass',
336
     *     'property1' => 'value1',
337
     *     'property2' => 'value2',
338
     * ]
339
     * ```
340
     *
341
     * Note that a behavior class must extend from [[Behavior]]. Behaviors can be attached using a name or anonymously.
342
     * When a name is used as the array key, using this name, the behavior can later be retrieved using [[getBehavior()]]
343
     * or be detached using [[detachBehavior()]]. Anonymous behaviors can not be retrieved or detached.
344
     *
345
     * Behaviors declared in this method will be attached to the component automatically (on demand).
346
     *
347
     * @return array the behavior configurations.
348
     */
349
    public function behaviors()
350
    {
351
        return [];
352
    }
353
354
    /**
355
     * Returns a value indicating whether there is any handler attached to the named event.
356
     * @param string $name the event name
357
     * @return boolean whether there is any handler attached to the event.
358
     */
359
    public function hasEventHandlers($name)
360
    {
361
        $this->ensureBehaviors();
362
        return !empty($this->_events[$name]) || Event::hasHandlers($this, $name);
363
    }
364
365
    /**
366
     * Attaches an event handler to an event.
367
     *
368
     * The event handler must be a valid PHP callback. The following are
369
     * some examples:
370
     *
371
     * ```
372
     * function ($event) { ... }         // anonymous function
373
     * [$object, 'handleClick']          // $object->handleClick()
374
     * ['Page', 'handleClick']           // Page::handleClick()
375
     * 'handleClick'                     // global function handleClick()
376
     * ```
377
     *
378
     * The event handler must be defined with the following signature,
379
     *
380
     * ```
381
     * function ($event)
382
     * ```
383
     *
384
     * where `$event` is an [[Event]] object which includes parameters associated with the event.
385
     *
386
     * @param string $name the event name
387
     * @param callable $handler the event handler
388
     * @param mixed $data the data to be passed to the event handler when the event is triggered.
389
     * When the event handler is invoked, this data can be accessed via [[Event::data]].
390
     * @param boolean $append whether to append new event handler to the end of the existing
391
     * handler list. If false, the new handler will be inserted at the beginning of the existing
392
     * handler list.
393
     * @see off()
394
     */
395
    public function on($name, $handler, $data = null, $append = true)
396
    {
397
        $this->ensureBehaviors();
398
        if ($append || empty($this->_events[$name])) {
399
            $this->_events[$name][] = [$handler, $data];
400
        } else {
401
            array_unshift($this->_events[$name], [$handler, $data]);
402
        }
403
    }
404
405
    /**
406
     * Detaches an existing event handler from this component.
407
     * This method is the opposite of [[on()]].
408
     * @param string $name event name
409
     * @param callable $handler the event handler to be removed.
410
     * If it is null, all handlers attached to the named event will be removed.
411
     * @return boolean if a handler is found and detached
412
     * @see on()
413
     */
414
    public function off($name, $handler = null)
415
    {
416
        $this->ensureBehaviors();
417
        if (empty($this->_events[$name])) {
418
            return false;
419
        }
420
        if ($handler === null) {
421
            unset($this->_events[$name]);
422
            return true;
423
        } else {
424
            $removed = false;
425
            foreach ($this->_events[$name] as $i => $event) {
426
                if ($event[0] === $handler) {
427
                    unset($this->_events[$name][$i]);
428
                    $removed = true;
429
                }
430
            }
431
            if ($removed) {
432
                $this->_events[$name] = array_values($this->_events[$name]);
433
            }
434
            return $removed;
435
        }
436
    }
437
438
    /**
439
     * Triggers an event.
440
     * This method represents the happening of an event. It invokes
441
     * all attached handlers for the event including class-level handlers.
442
     * @param string $name the event name
443
     * @param Event $event the event parameter. If not set, a default [[Event]] object will be created.
444
     */
445
    public function trigger($name, Event $event = null)
446
    {
447
        $this->ensureBehaviors();
448
        if (!empty($this->_events[$name])) {
449
            if ($event === null) {
450
                $event = new Event;
451
            }
452
            if ($event->sender === null) {
453
                $event->sender = $this;
0 ignored issues
show
Documentation Bug introduced by
It seems like $this of type this<yii\base\ComponentTrait> is incompatible with the declared type object of property $sender.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
454
            }
455
            $event->handled = false;
456
            $event->name = $name;
457
            foreach ($this->_events[$name] as $handler) {
458
                $event->data = $handler[1];
459
                call_user_func($handler[0], $event);
460
                // stop further handling if the event is handled
461
                if ($event->handled) {
462
                    return;
463
                }
464
            }
465
        }
466
        // invoke class-level attached handlers
467
        Event::trigger($this, $name, $event);
468
    }
469
470
    /**
471
     * Returns the named behavior object.
472
     * @param string $name the behavior name
473
     * @return null|Behavior the behavior object, or null if the behavior does not exist
474
     */
475
    public function getBehavior($name)
476
    {
477
        $this->ensureBehaviors();
478
        return isset($this->_behaviors[$name]) ? $this->_behaviors[$name] : null;
479
    }
480
481
    /**
482
     * Returns all behaviors attached to this component.
483
     * @return Behavior[] list of behaviors attached to this component
484
     */
485
    public function getBehaviors()
486
    {
487
        $this->ensureBehaviors();
488
        return $this->_behaviors;
489
    }
490
491
    /**
492
     * Attaches a behavior to this component.
493
     * This method will create the behavior object based on the given
494
     * configuration. After that, the behavior object will be attached to
495
     * this component by calling the [[Behavior::attach()]] method.
496
     * @param string $name the name of the behavior.
497
     * @param string|array|Behavior $behavior the behavior configuration. This can be one of the following:
498
     *
499
     *  - a [[Behavior]] object
500
     *  - a string specifying the behavior class
501
     *  - an object configuration array that will be passed to [[Yii::createObject()]] to create the behavior object.
502
     *
503
     * @return Behavior the behavior object
504
     * @see detachBehavior()
505
     */
506
    public function attachBehavior($name, $behavior)
507
    {
508
        $this->ensureBehaviors();
509
        return $this->attachBehaviorInternal($name, $behavior);
510
    }
511
512
    /**
513
     * Attaches a list of behaviors to the component.
514
     * Each behavior is indexed by its name and should be a [[Behavior]] object,
515
     * a string specifying the behavior class, or an configuration array for creating the behavior.
516
     * @param array $behaviors list of behaviors to be attached to the component
517
     * @see attachBehavior()
518
     */
519
    public function attachBehaviors($behaviors)
520
    {
521
        $this->ensureBehaviors();
522
        foreach ($behaviors as $name => $behavior) {
523
            $this->attachBehaviorInternal($name, $behavior);
524
        }
525
    }
526
527
    /**
528
     * Detaches a behavior from the component.
529
     * The behavior's [[Behavior::detach()]] method will be invoked.
530
     * @param string $name the behavior's name.
531
     * @return null|Behavior the detached behavior. Null if the behavior does not exist.
532
     */
533
    public function detachBehavior($name)
534
    {
535
        $this->ensureBehaviors();
536
        if (isset($this->_behaviors[$name])) {
537
            $behavior = $this->_behaviors[$name];
538
            unset($this->_behaviors[$name]);
539
            $behavior->detach();
540
            return $behavior;
541
        } else {
542
            return null;
543
        }
544
    }
545
546
    /**
547
     * Detaches all behaviors from the component.
548
     */
549
    public function detachBehaviors()
550
    {
551
        $this->ensureBehaviors();
552
        foreach ($this->_behaviors as $name => $behavior) {
553
            $this->detachBehavior($name);
554
        }
555
    }
556
557
    /**
558
     * Makes sure that the behaviors declared in [[behaviors()]] are attached to this component.
559
     */
560
    public function ensureBehaviors()
561
    {
562
        if ($this->_behaviors === null) {
563
            $this->_behaviors = [];
564
            foreach ($this->behaviors() as $name => $behavior) {
565
                $this->attachBehaviorInternal($name, $behavior);
566
            }
567
        }
568
    }
569
570
    /**
571
     * Attaches a behavior to this component.
572
     * @param string|integer $name the name of the behavior. If this is an integer, it means the behavior
573
     * is an anonymous one. Otherwise, the behavior is a named one and any existing behavior with the same name
574
     * will be detached first.
575
     * @param string|array|Behavior $behavior the behavior to be attached
576
     * @return Behavior the attached behavior.
577
     */
578
    private function attachBehaviorInternal($name, $behavior)
579
    {
580
        if (!($behavior instanceof Behavior)) {
581
            $behavior = Yii::createObject($behavior);
582
        }
583
        if (is_int($name)) {
584
            $behavior->attach($this);
585
            $this->_behaviors[] = $behavior;
586
        } else {
587
            if (isset($this->_behaviors[$name])) {
588
                $this->_behaviors[$name]->detach();
589
            }
590
            $behavior->attach($this);
591
            $this->_behaviors[$name] = $behavior;
592
        }
593
        return $behavior;
594
    }
595
}
596