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.
What the package actually is. symfony/gatewayapi-notifier is a thin Symfony Notifier transport bridge — roughly a DSN parser, a SmsMessage → POST /rest/messages mapper, and a GatewayApiOptions value object exposing class, callbackUrl, userRef, and label. It does not queue, retry, normalize phone numbers, receive delivery receipts, or validate callbacks. Everything operationally interesting happens around it, not in it. Any prior briefing that describes this as a webhook/retry platform should be corrected before budget discussion.
Architecture fit: poor for a Laravel-first codebase. This is a Symfony Notifier bridge; Laravel has its own notification abstraction (Illuminate\Notifications) with a channel contract and queue semantics. Adopting the bridge introduces a second notification stack alongside the existing one. Symfony components do coexist in Laravel (symfony/console, http-foundation, process are already transitively present), so this is friction, not impossibility — but it's friction with no offsetting benefit when the underlying capability is a single HTTP POST.
Integration feasibility: high, but most of the effort is outside the package. A spike is 2–3 days; production readiness is 1–2 sprints. The disproportionate work is adjacent:
| Work item | Owner | Effort |
|---|---|---|
| Composer install, DSN config, first send | Backend | ~0.5 day |
Laravel channel adapter (or TexterInterface binding) |
Backend | 1–2 days |
| Phone normalization to E.164 | Backend | 1 day |
| Queued send with retry/backoff | Backend | 1 day |
| Delivery-receipt webhook + signature/allowlist validation | Backend | 2–3 days |
| Sender ID registration (DK/NO/SE/FI carrier lead time) | Ops/Legal | 1–3 weeks calendar |
| Fallback provider + circuit breaker | Backend/SRE | 2–4 days |
| Segment/cost monitoring, opt-out and quiet-hours compliance | Product/Legal | ongoing |
Technical risk.
| Risk | Severity | Notes / Mitigation |
|---|---|---|
| Framework mismatch (Symfony bridge in Laravel) | Medium | Solves a problem we may not have. Prefer a ~80-line Laravel-native client. |
| PHP version pinning | Medium | Symfony 8.x requires PHP ≥ 8.4; 7.4.x targets 8.2/8.3. Pin to the Symfony minor matching our PHP baseline; validate with composer why-not. |
| Dependency surface expansion | Low–Medium | +4–6 Symfony packages for one HTTP call. Audit via composer licenses / SBOM. |
| Misread maturity signals | Low | 6 stars / 0 dependents is an artifact of Symfony's subtree-split repos; real usage flows through symfony/notifier. Not abandoned; also not a vendor SLA. |
| Feature stagnation | Low | Release notes are dominated by "no significant changes"; only material addition in two years was the label parameter. Stable, not evolving. |
| Provider fit | High if wrong | GatewayAPI is strong in the Nordics/EU and thin elsewhere. Provider choice dominates the decision far more than package choice. |
| OAuth token embedded in DSN | Medium | Bearer credential in configuration; requires secrets-manager sourcing and rotation runbook. |
| No built-in retries or DLR handling | High | Must be built. API acceptance ≠ delivery. |
Key questions for the team.
symfony/notifier? If not, the honest comparison is "this package" vs. "one Laravel service class calling the same REST API" — and the latter wins on dependency count, debuggability, and ownership.callbackUrl only says where GatewayAPI sends receipts; we still have to receive them.Notification facade already provides it.Recommendation: do not adopt the package in a Laravel-first stack. Reimplement the ~80 lines it saves. The bridge's value is making GatewayAPI a first-class citizen of Symfony Notifier. If we don't run Symfony Notifier, we inherit the dependency tree without the abstraction benefit.
Preferred path (Laravel-native).
App\Services\Sms\GatewayApiClient — a single service using Http::withBasicAuth($token, '') against https://gatewayapi.com/rest/messages, with sender, message, recipients[].msisdn, plus the same optional fields the bridge exposes (callback_url, user_ref, label, class). ~60–90 lines including error mapping.App\Notifications\Channels\GatewayApiSmsChannel implementing Laravel's channel contract, so it plugs into Notifiable::routeNotificationFor() and existing Notification::send() calls.App\Jobs\SendSms (ShouldQueue, $tries = 3, exponential backoff, failed() handler) for async delivery and retry — capability the DSN example implies but the package does not provide.config/services.php entry for token and sender, sourced from the secrets manager. Never a committed DSN.Alternate path (adopt the bridge) — only if one of these is true: we are mid-migration from Symfony and already run symfony/notifier; or we intentionally standardize on Symfony Notifier's transport model across SMS/Slack/email in a non-Laravel service. In that case, still isolate it behind an internal SmsSender interface so the transport can be swapped.
Compatibility and sequencing.
| Step | Action | Gate |
|---|---|---|
| 1 | Confirm GatewayAPI account, sender ID registration lead time, pricing for target geos | Ops sign-off |
| 2 | Spike: send one SMS from staging via native client and (optionally) via bridge; compare LOC and dependency delta | Tech decision |
| 3 | Land GatewayApiClient behind an SmsSender contract; wire notification channel |
Code merged |
| 4 | Add queued job, backoff, and failure alerting | Load test passes |
| 5 | Build DLR endpoint (signature/allowlist), persist delivery status | DLR verified in staging |
| 6 | Configure fallback provider + circuit breaker; enable segment/cost dashboards | Runbook published |
If the bridge is chosen: pin symfony/notifier to the minor matching our PHP runtime (^7.4 on 8.2/8.3, ^8.1 on 8.4+), and bind Symfony\Component\Notifier\TexterInterface through a container adapter so application code never depends on Symfony types directly.
Maintenance. The package itself is low-churn — recent release notes are almost entirely "no significant changes" — but Symfony's annual major cadence will force dependency bumps on our schedule, not theirs. MIT licensed, maintained inside the symfony/symfony monorepo, no commercial SLA. Expect scheduled upgrades with occasional breaking changes inherited from Symfony's PHP floor.
Support model. Framework-level support comes from the Symfony community and Symfony security advisories. SMS deliverability, rate limits, and carrier relationships come from GatewayAPI — separate escalation path, separate contract. A production incident may involve two vendors plus our own queue infrastructure; document that chain in the runbook.
Scaling. The bridge is stateless; throughput is bounded by GatewayAPI account rate limits and HTTP client concurrency. Scale is achieved by scaling queue workers, not the package. Watch message segmentation (160 GSM-7 / 70 UCS-2 chars) since cost and rate-limit consumption scale per segment, not per message.
Failure modes to design for.
symfony/notifier minors and our PHP runtime — caught by CI's composer why-not check, not by code review.Ramp-up. A mid-level PHP developer is productive on the bridge or the replacement client in roughly one day. The real learning curve is the surrounding surface — queue semantics, DLR handling, phone normalization, and opt-out/quiet-hours compliance — which is where I'd expect a new engineer to spend their first week. Estimated cost to MVP: 2–3 developer days. Production-grade with DLRs, monitoring, and fallback: 1–2 sprints plus the sender-ID registration calendar time, which should be started immediately since it gates launch independently of code.
How can I help you explore Laravel packages today?