Developers

The Notification Center has been built with developers in mind. Its main purpose is to provide a solution for the typical “Dear customer, what would you like to have the e-mail notification say and how should it be translated into French, German etc.?” question.

It gives you the opportunity to create “notification types” with its respective “simple tokens” with which users can then write and translate their messages (e-mails, SMS, Slack messages, basically any Gateway is possible) - and have complete freedom. All you need to do is to provide a notification type and a token configuration for it and the problem is solved for you.

Hence, sending a message through the Notification Center is pretty straight forward:

<?php

namespace Acme;

use Terminal42\NotificationCenterBundle\NotificationCenter;

class SomeService
{
    public function __construct(private NotificationCenter $notificationCenter) {}
    
    public function sendMessage(): void
    {
        $notificationId = 42: // Usually, some module setting of yours where the user can select the desired notification
        $tokens = [
            'firstname' => 'value1',
            'lastname' => 'value2',   
        ];
        
        $receipts = $this->notificationCenter->sendNotification($notificationId, $tokens);
    }
}

This will send the notification ID 42 with a “token collection” created based on your my-notification-type-name and two raw tokens token1 and token2. This means, your customer can go ahead and e.g. compose an e-mail saying Hello ##firstname## ##lastname##, welcome to my message.

We’ll get to the notification type in a second but let’s first check some more internal concepts.

Parcels and Receipts

The naming of classes within the Notification Center is designed around the concept of sending regular mail. As if you’d go to your local post office. Hence, the actual thing that is sent, is referred to as Parcel and the result of sending is, is your Receipt. As, sometimes, you are sending multiple parcels at once, there’s also a ParcelCollection and the corresponding ReceiptCollection.

Every Parcel has a MessageConfig which represents its contents. Moreover, it can have stamps which are represented by the StampInterface. It can have as many stamps as needed and they represent metadata to the parcel itself.

Because there is one Receipt per Parcel, you can always access the original information and inspect everything you need. So in our example above, $receipts is a ReceiptCollection and we can now do all kinds of operations with it:

$receipts = $this->notificationCenter->sendNotification($notificationId, $tokenCollection);
        
// Successful, let's go!
if ($receipts->wereAllDelivered()) {
    return;
}

// Otherwise, we can inspect:
/** @var Terminal42\NotificationCenterBundle\Receipt\Receipt $receipt */
foreach ($receipts as $receipt) {
    if (!$receipt->wasDelivered()) {
       dump($receipt->getException()); // Always an instance of CouldNotDeliverParcelException
    }
    
    $receipt->getParcel()->getMessageConfig(); // Access the contents of the parcel
    $receipt->getParcel()->getStamps(); // Access the metadata of the parcel, aka the stamps
}

So calling $this->notificationCenter->sendNotification() is actually an abbreviation for creating a Parcel with a MessageConfig and adding a TokenCollectionStamp which contains your token information.

The task of a gateway implementing the GatewayInterface then is to deliver that Parcel and returning a Receipt for it. Gateways have their dedicated documentation page.

For now, let’s look at notification types.

Notification types

Notification types define which tokens are available for a given message. This allows the Notification Center to assist the users with autocompletion in the back end. Let’s look at the one for the Contao form submission notification type (triggered, when a form is submitted):

class FormGeneratorNotificationType implements NotificationTypeInterface
{
    public const NAME = 'core_form';

    public function __construct(private TokenDefinitionFactoryInterface $factory)
    {
    }

    public function getName(): string
    {
        return self::NAME;
    }

    public function getTokenDefinitions(): array
    {
        return [
            $this->factory->create(AnythingTokenDefinition::class, 'form_*', 'form.form_*'),
            $this->factory->create(AnythingTokenDefinition::class, 'formconfig_*', 'form.formconfig_*'),
            $this->factory->create(AnythingTokenDefinition::class, 'formlabel_*', 'form.formlabel_*'),
            $this->factory->create(TextTokenDefinition::class, 'raw_data', 'form.raw_data'),
            $this->factory->create(TextTokenDefinition::class, 'raw_data_filled', 'form.raw_data_filled'),
        ];
    }
}

As you can see, it has a name (core_form) which we put in a constant, so it’s easier to reuse when sending (instead of typing contao_form you just use FormGeneratorNotificationType::NAME). Also, we use the TokenDefinitionFactoryInterface to create our token definitions.

In order to make the new notification type known to the Notification Center, you have to register it as a service and tag it using the notification_center.notification_type tag. If you use the autoconfiguration feature of the Symfony Container, you don’t need to tag the service. Implementing the NotificationTypeInterface will be enough.

Token definitions

A token definition has to implement the TokenDefinitionInterface and you can extend the AbstractTokenDefinition if you need to create your own one. This, however, is pretty unlikely as the Notification Center already ships quite a few of them:

  • EmailTokenDefinition
  • FileTokenDefinition
  • HtmlTokenDefinition
  • TextTokenDefinition
  • AnythingTokenDefinition (basically “this token can be used anywhere”)

Basically, the purpose of a token definition is to describe its values. For example, in the e-mail settings for the recipient, we don’t want anything different from EmailTokenDefinition instances to be allowed. You cannot send an e-mail to <html><title>Foobar</title></html> - it must be an e-mail address.

In the DCA, you can then configure which token definition context is required:

'recipients' => [
    'exclude' => true,
    'inputType' => 'text',
    'eval' => ['tl_class' => 'long clr', 'decodeEntities' => true, 'mandatory' => true],
    'nc_context' => TokenContext::Email,
    'sql' => ['type' => 'string', 'length' => 255, 'default' => null, 'notnull' => false],
],

So we now have a list of token definitions and a token context “email”. Someone now has to tell the Notification Center that for this context, all tokens of type EmailTokenDefinition and AnythingTokenDefinition should be listed. See the GetTokenDefinitionClassesForContextEvent for more details.

Bulky items

To stick to our post office analogy: Sometimes parcels are really heavy or bulky and they cannot be handed over the counter. So what you do is you hand over your bulky item at a separate place that has more space for big items and trucks to maneuver. You may deliver your item there and receive a voucher for it. Then, you take that voucher and take that to the post office counter to conclude the transaction.

Bulky items in web development might be things like user uploads, file attachments etc. We don’t want to log the contents of those files anywhere so they must not be part of our Parcel instance but instead we just pass along “vouchers”. Vouchers are nothing else than a simple combination of today’s date and a UUID for later reference.

Let’s look at how the Core uses this to pass on file uploads from the form generator to e.g. our mailer gateway:

<?php

foreach ($files as $k => $file) {
    $voucher = $this->notificationCenter->getBulkyItemStorage()->store(
        FileItem::fromPath($file['tmp_name'], $file['name'], $file['type'], $file['size'])
    );

    $bulkyItemVouchers[] = $voucher;
}

As you can see. All it’s doing is asking the Notification Center for the bulky goods storage and storing a FileItem in there. It can be anything implementing the BulkyItemInterface. The Notification Center will take care of actually storing the item and arbitrary metadata. It also takes care of cleaning them up. As a developer, that’s all you need to do. We then add a stamp to the parcel to inform the gateways about the fact that this parcel has bulky items to it:

<?php

$stamps = $stamps->with(new BulkyItemsStamp($bulkyItemVouchers));

Done. A gateway can now use those vouchers to actually ask for the bulky items when sending them. This could look for example like this:

<?php

$item = $this->getNotificationCenter()->getBulkyItemStorage()->retrieve($voucher);

if ($item instanceof FileItem) {
    $email->attach(
        $item->getContents(),
        $item->getName(),
        $item->getMimeType()
    );
}

💡 Technically, you could omit the BulkyItemsStamp if for example you added the voucher as a token value. So let’s say your ##form_upload## token contains an upload voucher. The gateway could just replace that token and use the voucher to fetch from the bulky goods storage, right? Well, yes. While possible, it’s still recommended you add the BulkyItemsStamp as this allows any gateway to differentiate whether the value of a token was really meant to be a bulky item or if - by chance - just has the same format as a voucher. Moreover, by providing the stamp, other extensions or your custom application code can read from the stamp and process those further if needed. In short: Always use the BulkyItemsStamp.

Events

Sometimes, you need to add tokens to an already existing notification type or you want to log information etc. That’s when we have to dig a little deeper into the processes of the Notification Center. There are four events you can use:

  • AsynchronousReceiptEvent
  • CreateParcelEvent
  • GetNotificationTypeForModuleConfigEvent
  • GetTokenDefinitionsForNotificationTypeEvent
  • GetTokenDefinitionClassesForContextEvent
  • ReceiptEvent

AsynchronousReceiptEvent

This event is dispatched when a Gateway triggers $notificationCenter->informAboutAsynchronousReceipt() with the matching AsynchronousReceipt. You can use it to get informed about asynchronous parcel delivery updates, see Asynchronous Gateways for more information.

CreateParcelEvent

This event is dispatched when a new Parcel is created within the Notification Center. If you create the Parcel instance yourself using new Parcel(), you may call $notificationCenter->dispatchCreateParcelEvent($parcel) for that.

It allows to add stamps to the parcel or even replace it altogether. Notification Center itself uses this event to add the admin_email token to all parcels for example. We’re talking about the value here, the definition is added in the GetTokenDefinitionsEvent.

GetNotificationTypeForModuleConfigEvent

A typical use case will be to have a notification type selection in a front end module. For that, the Notification Center ships with tl_module.nc_notification which is of inputType select. Depending on the front end module type, however, developers will want to restrict the available notification options to a certain type. E.g. the Lost password module shouldn’t list notifications for a Contao form submission or an Isotope eCommerce status update type.

This event is here for you to be able to reuse tl_module.nc_notification in your palette and then filter for your notification type.

GetTokenDefinitionsForNotificationTypeEvent

This event is here to extend the list of token definitions for a given notification type. The Notification Center itself uses this to add an admin_email token definition to all notification types for example. We’re talking about the definition here, the value is added in the CreateParcelEvent.

GetTokenDefinitionClassesForContextEvent

This event is here to extend the list of token definition classes for a given context. The Notification Center itself does not use this event. But if you introduced your own instance of TokenDefinitionInterface and you would want to make sure this is shown e.g. in the TokenContext::Email context, this is your event!

ReceiptEvent

This event is dispatched every time a Parcel was sent. It can be used to implement e.g. logging, retry logic etc.