Notifications placeholders

Concepts

  • Every notifiable event defines an exclusive list of placeholder classes that it supports
  • Every placeholder class has an associated prefix in its name that allows name spacing of the same classes (e.g. a user can be author and commenter) - the resulting placeholder list consist of namespace_property (e.g. author_firstname, commenter_firstname, etc.)
  • Notifications can use placeholders in the subject and body of the message
  • Notifiable events can instantiate a placeholder instance using event_data properties (e.g. event_data must have everything required to instantiate placeholder instances)
  • Placeholders (as well as the subject/body of the message) are processed when the message is being sent
  • Placeholders engine internally converts placeholders into mustache variables and runs text through mustache template engine

Database

There are no specific database fields associated with placeholders.

GraphQL

Placeholders subsystem provides query to fetch all placeholders for notifiable event class in specific context_id.

type totara_notifications_placeholders {
   key: String!
   value: String!
}

extend type Query {
    totara_notifications_placeholders(
        context_id: Int!
        notifiable_event_class: String!
    ): [totara_notifications_placeholders]!
}

Classes/interfaces

This table outlines classes, interfaces, and methods most relevant in placeholders processing.

Class, interface, or method

Notes

\totara_notification\placeholder\abstraction\placeholderBase interface for placeholders. Generally all placeholders must implement this interface, but there are better practical options like extending \totara_notification\placeholder\abstraction\single_emptiable_placeholder class.
\totara_notification\placeholder\abstraction\single_emptiable_placeholder

Abstract class with simple pre-existing logic for placeholder implementation. It is advised to extend this class to write own placeholder classes.
\totara_notification\event\notifiable_event::get_notification_available_placeholder_options()Every notifiable event must return list of supported placeholders options (or empty array if no placeholders supported). These options will be used in notifiable event placeholders listing GraphQL query and during processing of notifications.
\totara_notification\placeholder\placeholder_optionClass that represents one placeholder option for notifiable event. It defines all required information to process placeholders: group name and label, placeholder class, and call back to instantiate this class using existing event data.
\totara_notification\placeholder\template_engine\square_bracket\enginePlaceholder processor that converts message templates with square brackets placeholders into final message. It finds all square bracket placeholders and converts them into mustache variables and then processes it through the mustache placeholder engine to get a final output.
\totara_notification\placeholder\template_engine\mustache\enginePlaceholder processor that finds all mustache variables and prepares data for those variables using placeholder options with the same name provided by relevant notifiable event. This allows use of standard mustache templates and variables for notifications if needed and is used by the square bracket placeholder processor.

Custom placeholders implementation

Create the placeholder: class that implements the placeholder interface.

<?php
namespace your_component\totara_notification\placeholder;

use totara_notification\placeholder\abstraction\single_emptiable_placeholder;
use totara_notification\placeholder\option;
use your_component\your_entity;

// single_emptiable_placeholder is a good abstraction for most custom placeholders
class your_placeholder extends single_emptiable_placeholder {

    // Add all dependencies in constructor, so it is obvious what kind of information notifiable event needs to store
    public function __construct(int $item_id) {

        $this->entity = your_entity:from_id($item_id);
    }

    // Return options that placeholders support, they will be used to provide list of supported placeholders and passed to this object when message is generated
    public static function get_options(): array {
        return [
            option::create('name', get_string('name', 'your_component')),
            option::create('duration', get_string('duration', 'your_component')),
        ];
    }

    // Check if this placeholder has value to set
    public function is_available(string $key): bool {
        return !empty($this->entity);
    }

    // Provide value for placeholder
    public function do_get(string $key): string {

        // ...

        switch ($key) {
            case 'name':
                return $this->entity->get_name();

            case 'duration':
                return $this->entity->get_duration();
        }

        return '';
    }
}


Register placeholder in notifiable event:

<?php
namespace your_component\event;

use core\event\base;
use lang_string;
use totara_notification\event\notifiable_event;
use totara_notification\placeholder\placeholder_option;
use your_component\totara_notification\placeholder\your_placeholder;


final class comment_created extends base implements notifiable_event {
    // ...

    public static function get_notification_available_placeholder_options(): array {
        return [
            placeholder_option::create(
                'item_group',
                your_placeholder::class,
                new lang_string('item_group', 'your_component'),
                function (array $event_data): your_placeholder {
                    return new your_placeholder($event_data->itemid);
                }
            ),

            // More placeholders
        ];

}


Add language string:

$string['item_group'] = 'Item\'s {$a}';


After this placeholders should start working.