laravel-notification-channels/vodafone
Send Vodafone SMS notifications from your Laravel app via this notification channel, with a simple message class and driver config to plug into Laravel's built-in notification system.
Install and wire up the config.
composer require laravel-notification-channels/vodafone
The channel follows the standard laravel-notification-channels shape: service provider auto-discovered via composer.json extra.laravel, credentials read from config/services.php, no migrations, no facade. There is nothing to publish in most versions of this family — you add a block to config/services.php directly:
'vodafone' => [
'username' => env('VODAFONE_USERNAME'),
'password' => env('VODAFONE_PASSWORD'),
'from' => env('VODAFONE_FROM'), // sender ID / short code
],
Confirm the exact key names before assuming: read config/vodafone.php inside the package if one ships, otherwise the package's src/VodafoneChannel.php constructor — that is the authoritative list. Do not trust the README alone, and do not trust env var names you've seen in other channels; they are not uniform across the family.
Where to look first, in this order.
vendor/laravel-notification-channels/vodafone/src/VodafoneChannel.php — the send() method tells you exactly what toVodafone() must return (a plain string, or a message object if one ships), what it does with the response, and which HTTP client it uses.vendor/laravel-notification-channels/vodafone/composer.json — the illuminate/* and php constraints. This package's last tagged release is mid-2022, so on a current Laravel you may resolve against an old constraint or not at all.vendor/laravel-notification-channels/vodafone/tests/ — if present, it is the only reliable documentation of response handling and error paths.First use case: a transactional SMS notification.
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Notification;
use NotificationChannels\Vodafone\VodafoneChannel;
class OrderShipped extends Notification implements ShouldQueue
{
use Queueable;
public function __construct(public string $orderRef) {}
public function via($notifiable): array
{
return [VodafoneChannel::class];
}
public function toVodafone($notifiable): string
{
return "Your order {$this->orderRef} has shipped.";
}
}
Then give the notifiable a route:
// app/Models/User.php
public function routeNotificationForVodafone(): ?string
{
return $this->phone_e164; // store E.164, not display format
}
Send it:
$user->notify(new OrderShipped('A-1042'));
// or
Notification::send($users, new OrderShipped('A-1042'));
On-demand, without a model:
Notification::route('Vodafone', '+447700900123')
->notify(new OrderShipped('A-1042'));
The string key in Notification::route() must match the channel's expected route key (Vodafone for most channels in this family, sometimes lowercased). Check VodafoneChannel for the fallback chain — some channels fall back to routeNotificationForSms() if the named method is absent, which is a useful compatibility hook if you already have an SMS abstraction.
Treat the notification class as the unit of message design. One class per message type, not one class with a type switch. It keeps toVodafone() short, makes assertSentTo in tests meaningful, and lets you vary locale per notifiable:
public function toVodafone($notifiable): string
{
return __('sms.order_shipped', [
'ref' => $this->orderRef,
'locale' => $notifiable->locale,
]);
}
Always queue, and constrain the retry. SMS is a network call on the request path otherwise. implements ShouldQueue plus:
public int $tries = 1; // an SMS retried is a duplicate SMS the user paid attention to
public int $timeout = 10;
public function backoff(): array { return [5, 30]; } // only if you also dedupe
Better than relying on tries: add a rate limiter on the job so bursts (a mass announcement) don't hit Vodafone's per-second ceiling:
public function middleware(): array
{
return [new RateLimited('vodafone-sms')];
}
// AppServiceProvider::boot()
RateLimiter::for('vodafone-sms', fn ($job) => Limit::perSecond(10));
Wrap the channel behind a one-method seam. This package has one star, zero dependents, and no release since 2022 — the probability you swap providers is higher than for a mainstream channel. A five-line contract means the swap never touches your notification classes:
interface SmsTransport {
public function send(string $to, string $body): void;
}
class VodafoneTransport implements SmsTransport
{
public function send(string $to, string $body): void
{
Notification::route('Vodafone', $to)->notify(new RawSms($body));
}
}
Bind it in a service provider; notifications depend on the contract, not the channel class. Your test suite then fakes SmsTransport and never needs the vendor package at all.
Observability via the framework's own events. The package likely emits nothing. Listen to Illuminate\Notifications\Events\NotificationSent and NotificationFailed, filter on VodafoneChannel::class, and log/metric there. That gives you delivery-attempt counts and failure rates without reading vendor code:
Event::listen(NotificationFailed::class, function ($e) {
if (in_array(VodafoneChannel::class, array_map('get_class', [$e->channel]))) {
Log::warning('vodafone.failed', ['notifiable' => $e->notifiable->getKey(), 'e' => $e->data]);
}
});
Testing.
Notification::fake();
$user->notify(new OrderShipped('A-1042'));
Notification::assertSentTo($user, OrderShipped::class, function ($n, $channels, $notifiable) {
return in_array(VodafoneChannel::class, $channels)
&& str_contains($n->toVodafone($notifiable), 'A-1042');
});
Notification::fake() proves your wiring. It does not prove the channel's HTTP layer. For that, check whether send() uses Illuminate\Support\Facades\Http or a raw Guzzle client: if HTTP facade, Http::fake([...]) gives you deterministic success, 401, and 429 paths for free; if raw Guzzle, you must bind a mocked ClientInterface into the container, which is fiddly — this is a good reason to keep the transport seam above.
Multi-channel fan-out. via() returning [VodafoneChannel::class, MailChannel::class] is the normal pattern; add a guard so users without a phone number get email only:
public function via($notifiable): array
{
return array_filter([
$notifiable->phone_e164 ? VodafoneChannel::class : null,
MailChannel::class,
]);
}
Version constraints are the first real obstacle. Published mid-2022, so expect illuminate/support: ^8|^9 and PHP ^7.4|^8.0. On Laravel 10+ the require may fail outright. Workarounds, in order of preference: (a) fork and loosen constraints, run the package's tests against your Laravel version; (b) composer require ... --with-all-dependencies and see if it resolves anyway; (c) don't adopt. Avoid --ignore-platform-reqs — it converts a resolvable problem into a runtime one. If you do fork, vendor it with a VCS repository entry and pin the commit; do not leave dev-main floating.
The package may bypass the HTTP facade. Many channels written in that era instantiate new \GuzzleHttp\Client() inside send(). Consequence: Http::fake() silently does nothing and your tests hit the real API. Check the constructor first. If it's Guzzle, either mock the container binding or — better — put the seam in front and test the seam.
env() outside config files returns null under config:cache. If you're tempted to read env('VODAFONE_FROM') in the notification, stop: it works locally and breaks in production. All credentials belong in config/services.php, and after editing that file run php artisan config:clear locally / redeploy the cache in production.
Phone number formatting is your problem, not the package's. Most of these channels pass the string straight through. Store E.164 (+447700900123), strip spaces and dashes before send, and reject rather than send garbage. Vodafone's API may accept a national format and quietly misdeliver or 400; either way you'll be debugging at the carrier level, which is slow. Add a normalisePhone() helper and a validation rule on the model attribute.
Returning null from toVodafone() — behavior depends on the version. Some channels treat it as a no-op (handy for "skip if nothing to say"), others will send "" or throw a type error. Verify in send() before you rely on it; if it's not handled, guard in via() instead.
Duplicates from queue retries. The most common production bug with SMS channels: the API call succeeds, then something later in the job fails, the job retries, and the user gets two texts. Set $tries = 1, or store a sent_notifications marker keyed on (notifiable, notification class, dedupe key) and short-circuit at the top of toVodafone()-adjacent logic. Also confirm the channel throws on HTTP errors at all — if it swallows them, your queue reports success and SMS vanishes silently. Test this with Http::fake(['*' => Http::response(null, 500)]) and assert an exception.
Route key mismatch on anonymous notifiables. Notification::route('Vodafone', ...) vs Notification::route(VodafoneChannel::class, ...) vs 'vodafone' — the framework matches the string, and a typo means "no route, silently skipped" (or an exception, depending on version). It is a five-minute bug and a twenty-minute diagnosis.
Staging will text real people. There's no documented sandbox. Put a hard guard in the transport seam — route all non-production SMS to an allowlist number or log instead of send. Do this before the first deploy, not after.
Octane / long-lived workers. If the channel resolves config or builds an HTTP client in a constructor or singleton, that state is captured at boot and survives config reloads and credential rotation. Verify by running the same notification before and after a config change under Octane; if it's stale, resolve per-send rather than caching the client.
Extension point when you need it. The clean override is not subclassing the channel but replacing it: register your own notification channel class, bind it to the same via() slot, and implement send($notifiable, $notification) to call $notification->toVodafone($notifiable). That preserves every call site while giving you retries, failover, and logging inside a class you own — which, given the maintenance signal on this package, is where you want the logic to live.
How can I help you explore Laravel packages today?