Add A New Messaging Channel
Yaya sends messages over WhatsApp, SMS, Telegram, and reaches PWA users through a fourth channel value, all through one interface rather than per-integration branching in call sites. Adding a fifth channel means implementing that interface, registering the implementation, and — the part that is easy to skip — deciding where the new channel fits in the fixed resolution order that decides which channel to actually use for a given user. See Channel Abstraction for why this shape exists at all; this guide is about extending it.
Implement ChannelInterface¶
Every channel implements four methods:
interface ChannelInterface
{
public function getType(): ChannelTypeEnum;
public function send(User $user, string $message, array $options = []): SendResult;
public function sendToIdentifier(string $identifier, string $message, array $options = []): SendResult;
public function normalizeIdentifier(string $identifier): string;
}
[@channel-interface]. send() looks up the user's channel account and
delegates to sendToIdentifier(); sendToIdentifier() is what actually
talks to the underlying provider and is also usable for sending to a raw
identifier before a User record exists (for example, an unregistered phone
number). normalizeIdentifier() exists because different channels format
identifiers differently — SmsChannel normalizes phone numbers through its
SmsRoutingResolver, and each channel's send methods pass identifiers
through this before anything provider-specific happens
[@sms-channel].
If the new channel can also receive inbound messages (a webhook payload from
the provider, or a bot framework callback), implement
InboundChannelInterface instead, which extends ChannelInterface with one
additional method:
interface InboundChannelInterface extends ChannelInterface
{
public function parseInboundPayload(array $payload): InboundMessage;
}
[@inbound-channel-interface]. Both WhatsAppChannel and TelegramChannel
implement this extended interface, since both receive user replies through a
webhook/bot callback; SmsChannel implements only the base
ChannelInterface, since it is outbound-only in this codebase
[@sms-channel] [@telegram-channel].
send() and sendToIdentifier() both return a SendResult DTO regardless
of outcome — success or failure is a value on that object
(SendResult::success(...) / SendResult::failure(...)), not an exception.
Callers such as SendChannelMessage branch on $result->success rather than
catching exceptions for the ordinary failure path, so your implementation
should catch provider exceptions internally and translate them into a
SendResult::failure(...) with a useful error message, the way SmsChannel
does around every provider call [@sms-channel].
Add a ChannelTypeEnum case¶
ChannelTypeEnum is the single source of truth for which channel types
exist:
enum ChannelTypeEnum: string
{
case WhatsApp = 'whatsapp';
case Sms = 'sms';
case Pwa = 'pwa';
case Telegram = 'telegram';
public function label(): string { /* ... */ }
public function requiresPhoneNumber(): bool { /* ... */ }
}
[@channel-type-enum]. Add a new case here, its display label(), and update
requiresPhoneNumber() if the channel is identified by something other than
a phone number (Telegram and PWA return false; SMS and WhatsApp return
true) [@channel-type-enum]. This enum backs the channel column on channel
accounts and messages, so a channel implementation's getType() must return
the corresponding case.
Register the channel¶
ChannelManager is a plain registry keyed by ChannelTypeEnum::value:
public function register(ChannelInterface $channel): void
{
$this->channels[$channel->getType()->value] = $channel;
}
[@channel-manager]. It is populated once, in MessagingServiceProvider:
$this->app->singleton(ChannelManager::class, function () {
$manager = new ChannelManager;
$manager->register(new WhatsAppChannel);
$manager->register($this->app->make(SmsChannel::class));
$manager->register($this->app->make(TelegramChannel::class));
return $manager;
});
[@messaging-provider]. Add your new channel's registration here, using
$this->app->make(...) rather than new if the channel has constructor
dependencies (as SmsChannel and TelegramChannel do) [@messaging-provider].
Nothing auto-discovers channel implementations — a class that implements
ChannelInterface but is never registered here is simply invisible to
ChannelManager::channel() and ChannelManager::has(), both of which are
what the rest of the messaging pipeline calls to check availability
[@channel-manager].
Decide where the channel sits in ChannelResolver's preference order¶
ChannelResolver::resolve() is what actually picks a channel for a given
user when no specific channel is forced. It checks an explicit
preferredChannel first, then falls through a hardcoded preference order:
[@channel-resolver]. Telegram is checked first because pilot users in this
deployment are Telegram-only, and falling back to a phone-based channel for
them would require a phone number they were never asked for
[@channel-resolver]. WhatsApp is checked next, then SMS via a registered
channel account, then SMS again as a last-resort fallback if the user simply
has a phone_number on file even without an explicit SMS channel account
[@channel-resolver].
Adding a new channel means making an explicit decision about where in this
chain it belongs, and editing resolve() accordingly — this is the one place
in the codebase that decides which channel a user actually gets, so a new
channel that is only registered with ChannelManager but never inserted into
this resolution order will only ever be reachable when a caller passes it as
an explicit preferredChannel or forced_channel, never through normal
automatic resolution.
Reference implementations¶
SmsChannelis the most heavily built-out example: it resolves per-tenant routing and driver selection throughSmsRoutingResolver, supports a primary/fallback driver pair (including a custombongadriver), and normalizes provider-specific success/failure response shapes into a singleSendResultcontract [@sms-channel].TelegramChannelwraps a Nutgram bot instance, formats outgoing markdown into Telegram-flavored HTML, and — for alert messages carrying analert_idin their metadata — attaches an inline button that opens the Telegram Mini App [@telegram-channel]. See Telegram Bot for the wider bot architecture this channel sits inside.WhatsAppChannelwraps thecrenspire/laravel-whatsapppackage and resolves WhatsApp Business API credentials per FSP tenant.
For the job chain that calls into ChannelManager/ChannelResolver for both
single sends and bulk batches, see
Messaging Pipeline. If your new channel
also needs to fail gracefully mid-batch, read
Debug A Failed Or Silent Outbound Message for how
delivery failures are logged and surfaced.