toplan/laravel-sms
Laravel SMS is a flexible package for sending text messages and verification codes in Laravel apps. It offers pluggable gateways, message templates, rate limiting, queue support, events, and a clean API for reliable SMS delivery, ideal for OTP logins and notifications.
This is a patch release (2.6.5) of a database-backed logging package, so there is no migration path from a prior assessment — treat this as the baseline. Two changes matter: log records can now store an array of phone numbers in a single entry, and an update failure bug is fixed.
Require the package at ^2.6.5, then publish and run its migrations so the log table matches what the new array-storage code expects:
composer require <vendor>/<package>:^2.6.5
php artisan vendor:publish --tag=<package>-config
php artisan vendor:publish --tag=<package>-migrations
php artisan migrate
Read the config file first. Look for the log connection, table name, retention/pruning settings, and any queue toggle. Confirm the phone-number column is a JSON-capable type (json/longtext with a cast) rather than varchar, since 2.6.5 writes arrays into it.
First use case: log an outbound SMS/notification to one or more recipients in one write.
Log::channel('database')->info('sms.sent', [
'phones' => ['+8613800000001', '+8613800000002'],
'message' => 'Your code is 123456',
]);
If the package exposes a model/record API rather than a channel, the equivalent is a single create() with an array of numbers — check the package's formatter or DTO for the exact key name.
One record per event, many recipients. Before this release you either wrote one row per number or stored a delimiter-joined string. Now the natural pattern is a single log row per send, with the recipient list as an array. This keeps message-level metadata (template, provider response, cost) attached once instead of duplicated per recipient.
Querying by recipient. Array storage changes how you look logs up. Prefer the package's scopes if it ships any; otherwise use JSON containment, and index accordingly:
// MySQL 5.7+ / MariaDB 10.2+
Model::whereJsonContains('phones', $normalizedNumber)->latest()->get();
// SQLite / Postgres
Model::whereJsonContains('phones', $normalizedNumber); // Postgres uses jsonb
If your database can't index JSON containment efficiently at your volume, keep a denormalized primary_phone column alongside the array for the hot lookup path.
Delivery-status updates. The fixed update path is the one to build on. A typical workflow: create the log row at dispatch, then update it from the provider's webhook or callback with status, provider_message_id, and delivered_at. Since the update bug is resolved in 2.6.5, remove any saveQuietly() / raw-query / delete-and-recreate workarounds you added previously — they'll now mask real failures.
Wrap it in a service, not in controllers. Put logging behind a small class (SmsLogger::sent(), SmsLogger::failed()) that normalizes numbers, redacts message bodies if needed, and dispatches to the package. That isolates you from future key/schema renames — this package is on a 2.x line and the array format is brand new.
Queue the writes. A database log written synchronously inside a request adds latency and a failure mode to your critical path. If the package supports a queued channel or you wrap it in a job, use it — especially for bulk SMS where you might log hundreds of recipients per request.
Custom model / channel extension. If the package allows swapping the log model, extend the shipped one and add casts ('phones' => 'array'), scopes (scopeForPhone), and accessors there rather than in application code.
+8613800000001), not the raw user input. Array storage makes whereJsonContains exact-match against whatever formatting you happened to persist; mixed formats will silently fail to match in lookups.array_unique() and array_values() before handing it over, otherwise log rows differ for identical sends and comparisons become unreliable.varchar/text. The new array writes will succeed on text but whereJsonContains will not. Check the actual column type after migrating and cast the attribute to array/json on the model.updated_at never moved, or partial attribute writes), any code branching on those stale values will now start seeing fresh data. Audit webhook handlers and status dashboards after upgrading.$fillable/$guarded problem that looked like a package bug. If an update still doesn't persist after 2.6.5, check fillable attributes and dirty-attribute detection before reopening an issue.~2.6.5 and add a test asserting the stored JSON shape so a future 2.x patch can't quietly change it.How can I help you explore Laravel packages today?