Skip to content

Commit

Permalink
Fixes yiisoft#3250: Added support for events partial wildcard matching
Browse files Browse the repository at this point in the history
  • Loading branch information
samdark authored Dec 23, 2017
2 parents 072ef77 + be3ebb0 commit e813c20
Show file tree
Hide file tree
Showing 6 changed files with 361 additions and 15 deletions.
73 changes: 72 additions & 1 deletion docs/guide/concept-events.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ Attaching Event Handlers <span id="attaching-event-handlers"></span>
You can attach a handler to an event by calling the [[yii\base\Component::on()]] method. For example:

```php
$foo = new Foo;
$foo = new Foo();

// this handler is a global function
$foo->on(Foo::EVENT_HELLO, 'function_name');
Expand Down Expand Up @@ -355,3 +355,74 @@ done through the Singleton (e.g. the application instance).

However, because the namespace of the global events is shared by all parties, you should name the global events
wisely, such as introducing some sort of namespace (e.g. "frontend.mail.sent", "backend.mail.sent").


Wildcard Events <span id="wildcard-events"></span>
---------------

Since 2.0.14 you can setup event handler for multiple events matching wildcard pattern.
For example:

```php
use Yii;

$foo = new Foo();

$foo->on('foo.event.*', function ($event) {
// triggered for any event, which name starts on 'foo.event.'
Yii::trace('trigger event: ' . $event->name);
});
```

Wildcard patterns can be used for class-level events as well. For example:

```php
use yii\base\Event;
use Yii;

Event::on('app\models\*', 'before*', function ($event) {
// triggered for any class in namespace 'app\models' for any event, which name starts on 'before'
Yii::trace('trigger event: ' . $event->name . ' for class: ' . get_class($event->sender));
});
```

This allows you catching all application events by single handler using following code:

```php
use yii\base\Event;
use Yii;

Event::on('*', '*', function ($event) {
// triggered for any event at any class
Yii::trace('trigger event: ' . $event->name);
});
```

> Note: usage wildcards for event handlers setup may reduce the application performance.
It is better to be avoided if possible.

In order to detach event handler specified by wildcard pattern, you should repeat same pattern at
[[yii\base\Component::off()]] or [[yii\base\Event::off()]] invocation. Keep in mind that passing wildcard
during detaching of event handler will detach ony the handler specified for this wildcard, while handlers
attached for regular event names will remain even if they match the pattern. For example:

```php
use Yii;

$foo = new Foo();

// attach regular handler
$foo->on('event.hello', function ($event) {
echo 'direct-handler'
});

// attach wildcard handler
$foo->on('*', function ($event) {
echo 'wildcard-handler'
});

// detach wildcard handler only!
$foo->off('*');

$foo->trigger('event.hello'); // outputs: 'direct-handler'
```
1 change: 1 addition & 0 deletions framework/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ Yii Framework 2 Change Log
- Enh #15347: Add `Instance` support for object property in DI container (kojit2009)
- Enh #15340: Test CHANGELOG.md for valid format (sammousa)
- Enh #15360: Refactored `BaseConsole::updateProgress()` (developeruz)
- Enh #3250: Added support for events partial wildcard matching (klimov-paul)
- Bug #15317: Regenerate CSRF token if an empty value is given (sammousa)
- Bug #15380: `FormatConverter::convertDateIcuToPhp()` now converts `a` ICU symbols to `A` (brandonkelly)
- Enh: Added check to `yii\base\Model::formName()` to prevent source path disclosure when form is represented by an anonymous class (silverfire)
Expand Down
81 changes: 76 additions & 5 deletions framework/base/Component.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
namespace yii\base;

use Yii;
use yii\helpers\StringHelper;

/**
* Component is the base class that implements the *property*, *event* and *behavior* features.
Expand Down Expand Up @@ -103,6 +104,11 @@ class Component extends BaseObject
* @var array the attached event handlers (event name => handlers)
*/
private $_events = [];
/**
* @var array the event handlers attached for wildcard patterns (event name wildcard => handlers)
* @since 2.0.14
*/
private $_eventWildcards = [];
/**
* @var Behavior[]|null the attached behaviors (behavior name => behavior). This is `null` when not initialized.
*/
Expand Down Expand Up @@ -301,6 +307,7 @@ public function __call($name, $params)
public function __clone()
{
$this->_events = [];
$this->_eventWildcards = [];
$this->_behaviors = null;
}

Expand Down Expand Up @@ -456,6 +463,13 @@ public function behaviors()
public function hasEventHandlers($name)
{
$this->ensureBehaviors();

foreach ($this->_eventWildcards as $wildcard => $handlers) {
if (!empty($handlers) && StringHelper::matchWildcard($wildcard, $name)) {
return true;
}
}

return !empty($this->_events[$name]) || Event::hasHandlers($this, $name);
}

Expand All @@ -480,6 +494,14 @@ public function hasEventHandlers($name)
*
* where `$event` is an [[Event]] object which includes parameters associated with the event.
*
* Since 2.0.14 you can specify event name as a wildcard pattern:
*
* ```php
* $component->on('event.group.*', function ($event) {
* Yii::trace($event->name . ' is triggered.');
* });
* ```
*
* @param string $name the event name
* @param callable $handler the event handler
* @param mixed $data the data to be passed to the event handler when the event is triggered.
Expand All @@ -492,6 +514,16 @@ public function hasEventHandlers($name)
public function on($name, $handler, $data = null, $append = true)
{
$this->ensureBehaviors();

if (strpos($name, '*') !== false) {
if ($append || empty($this->_eventWildcards[$name])) {
$this->_eventWildcards[$name][] = [$handler, $data];
} else {
array_unshift($this->_eventWildcards[$name], [$handler, $data]);
}
return;
}

if ($append || empty($this->_events[$name])) {
$this->_events[$name][] = [$handler, $data];
} else {
Expand All @@ -501,7 +533,12 @@ public function on($name, $handler, $data = null, $append = true)

/**
* Detaches an existing event handler from this component.
*
* This method is the opposite of [[on()]].
*
* Note: in case wildcard pattern is passed for event name, only the handlers registered with this
* wildcard will be removed, while handlers registered with plain names matching this wildcard will remain.
*
* @param string $name event name
* @param callable $handler the event handler to be removed.
* If it is null, all handlers attached to the named event will be removed.
Expand All @@ -511,23 +548,44 @@ public function on($name, $handler, $data = null, $append = true)
public function off($name, $handler = null)
{
$this->ensureBehaviors();
if (empty($this->_events[$name])) {
if (empty($this->_events[$name]) && empty($this->_eventWildcards[$name])) {
return false;
}
if ($handler === null) {
unset($this->_events[$name]);
unset($this->_eventWildcards[$name]);
return true;
}

// plain event names
if (isset($this->_events[$name])) {
$removed = false;
foreach ($this->_events[$name] as $i => $event) {
if ($event[0] === $handler) {
unset($this->_events[$name][$i]);
$removed = true;
}
}
if ($removed) {
$this->_events[$name] = array_values($this->_events[$name]);
return $removed;
}
}

// wildcard event names
$removed = false;
foreach ($this->_events[$name] as $i => $event) {
foreach ($this->_eventWildcards[$name] as $i => $event) {
if ($event[0] === $handler) {
unset($this->_events[$name][$i]);
unset($this->_eventWildcards[$name][$i]);
$removed = true;
}
}
if ($removed) {
$this->_events[$name] = array_values($this->_events[$name]);
$this->_eventWildcards[$name] = array_values($this->_eventWildcards[$name]);
// remove empty wildcards to save future redundant regex checks :
if (empty($this->_eventWildcards[$name])) {
unset($this->_eventWildcards[$name]);
}
}

return $removed;
Expand All @@ -543,7 +601,19 @@ public function off($name, $handler = null)
public function trigger($name, Event $event = null)
{
$this->ensureBehaviors();

$eventHandlers = [];
foreach ($this->_eventWildcards as $wildcard => $handlers) {
if (StringHelper::matchWildcard($wildcard, $name)) {
$eventHandlers = array_merge($eventHandlers, $handlers);
}
}

if (!empty($this->_events[$name])) {
$eventHandlers = array_merge($eventHandlers, $this->_events[$name]);
}

if (!empty($eventHandlers)) {
if ($event === null) {
$event = new Event();
}
Expand All @@ -552,7 +622,7 @@ public function trigger($name, Event $event = null)
}
$event->handled = false;
$event->name = $name;
foreach ($this->_events[$name] as $handler) {
foreach ($eventHandlers as $handler) {
$event->data = $handler[1];
call_user_func($handler[0], $event);
// stop further handling if the event is handled
Expand All @@ -561,6 +631,7 @@ public function trigger($name, Event $event = null)
}
}
}

// invoke class-level attached handlers
Event::trigger($this, $name, $event);
}
Expand Down
Loading

0 comments on commit e813c20

Please sign in to comment.