Events are a fundamental way for different addons to communicate with each other. Events are fired with and any addon can hook into / listen to these events and respond to them.
The EventManager
is responsible for creating and responding to events in Vanilla.
Getting the Event Manager
The proper way to get the event manager is through the container. Do not create a new instance yourself.
Firing an Event
There are multiple ways to fire events.
fire()
The simplest method of firing an event is using the EventManager::fire(string $event, ...$args): array
. You pass an event name, and any arguments you want the event handlers to have, and get back an array of responses from every responding event.
Example 1
class HtmlProcessor {
// ...
private $allowedUrlSchemes = ['http://', 'https://', 'mailto://', 'tel://'];
public function __construct(EventManager $eventManager) {
// Allow addons to add extra allowed URL schemes
/** @var string[] */
$extraUrlSchemes = $eventManager->fire('getExtraAllowedUrlSchemes');
$this->allowedUrlSchemes = array_merge($this->allowedUrlSchemes, $extraUrlSchemes);
}
// ...
}
Let’s take a look at what the handler for this event would look like. Using Gdn_Plugin
is the easiest way to register event handlers. All methods on the plugin instance are automatically bound as events.
class SteamPlugin extends Gdn_Plugin {
public function getExtraAllowedUrlSchemes() {
return "steam://";
}
}
fireFilter()
The previous EventManager::fire()
call was simple, but does not work for every case. Imagine a scenario where you would like multiple addons to be able build on top of the results of each other. Such as the the GET /api/v2//discussions
endpoint. Here we want multiple addons to be able to modify the result, and we want them all to be working with the same thing. We also want to pass the context of the request into the event.
EventManager::fireFilter(string $event, $initialValue, ...$args)
is the perfect candidate for this. It will fire an event name of $event
, gather all of the handlers, and pass $initalValue
as the first parameter of the event handler, then pass the return value of the previous event handler into the first parameter of the each other handler. The result of the last handler will be returned.
The rest of the arguments will be passed along to each handler.
Example
class DiscussionsApiController extends AbstractApiController {
public function index(array $query) {
// ...
// Allow addons to modify the result.
$result = $this->getEventManager()->fireFilter(
'discussionsApiController_indexOutput',
$result,
$this,
$in,
$query,
$rows
);
// ...
}
}
And the handler:
class ReactionsPlugin extends Gdn_Plugin {
public function discussionsApiController_indexOutput(
array $previousResult,
DiscussionsApiController $sender,
Schema $inSchema,
array $query,
array $rows
): array {
$newResult = $previousResult
// Modify the result in same way
return $newResult;
}
Note that the return type should be the same as the $previousResult
type, because it will be passed into the next handler.
fireDeprecated()
The fire deprecated method is very similar to the fire()
method. It functions identically except:
- If any handler is bound to it
- It will trigger a deprecated notice (
E_USER_DEPRECATED
).
Gdn_Pluggable
The EventManager
class represents the modern way of firing and handling events in Vanilla. Previously events were fired through the PluginManager
or through and abstraction Gdn_Pluggable
. Lots of Vanilla classes extend from Gdn_Pluggable
. Almost every class beginning with Gdn
extends from Gdn_Pluggable
including Gdn_Plugin
and Gdn_Controller
.
Using this method of firing events is now discouraged. While the implementation now uses the EventManager
internally, firing events this way carries the overhead of multple additional function calls and has a less explicit syntax.
Gdn_Pluggable
exposes a method Gdn_Pluggable::fireEvent(string $eventName, array $args)
. It would join the name of the class firing and event with the $eventName
and fire that as an event. The class property ->EventArguments
would be merged with $args
and passed to any handler. If the class name was not preferable the event prefix could be set by calling the Gdn_Pluggable::fireAs(string $prefix)
method before calling fireEvent()
.
The common method of the receiving data back from the events was to use the fact that array values are passed by reference and modify them inside of the handlers.
Let’s look at an example.
class Gdn_Form extends Gdn_Pluggable {
public function bodyBox($column = 'Body', $attributes = []) {
// ...
$this->EventArguments['Table'] = val('Table', $attributes);
$this->EventArguments['Column'] = $column;
$this->EventArguments['Attributes'] = $attributes;
$this->EventArguments['BodyBox'] =& $result;
$this->fireEvent('BeforeBodyBox');
// ...
}
}
And a handler. Notice
- The calling class being passed in as the first parameter.
- The arguments array being passed as the second array.
- The arguments array is modified by reference.
class EditorPlugin extends Gdn_Plugin {
public function gdn_form_beforeBodyBox_handler(Gdn_Form $sender, array $args) {
// ...
// Convert the form body to WYSIWYG
if ($this->ForceWysiwyg == true && $needsConversion) {
$wysiwygBody = Gdn_Format::to($sender->getValue('Body'), $this->Format);
$sender->setValue('Body', $wysiwygBody);
$this->Format = 'Wysiwyg';
$sender->setValue('Format', $this->Format);
}
// Append the editor HTML
$view = $c->fetchView('editor', '', 'plugins/editor');
$args['BodyBox'] .= $view;
}
}
Handlers
Any class extending Gdn_Plugin
can handle these events fired by and instance of Gdn_Pluggable
. These handlers look like this:
/**
* @param object $sender Sending object instance.
* @param array $args Event's arguments.
*/
public function base_someEvent_handler($sender, $args) {
// Do something.
}
Each handler’s function name is made up of 3 parts. - The name of class implementing Gdn_Pluggable
to listen for - The event name - handler
Using base
instead of a class name will allow your handler to listen to every fired event for your event name. So base_someEvent_handler
would listen for a fireEvent('SomeEvent')
on every instance of Gdn_Pluggable
, while profileController_getConnections_handler
would listen only on the ProfileController
for the fireEvent('GetConnections)
.
The handler is passed a $sender
and $args
so that you method can call methods on it’s sending instance of Gdn_Pluggable
and its event arguments.
Magic Events
Magic events were an elaborate system of hook possibilities that involved the method prefix ‘x’ and PHP’s __call()
method. Currently, there is only one undeprecated magic event in Vanilla: render_before
. It invokes just before the page is rendered. Example use: base_render_before($sender)
. It is best to avoid when another event is usable.
For a better alternative hook that reliably fires early on every request, try gdn_dispatcher_appStartup_handler
instead. To universally include a CSS file, use assetModel_styleCss_handler
.
Magic Methods
Magic methods allow you to create new methods and add them to existing objects. They are created in much the same way that you plug into events. Imagine you wanted to add a method named Kaboom
to the DiscussionsController:
class MyPlugin extends Gdn_Plugin {
public function discussionsController_kaboom_create($sender) {
echo "Kaboom!";
}
}
With this addon enabled, going to the URL /discussions/kaboom
would now output the text “Kaboom!”. You can references other methods and properties on the extended object using the $sender
variable.
If you use a magic method to duplicate an existing method name, it will be overridden completely. And call to it will be directed to your plugin instead. The only exception is the index()
method.
Magic methods only work in classes that extend Gdn_Pluggable
. For example, notice the Gdn_Form
class does, but the Gdn_Format
class does not. All models and controllers do.
Example Events
Inject the the current user’s roles into every page
Sometimes you may want to adjust parts of the template based on the roles the current user. This will gather the roles of the current user and inject them into the smarty template.
public function base_render_before($sender) {
if (inSection('Dashboard')) {
return;
}
if(!val('UserRoles', $sender->Data)) {
$userRoles = val('UserRoles', $sender->Data);
if (!$userRoles) {
$user = val('User', Gdn::controller());
if (!$user && Gdn::session()->isValid()) {
$user = Gdn::session()->User;
}
$userID = val('UserID', $user);
$userRoles = Gdn::userModel()->getRoles($userID)->resultArray();
}
$sender->setData('UserRoles', $userRoles);
}
}
Inject a conditional based on roles
Sometimes you may not need all the roles in the page. Let’s say you wanted to make the page appear differently for a user with a certain role. Instead of injection all of the roles into the template and doing the conditional there, you can do it in the themehooks and inject just the boolean value you need.
public function base_render_before($sender) {
if (inSection('Dashboard')) {
return;
}
$userRoles = val('UserRoles', $sender->Data));
if (!$userRoles) {
$user = val('User', Gdn::controller());
if (!$user && Gdn::session()->isValid()) {
$user = Gdn::session()->User;
}
$userID = val('UserID', $user);
$userRoles = Gdn::userModel()->getRoles($userID)->resultArray();
}
$roleNames = array_column($userRoles, 'Name');
$isSuperSpecialRole = in_array("SuperSpecialRole", $roleNames);
$sender->setData("isSuperSpecialRole", $isSuperSpecialRole);
}
Create an additional settings page
This example creates a custom dashboard page that can set a few configuration options. You would then need to use these set configuration values in other hooks to customize your site. The configuration module uses Gdn_Form internally and renders an nice looking form for the dashboard. Its implementation can be found here. Further details can be found by looking through Gdn_Form. Not all form values are supported. Currently supported form values are
categorydropdown
labelcheckbox
checkbox
toggle
dropdown
imageupload
color
radiolist
checkboxlist
textbox
Additional and more complex examples of its use can be found in the SettingsController
and Vanilla’s bundled plugins.
class MySitePlugin extends Gdn_Plugin {
/**
* Create the `/settings/example` page to host our custom settings.
*
* @param SettingsController $sender
*/
public function settingsController_example_create($sender) {
$sender->permission('Garden.Settings.Manage');
$configurationModule = new ConfigurationModule($sender);
$configurationModule->initialize([
'ExmapleSite.BackgroundColour' => ['Control' => 'TextBox', 'Options' => ['class' => 'InputBox BigInput']],
'ExmapleSite.BackgroundImage' => ['Control' => 'ImageUpload'],
'ExmapleSite.BannerImage' => ['Control' => 'ImageUpload'],
]);
$sender->addSideMenu();
$sender->setData('Title', "My Site Setttings");
$configurationModule->renderAll();
}
/**
* Add the "My Site" menu item.
*
* @param Gdn_Controller $sender
*/
public function base_getAppSettingsMenuItems_handler($sender) {
/* @var SideMenuModule */
$menu = $sender->EventArguments['SideMenu'];
$menu->addLink('Appearance', t('My Site'), '/settings/example', 'Garden.Settings.Manage');
}
}
Add a link to the MeBox
/**
* Add link to drafts page to me module flyout menu.
*
* @param MeModule $sender The MeModule
* @param array $args Potential arguments
*
* @return void
*/
public function meModule_flyoutMenu_handler($sender, $args) {
if (!val('Dropdown', $args, false)) {
return;
}
/** @var DropdownModule $dropdown */
$dropdown = $args['Dropdown'];
$dropdown->addLink(t('My Drafts'), '/drafts', 'profile.drafts', '', [], ['listItemCssClasses' => ['link-drafts']]);
}
Add a custom location for a moderation message
/**
* Add custom asset location for messages
*
* @param MessageController $sender The message controller
* @param array $args The event arguments passed
*
* @return void
*/
public function messageController_afterGetAssetData_handler($sender, $args) {
$possibleAssetLocations = val('AssetData', $args);
$possibleAssetLocations['AbovePromotedContent'] = 'Above the promoted content module';
setValue('AssetData', $args, $possibleAssetLocations);
}
You would then need to place this new asset somewhere in your template
{asset name="AbovePromotedContent"}
Add the Views and Comments counts to a Discussion Page
/**
* Adds Views and Comments count to Discussion Page
*
* @param DiscussionController $sender
* @param array $args
*/
public function DiscussionController_AfterDiscussionTitle_handler($sender, $args) {
echo '<span class="Discussion-CountViews">';
echo t("Views").": ";
echo $args['Discussion']->CountViews;
echo '</span>';
echo '<span class="Discussion-CountComments">';
echo t("Reply").": ";
echo $args['Discussion']->CountComments;
echo '</span>';
}
Add Leaderboards to the panel on the categories page
/**
* Adds leaderboards to Activity page
*
* @param ActivityController $sender
* @param array $args
*/
public function categoriesController_render_before($sender, $args) {
if ($sender->deliveryMethod() == DELIVERY_METHOD_XHTML) {
$sender->addModule('LeaderBoardModule', 'Panel');
$module = new LeaderBoardModule();
$module->SlotType = 'a';
$sender->addModule($module);
}
}