php-mqtt/laravel-client
Laravel client for MQTT built on php-mqtt/client, making it easy to publish messages and subscribe to topics from Laravel applications. Includes configuration, a facade, and seamless integration with Laravel's container and service provider.
php-mqtt/laravel-client is a thin Laravel wrapper around php-mqtt/client: a manager + facade + config layer that gives you named broker connections, a broadcasting driver, a queue job, and logging hooks. You still write real MQTT logic against the underlying client.
composer require php-mqtt/laravel-client
php artisan vendor:publish --tag=mqtt-client-config
Set the basics in .env (MQTT_HOST, MQTT_PORT, MQTT_USERNAME, MQTT_PASSWORD, MQTT_CLIENT_ID) and open config/mqtt-client.php first — it is the whole package. Three blocks matter: default_connection, connections.* (host/port/credentials/client_id/mqtt_version), and the per-connection settings (keep-alive, connect timeout, auto-reconnect) plus protocols (MQTT v5-only options such as session expiry and receive maximum). Everything else in the package reads from this file.
The fastest first use case — fire-and-forget publish from a controller or listener:
use PhpMqtt\Client\Facades\MQTT;
use PhpMqtt\Client\QualityOfService;
MQTT::publish(
'devices/42/state',
json_encode(['online' => true, 'ts' => now()->timestamp]),
QualityOfService::QOS_1,
retain: true,
);
Note there is no connect() call: the manager connects lazily on first use and caches the client. Don't disconnect() after a single publish in a web request unless you have a reason — the connection dies with the request anyway.
Wrap the facade, don't sprinkle it. Create a small MqttPublisher service that takes a connection name and exposes publish(), so you can swap in a fake and so topic naming lives in one place.
final class TelemetryPublisher
{
public function __construct(private string $connection = 'default') {}
public function publish(string $topic, array $payload, bool $retain = false): void
{
MQTT::connection($this->connection)->publish(
$topic, json_encode($payload), QualityOfService::QOS_1, $retain
);
}
}
Named connections map to broker roles. Keep an internal connection (your own EMQX/Mosquitto) and an iot connection (device-facing broker) side by side; MQTT::connection('iot') is the only switching you need.
Publishing from jobs. The package ships PublishMessageJob for trivial fire-and-forget publishes, but for anything you care about write your own ShouldQueue job — you get $tries, backoff(), failed(), and Horizon visibility, which the shipped job doesn't provide. Keep the payload small; MQTT brokers reject large frames well before Laravel's queue limits bite.
Subscribing is a long-running process, not a request. Put subscriptions in an Artisan command and run it under Supervisor:
public function handle(): int
{
$client = MQTT::connection('iot');
$client->subscribe('devices/+/state', function (string $topic, string $message, bool $retained) {
ProcessDeviceState::dispatch($topic, json_decode($message, true));
}, QualityOfService::QOS_1);
$client->loop(true); // blocks
return self::SUCCESS;
}
In Laravel 11+ use $this->trap(SIGTERM, fn () => $client->interrupt()) (or pcntl_signal) so loop(true) exits cleanly instead of being SIGKILLed mid-write.
Broadcasting for fan-out. Register the mqtt driver in config/broadcasting.php ('driver' => 'mqtt', 'connection' => 'default') and your existing ShouldBroadcast events start publishing to topics derived from channel names. This is the lowest-effort integration if you already use events.
Testing. The manager resolves clients through the MqttClient contract, so $this->mock(MqttClient::class) or a hand-rolled fake bound in the container is the cleanest seam — you don't want unit tests opening sockets. Assert on topic/payload/QoS rather than on broker state.
client_id collisions are the #1 outage cause. Most brokers kick the older session when a duplicate ID connects. Two queue workers sharing MQTT_CLIENT_ID will silently flap all day. Suffix with the hostname/pid, or leave it unset if your broker assigns one.connect_timeout high and reconnect_automatically on, a wrong host produces a long hang inside a web request. Fail fast in a health check (php artisan tinker → MQTT::connection()->connect()) or set a short connect timeout on web-facing connections.MQTT::connection($name)->disconnect() in a JobProcessing/WorkerStopping listener, or re-resolve per job, otherwise you'll see DataTransferException/ConnectingToBrokerFailedException after ~keep-alive intervals.retain: true means the broker replays the last payload to every new subscriber. To clear a retained topic you must publish a zero-length payload with the retain flag — you can't delete it via the manager.DB::afterCommit / dispatchAfterResponse), not inside it.protocols, not settings. Setting session_expiry_interval on a connection configured as MQTT_3_1_1 is silently inert. Session persistence also requires a stable client_id and clean_session: false — a pairing that trips people up when they later "fix" the duplicate-ID issue by randomising the ID.PhpMqtt\Client\Exceptions\MqttClientException. Catch specific ones (ConnectingToBrokerFailedException, DataTransferException, ProtocolNotSupportedException) for retry decisions, then the base class as a backstop around your publisher.logging block in the config (channel + enabled flag) routes the client's internal debug output through your Laravel log — by far the fastest way to see why a handshake is being rejected. Turn it off in production; it's noisy per-packet output.php-mqtt/client constraint it pulls and confirm your PHP version meets Laravel 13's floor before bumping on a shared branch.How can I help you explore Laravel packages today?