simps/mqtt
simps/mqtt is a PHP MQTT client supporting MQTT 3.1, 3.1.1, and 5.0 with QoS, retained messages, wills, TLS, and WebSocket. Add publish/subscribe messaging to PHP and Laravel apps, including Swoole-based async environments.
simps/mqtt is a two-part package: a full MQTT protocol codec (pack/unpack for v3.1, v3.1.1, v5.0) and a coroutine client built on Swoole (or Swow in recent releases). It is not an HTTP client — it expects a coroutine context.
composer require simps/mqtt
Requirements: PHP 8.x, ext-swoole (4.4+ with coroutine support) or Swow. No Laravel-specific glue ships with it, so plan on a small wrapper.
Minimal first use case — publish from a worker, subscribe in a long-running process:
use Simps\MQTT\Client;
use Swoole\Coroutine;
Coroutine\run(function () {
$client = new Client('broker.emqx.io', 1883, [
'client_id' => 'laravel-publisher-' . getmypid(),
'clean_session' => true,
'keep_alive' => 60,
'protocol_level' => 4, // 3 = v3.1, 4 = v3.1.1, 5 = v5.0
]);
$client->connect();
$client->publish('app/events/order.created', json_encode(['id' => 42]), 1);
Coroutine::sleep(0.2); // let QoS 1 PUBACK drain
$client->close();
});
Where to look first, in order:
src/Client.php — the entire coroutine client API (connect, publish, subscribe, unsubscribe, recv, ping, close, getClient).src/Protocol/V3.php and src/Protocol/V5.php — pack()/unpack() entry points if you need the codec standalone (brokers, proxies, test doubles).src/Message/ — typed message objects (Publish, Subscribe, Auth, etc.) and the MQTT 5 property classes.src/Hex/ — protocol-level constants (ProtocolLevel, QoS, MessageType) to avoid magic numbers in config.Publish-per-request with connection reuse. Inside a queue worker or HTTP request handler, open one connection per coroutine is fine for low volume; for throughput, keep a single Client alive per worker and call publish() repeatedly. The underlying Swoole client is cheap, but the CONNECT/CONNACK round trip is not.
Long-lived subscriber loop. The canonical pattern is a blocking while (true) { $client->recv(); }. Keep the loop lean — hand the decoded response to a Swoole\Channel or a task worker, and do database work elsewhere. If you process inline, you stop reading the socket and the broker will eventually drop you on keep-alive.
$client->subscribe(['app/events/#' => 1]);
while ($client->getClient()->isConnected()) {
$packet = $client->recv();
if ($packet === false) { break; }
if (($packet['type'] ?? null) === MessageType::PUBLISH) {
$channel->push($packet);
}
}
Laravel integration shape. A php artisan mqtt:subscribe command that wraps its body in Coroutine\run() is the path of least resistance. Put broker host/port/credentials in config/mqtt.php, and drive protocol_level from an env flag so staging can run v3.1.1 while production runs v5. Avoid wiring this into Octane middleware — Octane's coroutine lifecycle is not a good fit for a persistent MQTT connection.
MQTT 5 request/response. Use the v5 properties array on publish(): set ResponseTopic, CorrelationData, and UserProperty pairs. The subscriber reads them back from the unpacked packet and publishes the reply to the response topic. This gives you RPC-style semantics over the broker without inventing your own envelope.
Codec-only mode. If you are building a broker, a protocol bridge, or a test harness, skip Client entirely and use V5::pack() / V5::unpack(). Pair it with a Swoole TCP server that has open_mqtt_protocol => true, or handle the remaining-length varint yourself if you are reading from an arbitrary stream.
Reconnect with backoff. Wrap connect() + subscribe in a supervisor loop with exponential backoff and jitter. After a reconnect, re-issue every subscribe() — brokers do not replay subscriptions for a clean session, and even persistent sessions should be re-asserted defensively.
Presence and last will. Set will in the client settings (topic, qos, retain, message) to publish an "offline" payload on ungraceful disconnect. Pair it with a retained "online" publish on connect for a lightweight presence system.
Coroutine context is mandatory. Calling new Client(...) or connect() outside Coroutine\run() or a coroutine-enabled Swoole server throws a fatal "API must be called in coroutine" error. If you see this from an Artisan command, you forgot the wrapper.
Keep-alive is on you. The client does not silently ping in the background in all versions. Send $client->ping() on a Swoole\Timer::tick() at roughly half the keep_alive interval, or your broker will close the socket with no error visible in your logs.
Framing matters in codec mode. unpack() expects one complete MQTT packet. If you are reading raw TCP, a single recv() may contain half a packet or three packets glued together. Buffer, parse the fixed header, then read the remaining-length varint before calling unpack().
Protocol level mismatches fail silently. Pointing a v5 client at a 3.1.1-only broker often results in a CONNACK with an error code and an immediate close, not an exception. Log the CONNACK return code on connect and treat non-zero as fatal. Empty properties arrays should be [], not null.
QoS 2 state is in memory. PUBREC/PUBREL/PUBCOMP tracking does not survive a process restart. For anything where duplicate delivery is unacceptable, prefer QoS 1 plus idempotent consumers keyed on message_id, or persist in-flight state yourself.
Binary payloads are strings. No base64 layer, no JSON assumption — publish() takes raw bytes. That is good news for Protobuf and MessagePack, but it means json_encode() can return false on invalid UTF-8 without warning; check it.
Debugging. var_dump on a raw packet gives you garbage. Dump the array returned by recv()/unpack() instead, or bin2hex() the buffer when you need to eyeball the fixed header. If nothing arrives, confirm the subscription topic filter and that your subscribe() returned a SUBACK with matching QoS.
TLS and self-signed certs. Pass SSL settings through the client config (or use a tls:// scheme, depending on version) and set ssl_verify_peer => false only in local development. The default verifies against the system CA store.
Reach for getClient() sparingly. It returns the underlying Swoole\Coroutine\Client, which is useful for timeouts and buffer sizes, but calling recv() on it directly will steal packets from the MQTT client's own read path and corrupt the stream.
Test against a real broker. eclipse-mosquitto in a Docker sidecar is the fastest way to catch protocol-level mistakes that mocks will happily let through. Pin simps/mqtt to a specific minor version — the package is actively released and the v5 property surface has grown across versions.
How can I help you explore Laravel packages today?