Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Gatewayapi Notifier Laravel Package

symfony/gatewayapi-notifier

Symfony Notifier bridge for GatewayApi SMS delivery. Configure via gatewayapi://TOKEN@default?from=FROM DSN, then send SmsMessage with optional GatewayApiOptions such as class, callback URL, user reference, and label.

View on GitHub
Deep Wiki
Context7

Getting Started

Reality check first. This is a Symfony Notifier transport bridge, not a Laravel package. It ships no service provider, no facade, no NotificationChannel, no config file, and no queue integration. Laravel does not ship symfony/notifier, so "install and go" does not exist. What you actually get is ~200 lines that turn gatewayapi://TOKEN@default?from=SENDER into a TexterInterface that POSTs to GatewayAPI's /rest/messages. Everything else — config, DI, queues, retries, receipts — is yours to write.

Minimal steps (30–60 min including first send):

composer require symfony/notifier symfony/http-client symfony/gatewayapi-notifier

Version pin matters: Symfony 8.x requires PHP ≥ 8.4. If you're on Laravel 10/11 with PHP 8.1–8.3, pin to the 6.4 or 7.x line of the bridge and verify:

composer why-not symfony/gatewayapi-notifier 8.0

Then, because nothing is auto-wired, build the texter yourself in a service provider:

// app/Providers/NotifierServiceProvider.php
use Symfony\Component\Notifier\Texter;
use Symfony\Component\Notifier\Transport;

$this->app->singleton('gatewayapi.texter', fn () => Transport::fromDsn(
    config('services.gatewayapi.dsn')
));
// config/services.phpphp
'gatewayapi' => [
    'dsn' => env('GATEWAYAPI_DSN'), // gatewayapi://TOKEN@default?from=MyApp
],

First use case — transactional/OTP SMS, sent synchronously to prove the credential works:

use Symfony\Component\Notifier\Message\SmsMessage;

app('gatewayapi.texter')->send(
    new SmsMessage('+4520101010', 'Your code is 123456')
);

Where to look first, in order: (1) GatewayApiTransport.php in the package — it's short and tells you exactly which fields are sent and how errors surface; (2) GatewayApiOptions.php — this is the entire provider feature surface you get for free; (3) GatewayAPI's own REST docs for anything past that; (4) Symfony's SmsMessage / TexterInterface docs. Skip everything else — the release history is almost entirely "no significant changes," and the only feature added in recent memory was the label option.

Implementation Patterns

Pattern 1 — Thin client wrapper, not raw bridge calls. Never let Texter leak into controllers or jobs. Wrap it so you can swap to Http::post() later without touching call sites:

final class GatewayApiSms
{
    public function __construct(private TexterInterface $texter) {}

    public function send(string $e164, string $text, array $opts = []): void
    {
        $message = new SmsMessage($e164, $text);

        if ($opts) {
            $message->options(
                (new GatewayApiOptions())
                    ->class($opts['class'] ?? 'standard')
                    ->label($opts['label'] ?? null)
                    ->callbackUrl($opts['callback_url'] ?? null)
                    ->userRef($opts['user_ref'] ?? null)
            );
        }

        $this->texter->send($message);
    }
}

Pattern 2 — Laravel notification channel adapter. This is the pattern that makes the bridge earn its keep in a Laravel app. Implement Laravel's channel contract so it plugs into Notifiable / routeNotificationFor / Notification::send():

class GatewayApiChannel
{
    public function send(object $notifiable, Notification $notification): void
    {
        $sms = $notification->toGatewayApiSms($notifiable);
        app(GatewayApiSms::class)->send($notifiable->routeNotificationFor('gatewayApiSms'), $sms);
    }
}

Then Notification::send($user, new OrderShipped()) with via(['gatewayApiSms']) works like any other channel. This is the single highest-leverage piece of glue — it's what turns a Symfony island into a Laravel-native feature.

Pattern 3 — Queue it, always. The bridge is synchronous and has zero retry logic. Never call it inside a web request for anything user-facing:

class SendSms implements ShouldQueue
{
    public $tries = 3;
    public $backoff = [5, 30, 120];

    public function __construct(public string $to, public string $text, public array $opts = []) {}

    public function handle(GatewayApiSms $sms): void
    {
        $sms->send($this->to, $this->text, $this->opts);
    }
}

Caveat: GatewayAPI returning 200 means accepted, not delivered. Retrying on HTTP failure is safe-ish; retrying on HTTP 200 is a duplicate-SMS generator. Use user_ref (mapped from GatewayApiOptions::userRef()) as your idempotency key so you can reconcile duplicates later against GatewayAPI's message log.

Pattern 4 — Options as a first-class part of your DTO, not an afterthought. The four knobs the bridge exposes map directly to GatewayAPI fields, and each one has an operational purpose:

Option Real use
class('standard'|'premium') Premium routes for OTP latency; standard for marketing blasts. Premium costs more — gate it.
callbackUrl() Tells GatewayAPI where to POST delivery receipts. It does not create the endpoint.
userRef() Your correlation ID — join DLRs back to your app records.
label() Non-unique tag for cost reporting / analytics in the GatewayAPI dashboard.

Pattern 5 — Multi-provider fan-out via Symfony Notifier only if you commit. If you already use symfony/notifier for Slack/email (e.g. a legacy Symfony service in the same estate), then Transport::fromDsn with per-environment DSNs is genuinely elegant: gatewayapi://... in prod, null:// in tests, and Symfony's Transports class for failover. If you don't already have Notifier wired, this abstraction is not worth introducing — you'd be adopting a second notification stack to get one HTTP POST.

Pattern 6 — Test with MockHttpClient, not Http::fake(). The bridge uses symfony/http-client; Laravel's Http::fake() will not intercept it and your tests will hit the real API. Inject Symfony's mock via the third fromDsn argument:

$mock = new MockHttpClient(new MockResponse('', ['http_code' => 200]));
$texter = Transport::fromDsn('gatewayapi://token@default?from=Test', null, $mock);

Assert on the request body to validate your GatewayApiOptions mapping — that's where regressions actually live.

Gotchas and Tips

Phone numbers are not normalized, at all. Neither SmsMessage nor the bridge touches the recipient. GatewayAPI expects msisdn in E.164 (+4520101010). A bare 20101010 will either fail at the API or route wrongly. Put normalization (PhoneNumber::parse($input, $defaultRegion) → +E164) in front of the wrapper and make it the only entry point. This is a one-day task that prevents a category of silent misroutes.

The DSN is a bearer credential with a URL-encoding trap. gatewayapi://TOKEN@default?from=SENDER — the token sits in the userinfo position. If your OAuth token contains @, :, /, or ?, parse_url will misparse it and you'll get an auth failure that looks like a token expiry. Always rawurlencode() the token when composing the DSN, and store it in a secrets manager, not .env committed to git. Rotation means rebuilding the texter — since you registered it as a singleton, that requires a config cache clear unless you bind it as a scoped/lazy singleton.

from is effectively mandatory. GatewayAPI requires a registered sender name on most routes. Sender ID registration with carriers (DK/NO/SE/FI especially) takes 1–3 weeks of calendar lead time and is an ops/legal task, not a code task. Start it day one; you cannot test production delivery until it's approved.

callbackUrl is a pointer, not a feature. The README's example makes it look like receipts are handled. They aren't. You must build the endpoint, and the bridge gives you nothing for it: no route, no signature verification, no payload parsing, no idempotent processing. GatewayAPI's DLR POSTs are unsigned (verify current docs, but historically IP-allowlist based) — so your endpoint needs an allowlist and must tolerate replays keyed on user_ref. Budget 2–3 days and don't let anyone believe it's included.

Errors surface as exceptions, not return values. Texter::send() throws Symfony\Component\Notifier\Exception\TransportException on HTTP/API failure. Because Laravel's exception handler is HTTP-centric, catch it in your job and translate it: 429 → retry with backoff, 401 → page on-call (token rotated/invalid), 400 with an msisdn validation message → mark the contact as bad, don't retry. Blind retries on 400 are pure noise.

There is no batching. SmsMessage takes exactly one recipient, so a 500-person campaign is 500 HTTP calls through 500 jobs. Rate-limit the queue (redis driver with ->onConnection('sms') and a throttle middleware at the worker level) or you'll trip GatewayAPI's rate limits and burn retries. If you need true batching, you must bypass the bridge and call the REST API directly.

Long messages and segment costs are invisible. The bridge does not expose split_count or encoding. A 200-character message is silently billed as multiple segments. If cost matters, enforce a character budget in your wrapper and log the computed segment count yourself before sending.

You're boxed in by GatewayApiOptions. It exposes a fixed set (class, callbackUrl, userRef, label). GatewayAPI's REST API supports more — scheduling, validity, flash SMS, encoding. The options value object will reject unknown keys, so anything extra means either a PR upstream or abandoning the bridge for a direct Http:: call. Check this before you design around a feature the bridge doesn't carry.

Don't read 6 stars / 0 dependents as abandonment. This repo is a Symfony subtree split; real usage flows through symfony/notifier, and the symfony/symfony monorepo issue tracker. Equally, don't read the Symfony brand as a vendor SLA for your integration — the release notes are overwhelmingly "no significant changes," so expect stability, not evolution, and no one upstream is thinking about Laravel ergonomics.

Extension points, such as they are: (1) wrap the texter and translate exceptions there; (2) inject a Symfony\Component\Notifier\Transport with a custom HttpClientInterface for retry/timeout policy at the HTTP layer — cleaner than retrying jobs; (3) wire an EventDispatcherInterface as the second fromDsn argument to get MessageEvent/FailedMessageEvent for observability, which is the closest thing to hooks the ecosystem offers. If you find yourself needing a fourth, that's the signal to drop the bridge and write the 80-line Laravel client instead.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
steadrun/laravel-monitor
labyrinthe/connect-laravel
elmogy/layan
drewdan/sentinel-client
dskripchenko/laravel-llm
fatryst/nuonuo
stevenay/myanfont
gmostafa/php-graphql-client
xeviant/paystack
snowsoft/faker
meditationsteps/blogetc
animalyoung/laravel-influxdb
onix-solutions/sms-telera
lonely-lights/prosetta
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin