creattico / yeastar-socket-sms
PHP class to send SMS via socket API through Yeastar TGxxxx gateways
Requires
- php: >=7.4
Requires (Dev)
- phpunit/phpunit: ^11.5
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
PHP library to send SMS and read gateway status via the socket API (AMI) of Yeastar TGxxxx gateways.
Requirements
- PHP >= 7.4
Install
composer require creattico/yeastar-socket-sms
Usage
use YeastarSocket\SocketApi; use YeastarSocket\Exceptions\SocketConnectionException; use YeastarSocket\Exceptions\SmsSendException; $sms = new SocketApi([ 'host' => 'domain.ext', // Yeastar gateway host or IP 'port' => 5038, // AMI port (default: 5038) 'gateway_port' => 1, // GSM span — see the note below 'account' => 'username', 'password' => 'password', 'to' => '0039123456789', // recipient number with country code 'message' => 'Your message here', 'timeout' => 5, // connection timeout in seconds (default: 5) 'confirm' => true, // wait for delivery confirmation (default: false) 'send_timeout' => 20, // seconds to wait for it (default: 20) 'debug' => true, // optional: enable debug logging ]); try { $sms->sendSms(); } catch (SocketConnectionException $e) { // connection or authentication failed echo $e->getMessage(); } catch (SmsSendException $e) { // the message did not go out echo $e->getMessage(); } finally { $sms->closeSocket(); }
Delivery confirmation (confirm)
Without confirm, a successful return does not mean the message was sent.
The gateway acknowledges the send command immediately with Response: Follows, and that
acknowledgement is byte-for-byte identical whether the message goes out or not. A send from a
port with no SIM, or with a SIM that lost network registration, is acknowledged exactly like a
successful one. What actually tells them apart is an asynchronous UpdateSMSSend event that
arrives three to four seconds later, carrying the send ID and a status.
With 'confirm' => true, sendSms() waits for that event and matches it against the ID of
this specific send — the connection carries events for other sends too. It returns true only
on Status: 1, and throws SmsSendException on any other status or when no event arrives
within send_timeout.
The trade-off is that sendSms() blocks for a few seconds, so it belongs in a queued job
rather than inside a web request.
confirm is off by default for backward compatibility: before v1.2 sendSms() never threw
on a swallowed message, and existing callers are not catching that case yet. New code should
turn it on.
What gets cleaned before sending
The recipient and the text end up on a single line of the AMI protocol:
command: gsm send sms <span> <number> "<text>" <id>
Since v1.2.1 sendSms() makes both safe for that line, whoever wrote them:
- Number: spaces, dashes, dots, slashes and brackets are removed. If anything other than
digits and a leading
+is left, it throwsSmsSendExceptionbefore connecting. There is no length check: what counts as a plausible number is up to your application. - Text: line breaks and other control characters become a space,
"becomes'and\becomes/. The text is changed rather than refused, because the recipient still reads the same thing.
command() throws InvalidArgumentException if the command contains a control character. Never
build one from user input: the same connection accepts gsm send ussd.
Reading gateway status
The same connection can run read commands, which is how you find out which ports are usable:
$api = new SocketApi(['host' => '...', 'account' => '...', 'password' => '...']); $api->openSocket(); $spans = $api->command('gsm show spans'); $detail = $api->command('gsm show span 3'); $api->closeSocket();
command() returns the raw response block and does not interpret it: what counts as a healthy
port is a question for your application, not for the protocol.
Known quirks of the TG400 firmware, worth designing around:
gsm show span allis broken — it returns a single span with its fields collapsed into one another. Ask for one span at a time.- Payload lines end with a bare
\nwhile AMI headers end with\r\n, and ingsm show span <n>the--END COMMAND--terminator is glued to the last payload line with no newline before it. - An empty slot has two different signatures:
Undetected SIM Cardif it was always empty, orUnregistered NetworkwithState: SIM READY REQif the SIM was pulled while the module was running. - The AMI surface is nine actions, and only
SMSCommanddoes anything useful. It acceptsgsm show spans,gsm show span <n>|all,gsm send smsandgsm send ussd. There is no hardware management: no power cycling, no reset, no forced re-registration.
Port numbering
gateway_port is the span, which is the physical port plus one: physical port 2 is
span 3. Yeastar's HTTP SMS API numbers the same ports differently — there the value is the
physical port. Reusing the same number across the two channels sends from the wrong SIM, and it
fails silently: the message is delivered, just from another number.
Debug
Set debug to true to collect log messages:
print_r($sms->log);
Exceptions
| Exception | When |
|---|---|
SocketConnectionException |
Cannot connect to the gateway, authentication fails, or a command is issued on a closed session |
SmsSendException |
Recipient missing or invalid, or — with confirm on — the gateway never confirmed the message left the device |
InvalidArgumentException |
command() received a command with a control character |
Tests
composer install vendor/bin/phpunit
The suite talks to a scripted fake gateway started as a separate process, replaying responses captured from a real TG400. No test touches a device.
License
MIT