binsoul/net-mqtt
A lightweight PHP MQTT client for connecting to MQTT brokers, publishing messages, and subscribing to topics. Supports QoS levels, retained messages, clean sessions, keepalive, and configurable client options for IoT and real-time PHP applications.
binsoul/net-mqtt is a small, framework-agnostic MQTT 3.1/3.1.1 client. It has no Laravel service provider, facade, or config file — you wire it yourself. That is the first thing to internalize: treat it as a protocol library, not a Laravel package.
Install and prerequisites
composer require binsoul/net-mqtt
You need a broker (Mosquitto locally, EMQX/HiveMQ in production), PHP with stream/socket support, and network access to 1883 or 8883 for TLS. Confirm PHP version constraints with composer show binsoul/net-mqtt.
Where to look first
src/Client/ — Client, CallbackClient, and Connection are the classes you will actually use.src/Packet/ — only relevant if you need to inspect or extend protocol behavior.Minimal Laravel config
// config/mqtt.php
return [
'host' => env('MQTT_HOST', '127.0.0.1'),
'port' => (int) env('MQTT_PORT', 1883),
'client_id' => env('MQTT_CLIENT_ID', 'laravel-'.gethostname()),
'username' => env('MQTT_USERNAME'),
'password' => env('MQTT_PASSWORD'),
'keepalive' => 60,
'tls' => env('MQTT_TLS', false),
];
First use case: publish a domain event
Wrap the client immediately so your app code never imports the vendor namespace directly:
namespace App\Mqtt;
use Binsoul\NetMqtt\Client\Client;
use Binsoul\NetMqtt\Client\Connection;
class MqttPublisher
{
public function __construct(private array $config) {}
public function publish(string $topic, string $payload, int $qos = 0, bool $retain = false): void
{
// Exact constructor/connect signature varies by version — confirm in README.
$connection = new Connection($this->config['host'], $this->config['port']);
$client = new Client($connection, $this->config['client_id']);
try {
$client->connect($this->config['username'], $this->config['password']);
$client->publish($topic, $payload, $qos, $retain);
} finally {
$client->disconnect();
}
}
}
Bind it in AppServiceProvider:
$this->app->singleton(MqttPublisher::class, fn () => new MqttPublisher(config('mqtt')));
Then call it from an event listener or controller. That is the lowest-friction path to a working integration.
1. Publish-per-action with a short-lived connection
The simplest and safest pattern for HTTP requests: connect, publish, disconnect inside the request. Handshake overhead is real, but for low-volume event publishing it avoids stale connections and queue-worker complications. Use QoS 0 for fire-and-forget telemetry; QoS 1 when the message matters.
2. Long-lived publisher in queue workers
If you publish frequently, connect once per worker process and reuse. Because queue workers are long-lived, a singleton client can go stale after broker restarts. Wrap publish calls with a connectivity check and reconnect-on-exception:
public function publish(...): void
{
try {
$this->client->publish(...);
} catch (\Throwable $e) {
$this->reconnect();
$this->client->publish(...);
}
}
Also register a terminate hook or use finally to disconnect cleanly.
3. Subscriber as an Artisan command
Consumers should not run inside HTTP requests. Create a command that resolves a CallbackClient, registers message/connect/disconnect callbacks, subscribes, and enters the read loop:
protected $signature = 'mqtt:listen {topic}';
public function handle(MqttSubscriber $subscriber): int
{
$subscriber->subscribe($this->argument('topic'), function (string $topic, string $message) {
// Keep handlers fast; optionally dispatch to the queue.
MqttMessageReceived::dispatch($topic, $message);
});
$subscriber->loop(); // blocks until disconnected
return self::SUCCESS;
}
Run it under Supervisor or systemd, never php artisan serve. Handle SIGTERM so the client disconnects cleanly instead of being killed mid-packet.
4. Bridge MQTT into Laravel’s event system
Inbound messages should become application events or queued jobs as quickly as possible. Do not do heavy work inside the MQTT callback — it blocks the read loop and delays keepalive processing. A good shape is:
MQTT callback -> event(MqttMessageReceived) -> queued listener -> business logic
For outbound, listen to domain events and publish:
Event::listen(OrderShipped::class, fn ($event) => app(MqttPublisher::class)
->publish("orders/{$event->order->id}/shipped", json_encode($event->payload), qos: 1));
5. Topic naming and QoS policy
Centralize topic construction in a small class so orders/{id}/shipped is never assembled ad hoc. Adopt a QoS policy: 0 for high-volume telemetry, 1 for commands and events that must arrive, 2 only when duplicate delivery is genuinely harmful. Use retained messages for current-state topics (device status, last known value), not for event streams.
6. Testing strategy
Put an interface in front of the vendor client (MqttPublisherInterface) and fake it in unit tests. For integration tests, run a real Mosquitto container and verify actual publish/subscribe behavior — MQTT semantics (retain, QoS, clean session) are exactly what mocks fail to capture.
7. Presence and last will
Use the MQTT Last Will and Testament feature for online/offline presence. The library exposes will configuration at connect time; set a retained clients/{id}/status topic to offline as the will and publish online after connecting.
The client is synchronous and blocking. There is no event loop or async I/O. A subscriber loop owns its process completely. Keep message handlers short, or push work onto Laravel’s queue. Never run a subscribe loop inside a web request or a standard queue worker.
Client ID collisions are silent and destructive. If two processes connect with the same client ID, the broker disconnects the older session. Generate unique IDs per process (laravel-{hostname}-{pid} or a UUID) for ephemeral clients. Use a stable ID only when you intentionally want a persistent session.
Keepalive requires the loop to run. If a message handler blocks longer than the keepalive interval, the broker will drop the connection. Long-running handlers are the most common cause of “mysterious disconnects.” Offload them.
Reconnection is your responsibility. Expect to write a reconnect loop with backoff. Catch connection exceptions around connect(), publish(), and the read loop, sleep, and retry. Log the failure reason — “connection reset by peer” and “not authorized” need very different responses.
Clean session and QoS 1 mean at-least-once delivery. With cleanSession=false and QoS 1, the broker may redeliver after reconnect. Make handlers idempotent, or deduplicate on a message ID you include in the payload. QoS 2 is slower and rarely worth it.
TLS needs more than a port change. Use the TLS scheme or a stream context with certificate verification enabled. Test against the real broker; local Mosquitto often has TLS disabled, so a production-only failure is easy to miss. Verify CA paths and hostname matching.
Do not share a connected socket across forks or requests. Laravel Octane, pcntl_fork, and some queue configurations can inherit an open socket. A singleton client resolved in one request can be reused in another with stale state. Either reconnect defensively or scope the client per job/request.
Payload and packet size limits. MQTT’s remaining-length field caps packet size, and brokers impose their own limits. Large JSON payloads should be chunked or replaced with a reference (publish a URL/ID and fetch the body over HTTP/S3).
Version differences matter. MQTT 3.1 restricts client IDs to 23 characters; 3.1.1 relaxes this. If you target 3.1 for legacy devices, keep IDs short and alphanumeric.
Wildcards are subscription-only. + matches one topic level, # matches the rest. You cannot publish to a wildcard. Also remember retained messages are delivered immediately on subscribe — consumers must handle a burst of stale state at startup.
Debugging workflow. Use mosquitto_sub -v -t '#' and mosquitto_pub to isolate whether the problem is your client or the broker. Enable broker verbose logs. Check ACLs — “connection accepted but no messages arrive” is frequently an authorization issue, not a code issue. For packet-level issues, tcpdump -i lo port 1883 is faster than adding logging to the library.
Low bus-factor dependency. This is a 28-star, single-maintainer package. MIT licensing means you can fork or vendor it if needed. Keep it behind your own MqttPublisher/MqttSubscriber interfaces so swapping to a more actively maintained client (or an async one) is a contained change rather than a codebase-wide rewrite.
Extension points are limited. The packet classes are public and can be extended, but the practical extension surface is your wrapper: connection factory, topic builder, QoS policy, reconnect strategy, and callback-to-event bridge. Invest there, not in subclassing the vendor client.
How can I help you explore Laravel packages today?