This package adds a Mailjet SMS transport to Laravel in two shapes: a notification channel (MailjetSmsChannel) so SMS rides alongside mail/database in via(), and a direct send facade/client for one-off messages. It is small and single-purpose — expect a config file, a channel, a message object, and a thin HTTP wrapper around Mailjet's v4 SMS endpoint. Nothing about your existing notification flow changes.
Install and configure
composer require lab404/laravel-mailjet-sms
php artisan vendor:publish --tag=mailjet-sms-config # confirm tag in README
MAILJET_SMS_TOKEN=your-sms-token
MAILJET_SMS_SENDER=MyApp
Two things to sort out before writing code, because they cause the most day-one friction:
Where to look first
config/mailjet-sms.php — token, sender, any base URL/endpoint override.Lab404\MailjetSms\Channels\MailjetSmsChannel) — this defines the exact method name it looks for on your notification (toMailjetSms() in the typical 404lab convention; verify against the source).toMailjetSms(), so its fluent API is your day-to-day surface.First use case: order/OTP notification
use Illuminate\Notifications\Notification;
use Lab404\MailjetSms\Channels\MailjetSmsChannel;
use Lab404\MailjetSms\Messages\MailjetSmsMessage;
class OrderShipped extends Notification
{
public function via($notifiable): array
{
return [MailjetSmsChannel::class];
}
public function toMailjetSms($notifiable): MailjetSmsMessage
{
return (new MailjetSmsMessage())
->to($notifiable->phone) // E.164: +33612345678
->content("Order #{$this->order->id} is on its way.");
}
}
Add the Notifiable trait to your model if it isn't there, and implement routeNotificationForMailjetSms() when the destination phone isn't a simple phone column. That single method is the cleanest integration point for multi-tenant or "notify a different number than the account holder" cases.
Notification channel is the default; direct send is the escape hatch. Put anything triggered by a domain event through Notification — you get queueing, Notification::fake() in tests, and a single via() switch per user preference. Reserve the facade/client for cases where there's no notifiable (health-check pings, admin console "send test SMS", batch scripts).
// Direct send — check the actual facade accessor in the README;
// 404lab packages typically register an alias and a container binding.
MailjetSms::send('+33612345678', 'Deploy finished');
Routing via the notifiable is the pattern that scales. Instead of hardcoding ->to() in every notification, centralize it:
public function routeNotificationForMailjetSms(Notification $notification): ?string
{
return $this->phone_e164 ?: $this->phone;
}
Return null to skip — but see the gotcha below, since silent skips are easy to miss.
Queue everything. SMS is an external HTTP call inside your request cycle otherwise. Either implement ShouldQueue on the notification or dispatch the notification from a job. Because there's typically no retry logic inside the package, wrap it yourself:
class SendShipmentSms implements ShouldQueue
{
public int $tries = 3;
public array $backoff = [10, 60, 300];
public function handle(): void
{
Notification::route('mailjetSms', $this->phone) // on-demand route
->notify(new OrderShipped($this->order));
}
}
Notification::route() is the cleanest way to notify a number that has no model — good for guest checkout confirmations.
Keep message construction boring. A single message class, .-concatenated short strings, no markdown. If you need templating, render a tiny Blade string first and only then hand it to the message object:
->content(view('sms.order-shipped', ['order' => $this->order])->render())
Testing. Use Laravel's standard fakes — the channel is just another notification channel, so Notification::fake() plus assertSentTo() covers it, and you never hit the network. For the direct-send path, bind a fake into the container or Http::fake() the Mailjet endpoint (the package uses the HTTP client, so the standard fake intercepts it):
Http::fake(['api.mailjet.com/*' => Http::response(['status' => 'success'])]);
Local/staging. Guard with an environment check in via() — return an empty array or a Log channel in local — rather than disabling the token. It keeps the code path exercised and prevents an accidental production SMS bill from a seeder.
Phone numbers must be E.164. +33612345678, never 06 12 34 56 78, never (555) 010-9999. Mailjet won't normalize for you, and a malformed number often shows up as a silent non-delivery rather than a validation error. Normalize on write (a phone cast/mutator) and validate with phone:AUTO at the boundary.
A null from routeNotificationForMailjetSms() fails silently. The channel typically just returns early. When an SMS "isn't sending," check for a null/blank phone before you touch the config — it's the single most common cause.
No delivery receipts out of the box. Mailjet SMS supports status callbacks, but this package (last release Feb 2023, 5 stars, effectively one maintainer) does not ship webhook handling or a status model. If you need "delivered vs. failed," you're implementing a callback endpoint and a status column yourself, and parsing Mailjet's payload. Plan for that before you promise anyone delivery analytics.
Content length and encoding. GSM-7 gets ~160 chars/segment; a single curly quote, emoji, or accented character flips the whole message to UCS-2 at ~70 chars/segment, and a 161-character GSM message costs two SMS. Budget content length explicitly and strip smart quotes from rendered Blade.
Config caching. After changing .env in a deployed environment, php artisan config:cache again. MAILJET_SMS_TOKEN read via env() outside a config file returns null once cached — make sure the package config actually reads it (it should) rather than the package calling env() directly.
Errors are not retried and often surface as generic exceptions. Wrap sends in try/catch or a queued job with $tries/$backoff, and log the response body. Mailjet returns useful detail (invalid sender, insufficient credit, blocked country) that's easy to lose behind a generic RequestException.
Laravel version constraints. With a 2023 release and essentially no dependents, check composer.json for the supported Laravel range before upgrading. On newer Laravel you may need --with-all-dependencies or a fork. Vendor-publish and pin, or fork it into your packages/ directory — the surface is small enough to own.
Extension points, in order of usefulness:
dry-run mode that logs instead of sending. Best single lever.->sender() overrides or a ->template() helper without forking the package.Http::macro or middleware handles retries/timeouts more cleanly than editing vendor code.Sanity-check the README before committing. With a package this small and this stale, method names (toMailjetSms vs toSms, the facade alias, the publish tag) are worth confirming against the source once rather than trusting a blog post. Read the channel class — it's ~30 lines and tells you the entire contract.
How can I help you explore Laravel packages today?