PHPackages                             deegitalbe/laravel-trustup-io-notifications-contracts - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [PSR &amp; Standards](/categories/psr-standards)
4. /
5. deegitalbe/laravel-trustup-io-notifications-contracts

ActiveLibrary[PSR &amp; Standards](/categories/psr-standards)

deegitalbe/laravel-trustup-io-notifications-contracts
=====================================================

Shared contracts for the Trustup IO Notifications system.

0.5.1(today)001MITPHPPHP ^8.2

Since Jul 27Pushed todayCompare

[ Source](https://github.com/deegitalbe/laravel-trustup-io-notifications-contracts)[ Packagist](https://packagist.org/packages/deegitalbe/laravel-trustup-io-notifications-contracts)[ RSS](/packages/deegitalbe-laravel-trustup-io-notifications-contracts/feed)WikiDiscussions main Synced today

READMEChangelogDependencies (7)Versions (4)Used By (1)

Trustup IO Notifications Contracts
==================================

[](#trustup-io-notifications-contracts)

Shared contracts for the Trustup IO Notifications system: the data classes, capability interfaces, enums, and wire types exchanged between a source application (via the client package) and the notifications service.

This package holds no transport logic. It defines **what** a notification is and **how** it serializes. The client package (`deegitalbe/laravel-trustup-io-notifications`) depends on it to send notifications and to receive delivery feedback.

- Package name: `deegitalbe/laravel-trustup-io-notifications-contracts`
- Namespace: `Deegitalbe\TrustupIoNotificationsContracts\`
- Requires: PHP 8.2+, `ext-intl`

What lives here
---------------

[](#what-lives-here)

ConcernTypeA notification's payload`Data\*NotificationData` classes implementing `Contracts\NotificationData`Which channels a notification supportsCapability interfaces: `EmailCapable`, `SmsCapable`, `PushCapable`Default channel renderingTraits: `RendersEmail`, `RendersSms`, `RendersPush`Automatic array serialization`SerializesFromConstructor`The registry of notification types`Enums\NotificationType`The recipient of a notification`Request\Recipient`The wire envelope`Envelope`, `Request\RequestPayload`, `Status\StatusPayload`, `Engagement\EngagementPayload`Most consumers only ever touch **data classes** (to send) and **payloads** (to read feedback). The rest is machinery.

Notification data classes
-------------------------

[](#notification-data-classes)

A data class is the typed payload of one notification type. It declares which channels it supports by implementing capability interfaces, and gets array serialization for free from `SerializesFromConstructor`.

### Minimal, email-only

[](#minimal-email-only)

```
use Deegitalbe\TrustupIoNotificationsContracts\Contracts\EmailCapable;
use Deegitalbe\TrustupIoNotificationsContracts\Contracts\NotificationData;
use Deegitalbe\TrustupIoNotificationsContracts\Data\Concerns\RendersEmail;
use Deegitalbe\TrustupIoNotificationsContracts\Data\Concerns\SerializesFromConstructor;
use Deegitalbe\TrustupIoNotificationsContracts\Enums\NotificationType;

final readonly class ToolsCommentNotificationData implements EmailCapable, NotificationData
{
    use RendersEmail;
    use SerializesFromConstructor;

    /** @param list $attachment_details */
    public function __construct(
        public string $product_url,
        public string $product_name,
        public string $body,
        public array $attachment_details,
        public string $commenter_name,
        public string $timestamp,
        public string $action_url,
        public string $notifications_url,
        public string $company_name,
        public string $company_address,
    ) {}

    public function notificationType(): NotificationType
    {
        return NotificationType::ToolsCommentNotification;
    }
}
```

Two rules:

1. Implement `NotificationData` (which requires `notificationType()`), plus one capability interface per channel the notification supports.
2. Use `SerializesFromConstructor` so the payload can cross the wire.

The constructor parameter names are the serialized keys. For email, they are also the Postmark template model variables (see below), so name them to match your template placeholders (`product_url`, `body`, ...).

### Multi-channel

[](#multi-channel)

Implement several capability interfaces and pull in the matching traits:

```
final readonly class ToolsTestNotificationData implements EmailCapable, NotificationData, PushCapable, SmsCapable
{
    use RendersEmail;
    use RendersPush;
    use RendersSms;
    use SerializesFromConstructor;

    public function __construct(
        public string $title,
        public string $body,
    ) {}

    public function notificationType(): NotificationType
    {
        return NotificationType::ToolsTestNotification;
    }
}
```

`NotificationType::supportedChannels()` derives the channel list automatically from the interfaces the data class implements. You never declare channels twice.

Capability interfaces and their default rendering
-------------------------------------------------

[](#capability-interfaces-and-their-default-rendering)

Each capability interface has a companion `Renders*` trait that provides a sensible default implementation. You only override a method when the default does not fit.

### `EmailCapable` + `RendersEmail`

[](#emailcapable--rendersemail)

Email is rendered **provider-side** (Postmark owns the layout and per-language wording). The data class only supplies the template model (variables) and picks the template.

MethodDefaultOverride to`toEmail(): EmailContent``new EmailContent($this->emailVariables())`rarely`emailTemplate(): string``notificationType()->slug()` (e.g. `tools-comment-notification`)use a different Postmark template alias`emailVariables(): array``$this->toArray()` (all constructor data)send a subset / reshape the template model`emailTemplateLocaleGranularity(): EmailTemplateLocaleGranularity``Language`opt into per-locale templates (see enum below)`EmailContent` is `readonly` and wraps `public array $variables` (the Postmark template model).

### `SmsCapable` + `RendersSms`

[](#smscapable--renderssms)

SMS has no provider template, so the body is translated **service-side** with `__()`.

MethodDefaultOverride to`toSms(): SmsContent``new SmsContent($this->smsBody())`rarely`smsBody(): string``__($this->smsTranslationKey(), $this->smsBodyTranslationParams())`build the body differently`smsTranslationKey(): string``notifications.{slug}.sms`point at another translation key`smsBodyTranslationParams(): array``$this->toArray()`change the placeholders`SmsContent` wraps `public string $body` and rejects an empty body.

### `PushCapable` + `RendersPush`

[](#pushcapable--renderspush)

Same model as SMS: title and body translated service-side.

MethodDefaultOverride to`toPush(): PushContent``new PushContent($this->pushTitle(), $this->pushBody(), $this->pushData())`rarely`pushTitle(): string``__("{pushTranslationKey}.title", pushTitleTranslationParams())`custom title`pushBody(): string``__("{pushTranslationKey}.body", pushBodyTranslationParams())`custom body`pushData(): array``$this->toArray()`change the data payload sent to the device`pushTranslationKey(): string``notifications.{slug}.push`another key`pushTitleTranslationParams()` / `pushBodyTranslationParams()``$this->toArray()`change placeholders`PushContent` wraps `public string $title, public string $body, public array $data` and rejects an empty title or body.

Translations (SMS and Push only)
--------------------------------

[](#translations-sms-and-push-only)

Email is not translated here (Postmark owns the per-language wording). SMS and Push **are** translated, service-side, via `__()`.

### Who sends what

[](#who-sends-what)

A source application never sends translated text. It sends the **data** (raw values). The notifications service renders the final SMS/Push text in the recipient's locale, using those values as translation parameters.

```
source app            wire            notifications service              provider
data (values)  ────────────────►  __(key, params) in recipient  ──────►  final text
                                    locale                                (FCM / Vonage)

```

### Parameters are the data class properties

[](#parameters-are-the-data-class-properties)

`RendersSms` / `RendersPush` pass `$this->toArray()` (every constructor property) as the translation parameters. The Laravel placeholders (`:name`) in the translation string are therefore the **property names**.

Data class:

```
final readonly class OrderShippedData implements PushCapable, NotificationData
{
    use RendersPush;
    use SerializesFromConstructor;

    public function __construct(
        public string $order_number,
        public string $customer_name,
    ) {}

    public function notificationType(): NotificationType { return NotificationType::OrderShipped; }
}
```

Translation string (placeholders match the property names):

```
'order-shipped' => [
    'push' => [
        'title' => 'Commande expédiée',
        'body'  => 'Bonjour :customer_name, votre commande :order_number est en route',
    ],
],
```

The source app sends `OrderShippedData(order_number: 'A-123', customer_name: 'Marie')`; the service renders `Bonjour Marie, votre commande A-123 est en route` in the recipient's language.

### Where the translations live

[](#where-the-translations-live)

The text is rendered inside the **notifications service**, so the translations belong to it, not to each source application. They are registered once in the central translation system (`trustup-io-translations`) under the app name **`trustup-io-notifications`**, keyed by `notifications.{slug}.sms` / `notifications.{slug}.push.{title,body}`. Source apps (tools, marketplace, ...) define **no** notification translations.

### Decoupling parameters from the payload

[](#decoupling-parameters-from-the-payload)

By default the parameters are all properties. To send different placeholders than the raw payload, override the params methods:

```
protected function pushTitleTranslationParams(): array { return ['name' => $this->customer_name]; }
protected function pushBodyTranslationParams(): array  { return ['order' => $this->order_number]; }
```

Now the placeholders are `:name` / `:order`, independent of the property names. Same pattern for SMS via `smsBodyTranslationParams()`, and you can retarget the key with `smsTranslationKey()` / `pushTranslationKey()`.

Serialization
-------------

[](#serialization)

`SerializesFromConstructor` derives `toArray()` / `fromArray()` from the constructor by reflection:

- `toArray()`: one key per promoted constructor parameter, value read from the matching property. Nested `Serializable` values are serialized recursively.
- `fromArray($data)`: rebuilds the object; a missing key falls back to the constructor default, then to `null` if the parameter is nullable, otherwise throws `InvalidNotificationDataException` (the wire boundary fails loud, not with a raw `TypeError`).

Override either method for non-trivial mappings (enums, value objects).

The `NotificationType` registry
-------------------------------

[](#the-notificationtype-registry)

`Enums\NotificationType` is the single source of truth for the notification catalogue.

```
NotificationType::ToolsCommentNotification->value;              // 'tools.comment.notification'
NotificationType::ToolsCommentNotification->slug();             // 'tools-comment-notification'
NotificationType::ToolsCommentNotification->dataClass();        // ToolsCommentNotificationData::class
NotificationType::ToolsCommentNotification->source();           // Source::Tools
NotificationType::ToolsCommentNotification->supportedChannels(); // [NotificationChannel::Email]
NotificationType::forSource(Source::Tools);                     // list of types for a source
```

### Adding a new notification type

[](#adding-a-new-notification-type)

1. Add a case with its dotted value: `case FooBar = 'tools.foo.bar';`
2. Add a branch in `dataClass()` and in `source()` (both `match` throw on unmapped cases).
3. Create the `NotificationData` class implementing the capability interfaces for the channels it supports.

`slug()`, `supportedChannels()`, and `forSource()` need no changes; channels are derived from the interfaces.

`EmailTemplateLocaleGranularity`
--------------------------------

[](#emailtemplatelocalegranularity)

Controls how the notifications service suffixes the Postmark template alias with the recipient locale:

- `Language` (default): per-language template (e.g. `tools-comment-notification-en`).
- `Locale`: per-locale template (e.g. `tools-comment-notification-en-BE`).

Override `emailTemplateLocaleGranularity()` on a data class to opt into `Locale`. The suffix itself is applied service-side from the recipient's resolved locale.

`Recipient`
-----------

[](#recipient)

The addressee of a notification. Private constructor; use the factories.

```
use Deegitalbe\TrustupIoNotificationsContracts\Request\Recipient;
use Deegitalbe\TrustupIoNotificationsContracts\Enums\Source;

// Identified, source deferred to config: the client fills it from
// config('trustup-io-notifications.source') before publishing
Recipient::identified((string) $user->id);

// Identified with an explicit source
Recipient::identified((string) $user->id, Source::Tools);

// Anonymous: you supply the coordinates directly (at least one required)
Recipient::anonymous(email: 'a@b.com', phone: null, deviceTokens: [], locale: 'fr-BE');
```

- `identified(string $externalUserId, ?Source $source = null)`: the service looks up the recipient's coordinates and locale from the source system. The source is optional; when omitted, the client resolves it from `config('trustup-io-notifications.source')` at publish time (a single-source app therefore only needs the external id). If neither is set, publishing throws `MissingSourceException`.
- `anonymous(?string $email, ?string $phone, array $deviceTokens, ?string $locale = null)`: requires at least one coordinate, and a locale that normalizes via `LocaleNormalizer` (else `InvalidRecipientException`).
- `isIdentified(): bool`, `withSource(Source $source): self` (returns a copy; used to inject a default source).

An identified recipient serialized without a source (the deferred case) must have its source resolved before it crosses the wire. `fromArray()` throws `InvalidRecipientException` if it decodes an identified recipient with no source.

Wire types
----------

[](#wire-types)

You rarely build these directly (the client package does), but you read `StatusPayload` / `EngagementPayload` when handling feedback.

- `Envelope` — wire wrapper. `CURRENT_VERSION = 1`, a `direction` (`request` / `status` / `engagement`) and the matching payload.
- `RequestPayload` — `type`, `recipient`, `data`, `channels` (`null` = all supported, or a non-empty list; `[]` is rejected).
- `StatusPayload` — delivery status feedback: ```
    public string $sendId;
    public NotificationChannel $channel;   // email|sms|push
    public NotificationStatus $status;     // pending|sent|delivered|error
    public NotificationType $type;
    public NotificationData $data;
    ```
- `EngagementPayload` — engagement feedback: ```
    public string $sendId;
    public NotificationChannel $channel;
    public ChannelEventKind $kind;         // delivered|bounced|spam_complaint|opened|clicked|...
    public NotificationType $type;
    public NotificationData $data;
    public ?string $clickedUrl;
    ```

Locale helpers
--------------

[](#locale-helpers)

- `Support\LocaleNormalizer::normalize(?string $raw): ?string` — canonicalizes to `lang-REGION` (default region `BE`). Known languages: `fr, nl, en, de`; anything else returns `null`. Maps proprietary tags (`be-fr` → `fr-BE`).
- `Support\LocaleLanguageExtractor::language(string $locale): string` — extracts the language subtag (`fr-BE` → `fr`), used for `Language`-granularity email templates.

###  Health Score

38

—

LowBetter than 83% of packages

Maintenance100

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity38

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

3

Last Release

0d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/24230736?v=4)[Mathieu Henrotay](/maintainers/henrotaym)[@henrotaym](https://github.com/henrotaym)

---

Top Contributors

[![trustup-foreman[bot]](https://avatars.githubusercontent.com/in/1802739?v=4)](https://github.com/trustup-foreman[bot] "trustup-foreman[bot] (3 commits)")

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/deegitalbe-laravel-trustup-io-notifications-contracts/health.svg)

```
[![Health](https://phpackages.com/badges/deegitalbe-laravel-trustup-io-notifications-contracts/health.svg)](https://phpackages.com/packages/deegitalbe-laravel-trustup-io-notifications-contracts)
```

###  Alternatives

[spatie/laravel-pdf

Create PDFs in Laravel apps

1.0k4.8M48](/packages/spatie-laravel-pdf)[rawilk/profile-filament-plugin

Profile &amp; MFA starter kit for filament.

3914.8k](/packages/rawilk-profile-filament-plugin)[harris21/laravel-fuse

Circuit breaker for Laravel queue jobs. Protect your workers from cascading failures.

45955.7k](/packages/harris21-laravel-fuse)[vormkracht10/laravel-mails

Laravel Mails can collect everything you might want to track about the mails that has been sent by your Laravel app.

24957.5k](/packages/vormkracht10-laravel-mails)[stephenjude/filament-jetstream

A Laravel starter kit built with Filament inspired by Jetstream.

17760.2k3](/packages/stephenjude-filament-jetstream)[lettermint/lettermint-laravel

Official Lettermint driver for Laravel

1190.2k1](/packages/lettermint-lettermint-laravel)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
