Cleaning up your Event code in Craft CMS

Nate IlerNate Iler

Nate Iler

Event Basics

Craft CMS allows you to do a lot of customization by hooking into Events via a Plugin or Module. You can see the basics of how Events and Hooks work in the documentation.

Here's a full list of Events provided by Craft that you can use in your plugins or modules:

Here's an example Module named `Demo` that runs code after an Element has saved:

<?php

namespace flipbox\demo;

use yii\base\Module;

class Demo extends Module
{
    public function init()
    {
        Event::on(
            Elements::class,
            Elements::EVENT_AFTER_SAVE_ELEMENT,
            function (ElementEvent $event) {
                // do stuff
            }
        );
    }
}

My Plugin/Module is Growing

As a Craft CMS project's size or complexity grows, you'll most likely end up adding a lot of custom business logic to either a Module or Plugin. This may require hooking into more and more of Craft's events. This will inevitably lead to your init() function getting very large and hard to maintain.

Here's an example Plugin named `Demo` that has grown very large (logic removed):

<?php # Demo.php

namespace flipbox\demo;

use craft\base\Plugin;
...

class Demo extends Plugin
{
    public function init()
    {
        Event::on(
            Elements::class,
            Elements::EVENT_AFTER_SAVE_ELEMENT,
            function (ElementEvent $event) {
                // do stuff
            }
        );

        Event::on(
            View::class,
            View::EVENT_BEFORE_RENDER_PAGE_TEMPLATE,
            function (TemplateEvent $event) {
                // do stuff
            }
        );

        Event::on(
            SystemMessages::class,
            SystemMessages::EVENT_REGISTER_MESSAGES,
            function (RegisterEmailMessagesEvent $event) {
                // do stuff
            }
        );

        Event::on(
            Fields::class,
            Fields::EVENT_REGISTER_FIELD_TYPES,
            function (RegisterComponentTypesEvent $event) {
                // do stuff
            }
        );

        ... x20
    }
}

Solution #1 - Group Event Handlers Into Functions

Group your events in separate functions and call those from init(). This is a simple first step.

<?php # Demo.php

namespace flipbox\demo;

use craft\base\Plugin;
...

class Demo extends Plugin
{
    public function init()
    {
        self::registerViewHandlers();
        self::registerUserHandlers();
        self::registerHubspotHandlers();
        self::registerSalesforceHandlers();
    }

    protected static function registerViewHandlers()
    {
        // set up event handling
    }

    protected static function registerUserHandlers()
    {
        // set up event handling
    }

    protected static function registerHubspotHandlers()
    {
        // set up event handling
    }

    protected static function registerSalesforceHandlers()
    {
        // set up event handling
    }
}

Solution #2 - Put Event Registration in a Separate File

Create an Events.php (or Handlers.php) file in the root of your plugin or module and call that from init().

This cleanly separates your Event setup from the rest of your plugin/module code.


<?php # Demo.php

namespace flipbox\demo;

use craft\base\Plugin;

class Demo extends Plugin
{
    public function init()
    {
        Craft::$app->getPlugins()->on(
            Plugins::EVENT_AFTER_LOAD_PLUGINS,
            function () {
                Events::register();
            }
        );
    }
}
<?php # Events.php

namespace flipbox\demo;

class Events
{
    public static function register()
    {
        // register event handlers here however you prefer
    }
}

Solution #3 - Separate Classes (Recommended)

Create a directory in your plugin/module and create separate PHP classes for each event you're hooking into.

Here are some ideas of how you could structure this:

  • A new /handlers directory at the root (or /listeners if you prefer)
  • Create /events/handlers and keep your events and handlers close

It's up to you since there is currently no Craft CMS recommendation to follow.

We've started converting our plugins and modules to use these invokable classes. It organizes the business logic in descriptively-named classes, it cleans up event/handler registration in our plugins/modules, and you can add private functions as needed. Occasionally, we won't move an event handler to a class if there are only a few lines of logic.

Here are two ways you can set this up via an invokable class or a static function.

<?php # Events.php

namespace flipbox\demo;

use flipbox\demo\events\ElementSaved;
use flipbox\demo\events\RenderingPageTemplate;

class Events
{
    public function init()
    {
        // Invokable class
        Event::on(
            Elements::class,
            Elements::EVENT_AFTER_SAVE_ELEMENT,
            new ElementSaved
        );

        // Static function
        Event::on(
            View::class,
            View::EVENT_BEFORE_RENDER_PAGE_TEMPLATE,
            [
                RenderingPageTemplate::class,
                'handle'
            ]
        );
    }
}

Invokable Class

<?php # /handlers/ElementSaved.php

namespace flipbox\demo\handlers;

use craft\events\ElementEvent;

class ElementSaved
{
    public function __invoke(ElementEvent $event)
    {
        // do stuff

        $this->convertTimezones();
    }

    private function convertTimezones()
    {
        // why me?!
    }
}

Static Function

<?php # /handlers/RenderingPageTemplate.php

namespace flipbox\demo\handlers;

use craft\events\TemplateEvent;

class RenderingPageTemplate
{
    public static function handle(TemplateEvent $event)
    {
        // do stuff

        $this->generatePdf();
    }

    private function generatePdf()
    {
        // please help!!
    }
}

Note Regarding Unregistering Events

Yii & Craft can register and unregister events. Something to consider with invokable Event Handler classes is you aren't able to easily unregister a single Event Handler using this method because you don't have a link to the original Event Handler when it's turned on.

Calling the code below will turn off the Event and therefore anything that hooks into it, not just a single Event Handler.

$elements = Craft::$app->elements;
$elements->off(
    \craft\services\Elements::EVENT_AFTER_SAVE_ELEMENT
);

Summary

If you're working on a simple Craft CMS plugin or module, you may not need to worry about keeping Events organized as you may only have a few. (We'd still suggest Solution #1 or #2 so you'll be ready for the future.) As your plugin or module grows, we'd highly recommend considering Solution #3 so that your Event business logic is separate from Event registration. We'd also recommend using Services to keep your complex logic out of your new classes.

Thanks for reading and I hope this was helpful. You can reach out with questions or feedback at @dstjohn, @flipboxdigital, or the Contact page.