For arlanix/notification 1.0.0
Add a protocol
A protocol is a way of delivering: email, Telegram, Slack and Discord are four, each a module of its own. Another module adds another, such as Microsoft Teams or SMS, and the subscription form offers it under Delivered by. Every subscription has one address in its protocol's terms: one email, one chat, one webhook.
The class
Implement Arlanix\Notification\Model\Protocol\ProtocolInterface:
| Method | |
|---|---|
deliver(Endpoint $endpoint, Digest $digest): void |
Send one notification, or several as one digest ($digest->isSingle(), entries(), total(), severity()). Throw on any failure: the delivery is retried. A DeliveryException's message is shown to the operator as it is; say what to fix, and no secret. Anything else is logged and shown as a generic message. A service that asks to wait (HTTP 429): throw Arlanix\Notification\Model\Protocol\RateLimitedException($phrase, $retryAfter), $retryAfter in seconds, 60 when not given and an hour at most, and that delivery and the subscription's digests after it wait that long; during the first day after the message was published the attempt does not count. |
address(Endpoint $endpoint): string |
The one place it delivers to, as one value: a topic takes one subscription per address. Lower-case what is case-blind; hash a secret, such as a webhook URL, as the table keeps it in clear. |
describe(Endpoint $endpoint): string |
One line for the grid and the log, with no secret. Never throws. |
validate(Endpoint $endpoint): string[] |
Why the endpoint cannot be used; none when it can. Saving the subscription refuses it with these. |
setupProblem(Endpoint $endpoint): ?string |
What keeps the protocol from sending to this endpoint before anything is tried, such as a bot token set up neither for the store nor on the subscription; null when it can send. Delivery, Send test message and the warning on save ask it. |
Endpoint gives the subscription's settings (setting('endpoint/<code>_…'), typed by their normalisers) and secrets (secret(…), in clear). Arlanix\Notification\Model\Render\PlainTextRenderer writes a subject and a body from a digest, with maxLength for a chat's limit; render your own if your protocol has formatting. Cut to maxLength, a digest ends in a line that says how many more the Message Log has, never dropping one unsaid.
A protocol that posts to an API or a webhook does it through Arlanix\Notification\Model\Protocol\Http\HttpClientInterface::postJson($url, $body, $timeout), which answers whatever the status and, when no answer comes, throws a DeliveryException naming the host and never the URL, which may hold a secret. Its HttpResponse has the status, the body (json()), the headers (header('Retry-After')), and, for a 429 (isRateLimited()), how long the service asks to wait: retryAfter() reads the Retry-After header, in seconds or as a date, or a retry_after in the JSON, and is null when it says nothing.
Registering it
In etc/di.xml, the protocol, with a code of lower-case letters and digits, and its settings. A secret is not a setting: it is listed under secrets, stored encrypted on the subscription and never sent back to the form.
<type name="Arlanix\Notification\Model\Protocol\ProtocolPool">
<arguments>
<argument name="protocols" xsi:type="array">
<item name="teams" xsi:type="array">
<item name="label" xsi:type="string" translate="true">Microsoft Teams</item>
<item name="protocol" xsi:type="object">Vendor\NotificationTeams\Model\TeamsProtocol</item>
<item name="icon" xsi:type="string">Vendor_NotificationTeams::images/teams.svg</item>
<item name="sortOrder" xsi:type="number">30</item>
<item name="secrets" xsi:type="array">
<item name="webhook" xsi:type="string">endpoint/teams_webhook_url</item>
</item>
</item>
</argument>
</arguments>
</type>
<virtualType name="Arlanix\Notification\Model\Subscription\SettingPool">
<arguments>
<argument name="settings" xsi:type="array">
<item name="endpoint/teams_channel" xsi:type="array">
<item name="normalise" xsi:type="object">Arlanix\Core\Model\Settings\Normaliser\StringNormaliser</item>
</item>
</argument>
</arguments>
</virtualType>
Every setting and secret of a protocol is under endpoint/<code>_; those of another protocol are dropped when a subscription is saved, so a webhook typed for Teams does not linger after a switch to email. A normaliser must not throw: every registered one runs on every save.
The form
view/adminhtml/ui_component/arlanix_notification_subscription_form.xml adds the fields to the endpoint fieldset, each with variants naming the code, so the form shows them only for that protocol:
<form xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd">
<fieldset name="endpoint">
<field name="teams_channel" formElement="input" sortOrder="100">
<argument name="data" xsi:type="array">
<item name="config" xsi:type="array">
<item name="variants" xsi:type="array">
<item name="0" xsi:type="string">teams</item>
</item>
</item>
</argument>
<settings>
<label translate="true">Channel</label>
<dataType>text</dataType>
</settings>
</field>
</fieldset>
</form>
A secret field posts its value under the same name (teams_webhook_url); left blank, it keeps the stored one. The form gets has_teams_webhook_url to say one is stored. Arlanix_Core/js/form/element/secret is the field for it: masked, never filled, and saying a secret is stored when its showStoredNotice imports ${ $.provider }:data.endpoint.has_teams_webhook_url, with the words in its storedNotice.
The configuration
What the protocol needs once for the store, such as a bot token, goes in a group of the arlanix_notification section of system.xml, under the same ACL resource, Arlanix_Notification::config. Have setupProblem() say when it is missing.
A secret kept in the configuration takes Magento\Config\Model\Config\Backend\Encrypted in system.xml, and in config.xml as the default's backend_model: the configuration then decrypts it as it reads it, so read it as it comes and never decrypt it again.
Trying it
The four protocols that come with it, arlanix/module-notification-smtp, -telegram, -slack and -discord in vendor/, are the examples to follow.
Save a subscription delivered by yours, then use its Send test message (see Subscriptions): one message goes straight to deliver(), past the queue and the log. A problem setupProblem() returns stops it there and is shown. Otherwise the notice above the grid says it was sent, or shows a DeliveryException's message as it is and the generic one for anything else; the row's Last delivery keeps it. Publish test message, on a topic's row (see Topics), takes the way a module's message does: through the queue, to every subscription of the topic that takes everything.