kronas/smpp-client-bundle
Laravel bundle that wraps an SMPP client, letting PHP applications send SMS through an SMSC using the SMPP protocol. Configure the SMSC host, port, and credentials, then submit messages directly from your Laravel app without handling raw SMPP sockets yourself.
The first thing to internalize: kronas/smpp-client-bundle is Symfony2 DI glue, not an SMS API. It maps bundle configuration onto the constructor of SM\SMPPTransport from onlinecity/php-smpp and registers the client as a container service. There is no Laravel equivalent of AppKernel, so "getting started" splits into two paths.
Path A — you actually have a Symfony 2.x / PHP 5.x app (the only path where the bundle itself is used):
composer require kronas/smpp-client-bundle
// app/AppKernel.php
new Kronas\SmppClientBundle\KronasSmppClientBundle(),
# app/config/config.yml
kronas_smpp_client:
host: smpp.provider.example
port: 2775
login: my_system_id
password: my_password
sender: "MyBrand"
Then find the real service id before writing a line of code — this is the single biggest first-hour time sink, because bundle naming conventions are unpredictable:
php app/console container:debug | grep -i smpp
container:debug <service_id> shows the class and constructor arguments, which is how you confirm whether sender was folded into the transport or exposed separately.
Path B — you are on Laravel (the realistic case): do not install the bundle. Install the underlying, framework-agnostic library and lift the bundle's config shape as your reference:
composer require onlinecity/php-smpp
// config/smpp.php
return [
'host' => env('SMPP_HOST'),
'port' => (int) env('SMPP_PORT', 2775),
'login' => env('SMPP_SYSTEM_ID'),
'password' => env('SMPP_PASSWORD'),
'sender' => env('SMPP_SENDER', 'MyBrand'),
'timeout' => 10,
];
Bind a lazily-resolved singleton in a service provider, but treat it as a factory for short-lived sessions inside a daemon, not a global connection — see Gotchas.
First use case: a one-shot Artisan command that binds transceiver, submits a single submit_sm, logs the returned message_id, and closes. Run it against SMPPSim, not production. Do not start by wiring SMS into a controller; you will learn nothing about session behaviour and will learn it painfully in production instead.
1. Put the transport behind your own contract from day one. The whole value of this package is "is SMPP worth it for us?", so the answer must be cheap to reverse.
interface SmsSender {
/** @return string provider message id for DLR correlation */
public function send(string $to, string $text, ?string $from = null): string;
}
Bind SmppSender in the container. Later you can swap in a Twilio/Vonage adapter without touching call sites. This also lets you ship a FakeSmsSender for tests.
2. Never send from a web request. SMPP is a stateful TCP bind; PHP-FPM will tear the socket down mid-submit_sm. The production pattern is:
POST /notifications → validate → dispatch a job → return 202.php artisan sms:daemon under supervisor/systemd) holds the bind and drains the queue.class SendSmsJob implements ShouldQueue {
public function handle(SmsSender $sender): void {
$id = $sender->send($this->to, $this->text);
SmsMessage::whereKey($this->smsId)->update(['provider_id' => $id, 'status' => 'submitted']);
}
}
If you use SmsSender as a singleton inside the daemon only, the bind is reused and the queue provides your backpressure. That is the pattern the bundle does not give you and the one you must build.
3. Correlate DLRs explicitly. submit_sm_resp returns a message_id that may be a hex string, a decimal string, or vendor-prefixed garbage. Store it as a string column with an index, never cast it to int. Then a receiver loop handles deliver_sm:
foreach ($client->readSMS() as $pdu) { // pseudo: your poll loop
SmsMessage::where('provider_id', $pdu->receiptedMessageId)
->update(['status' => $this->mapState($pdu->stat), 'delivered_at' => now()]);
}
Set registered_delivery on submit (0 none, 1 final, 2 failure, 3 both). If you omit it, you get silent messages and no receipts, and you will blame the carrier.
4. Choose encoding deliberately, per message. GSM 7-bit for plain ASCII/Latin text; UCS-2 for anything with emoji or non-GSM characters. data_coding on the PDU must match the body you handed the library, or the handset renders mojibake. Write one helper that returns [encoding, segments] and unit-test it — that helper is where most real-world SMS bugs live.
5. Multipart is your job, not the SMSC's. Long messages become concatenated submit_sm PDUs with a UDH reference number. Budget 153 chars per GSM-7 segment and 67 per UCS-2 segment, and reuse a single UDH reference across the segments of one logical message so handsets reassemble them. Keep the library's sendSMS for single-part and add your own splitter for long text.
6. Inbound (MO) shares the same bind. In transceiver mode, deliver_sm carries both DLRs and inbound replies; your loop must distinguish esm_class and route accordingly. Design the receiver once, with two handlers, rather than bolting on inbound later.
7. Instrument the session, not just the message. Log bind attempts, enquire_link round-trips, reconnect counts, and submit_sm_resp command status. A messaging integration that "works" but reconnects every 90 seconds looks healthy in your app logs and is failing at the SMSC.
container:debug | grep -i smpp is non-negotiable; do not guess the service id from the bundle name.each(), create_function(), old constructor styles) and Composer conflicts. If you must use it, vendor and pin the exact commit, and record it as technical debt with an exit date — 4 stars and 0 dependents means bus factor ≈ 1.bind_transmitter cannot receive DLRs. If you want receipts or inbound, you need transceiver (or separate tx/rx binds and a second connection). Getting this wrong produces "delivery receipts just don't arrive".ESME_RTHROTTLED (0x58) and ESME_RMSGQFUL mean slow down. Implement a bounded in-flight window (start at 10–20) and exponential backoff; without it you will fill the SMSC queue and lose messages silently.enquire_link keepalive needs a timeout. If the SMSC stops answering, you must detect it and rebind. A socket that is open but dead is the classic silent-failure mode; supervise the worker and health-check the session, not just the process.TON = 5 / NPI = 0; numeric ones use TON = 1. Many aggregators silently rewrite or reject mismatched addressing, and the failure surfaces as "message accepted, never delivered".sleep()-based polling in a controller; no SMPP singleton resolved in a web context. If you need a quick path, dispatch() + worker is still the answer.submit_sm against the SMPP 3.4 spec is faster than guessing at encoding or TLV problems.message_id, and multipart reassembly. Those five scenarios are the integration.SmsSender binding, replace the client factory, and add a SmsMessage model for DLR state. Do not patch the vendor bundle in place — fork it into packages/ or replace it outright with the underlying library so upgrades (or the eventual migration to a modern SMPP client or HTTP aggregator) stay possible.How can I help you explore Laravel packages today?