calisero/calisero-php
A PHP SDK for integrating Calisero into Laravel and other PHP applications. It offers a lightweight client for authenticating with the Calisero API and making requests, with straightforward configuration and clean, reusable calls to help add Calisero-powered functionality quickly.
This is a framework-agnostic API client, not a Laravel package — there is no service provider, facade, config file, or migration. Budget an hour to read it before wiring it in.
Confirm compatibility first. Check the package's composer.json for its PHP constraint and its HTTP dependency (Guzzle 7 vs. PSR-18). A one-star package released recently can easily pin a Guzzle major that conflicts with something else in your lockfile. Install with an exact constraint so a silent patch can't change request behavior under you:
composer require calisero/calisero-php:"^1.0" --prefer-dist
Read the source, not just the README. For packages at this scale, src/ plus tests/ is the documentation — the tests show the real request shape, required parameters, and how errors surface. Note the client class name and whether the constructor takes an API key, an HTTP client, or both.
Put credentials in config/services.php, never in code. Add a block alongside your other third-party services and back it with env vars (CALISERO_API_KEY, CALISERO_SENDER_ID). Cache the config in deploy as usual.
First use case: prove the credentials from tinker. Send a single message to your own phone before building anything:
$client = app(App\Support\Sms\SmsClient::class);
$client->send(to: '+15551234567', body: 'Calisero smoke test');
Log the raw response and, critically, the message ID it returns — you will need that ID for every delivery-status lookup later. If this step is painful, the rest of the integration will be too, and it is worth evaluating a Laravel-native alternative (Vonage, Twilio, or a Notification channel package) before you commit.
Wrap the vendor client in your own interface. This is the single highest-value decision. The package is a thin transport wrapper around one vendor; do not let its class names leak into controllers, jobs, and notifications. Define App\Support\Sms\SmsClient with a send() method, implement it with the vendor client, and bind it as a singleton in AppServiceProvider. Swapping providers later becomes a one-file change.
// AppServiceProvider::register()
$this->app->singleton(SmsClient::class, fn () => new CaliseroSmsClient(
// construct the vendor client exactly as its README shows,
// reading config('services.calisero.*') — never env() outside config
));
Send from a queued job, not the request cycle. SMS gateways are the slowest and flakiest dependency in most apps. A ShouldQueue notification or a dedicated SendSms job keeps a carrier timeout from becoming a 504 on your checkout page, and gives you retries and a failure path for free.
The real day-to-day pattern is a custom Notification channel. Once the adapter exists, this is where 90% of your usage lives — password resets, order updates, OTPs:
// app/Channels/CaliseroChannel.php
class CaliseroChannel
{
public function __construct(private SmsClient $sms) {}
public function send(object $notifiable, Notification $notification): void
{
$message = $notification->toCalisero($notifiable);
$this->sms->send(
to: $notifiable->routeNotificationFor('calisero'),
body: $message->body,
);
}
}
Then via() returns [CaliseroChannel::class] and each notification exposes a toCalisero() returning a small DTO. Keep the DTO dumb — body text, optional sender override, optional scheduled-at.
Route phone numbers through the notifiable, not the call site. Implement routeNotificationForCalisero() on your User (or a HasSms trait) so opt-outs, number changes, and per-tenant sender IDs are handled in one place.
Testing: fake the adapter, not the network. Because you own the interface, tests stay trivial and fast:
$this->app->instance(SmsClient::class, $fake = new FakeSmsClient());
$this->post('/password/email', [...]);
$fake->assertSentTo('+15551234567');
Do not rely on Http::fake() — this package almost certainly uses Guzzle directly, not Laravel's HTTP client, so Http::fake() will not intercept it. If you need to test the adapter itself, check whether the vendor client accepts a PSR-18/Guzzle client in its constructor and inject a MockHandler stack.
Delivery receipts: webhook if offered, polling otherwise. If Calisero supports a DLR callback, add a controller and register it in routes/api.php. In Laravel 11+ you must explicitly exclude that path from CSRF in bootstrap/app.php (the old VerifyCsrfToken::$except array is gone), and you should verify the signature/shared secret the vendor documents — an unauthenticated endpoint that flips message statuses is an easy target. Otherwise, schedule a checkStatus job per message with increasing backoff, capped at a few hours.
Persist every outbound message. A small sms_messages table with provider_message_id, to, status, segments, cost, and error_code pays for itself the first time a customer says "I never got the code." Correlate with your existing logs by the message ID.
Zero dependents and one star is a maintenance signal. Treat the package as a thin, replaceable wrapper: pin the version, read the diff before bumping, and assume you may be the one fixing a bug. Your adapter interface is what makes that survivable.
Distinguish transport errors from API errors. Guzzle throws RequestException on 4xx/5xx, but many SMS APIs return HTTP 200 with an error payload ({"status":"error"}). Handle both; a bare try/catch on the Guzzle exception will silently swallow rejected sends.
Retrying can double-charge and double-text. Only retry on connection failures and 5xx responses you're confident occurred before the message was accepted. If the vendor supports an idempotency key or a client-side reference, use it — $tries = 3 on a job that sends SMS without one is a bug, not a feature.
Phone numbers must be E.164. +15551234567, no spaces, no parentheses, no leading 00. Validate and normalize at the boundary; carrier-side rejection errors are far less readable than a validation rule.
Segment math drives cost. A single emoji or curly quote drops a message from GSM-7 to UCS-2, cutting the per-segment character budget roughly in half. If you send templated text with customer names or currency symbols, count segments before send or you'll discover the bill later. Keep SMS bodies short and ASCII when you can.
Sender IDs are country-specific. Alphanumeric sender IDs are rejected outright in some markets (notably the US and Canada, where long codes/toll-free are required), and short codes must be provisioned. Make the sender configurable per notification, not hardcoded globally.
Never log the API key or the raw client. Log the message ID, recipient (masked), and status. Framework debug tooling (Telescope, Ray) dumping the constructed client will happily serialize your credentials.
Sanitize user-supplied text. SMS bodies assembled from user input can be used to inject URLs or premium-rate numbers. Treat the body as untrusted output.
Extension points are limited to the HTTP layer. If the vendor client lets you pass a Guzzle handler stack, that's your hook for logging, metrics, and retries — configure it once in the container binding. If it doesn't expose one, do the instrumentation in your adapter, and don't poke at the client's private state.
Watch the client's statefulness. API clients are usually safe as singletons, but if the vendor's client accumulates per-request options (default sender, timeout), a singleton will leak them across requests in queue workers. If in doubt, bind it as scoped and let the worker reset it per job.
How can I help you explore Laravel packages today?