Tutoriais

MQTT em PHP com php-mqtt/client

Use MQTT em PHP com php-mqtt/client: instale, publique com QoS 1, rode um loop de subscriber, adicione TLS e Last Will e use o pacote para Laravel.

Atualizado · 8 min de leitura

MQTT em PHP com php-mqtt/client

A forma padrão de usar MQTT em PHP é a biblioteca php-mqtt/client: instale com o Composer, crie um MqttClient, chame connect() e depois publish() de qualquer lugar e subscribe() com uma closure dentro de um loop CLI de longa duração. Ela fala MQTT 3.1 e 3.1.1 sobre TCP ou TLS, não precisa de extensões do PHP além das padrão e tem um wrapper oficial para Laravel.

Este tutorial cobre a publicação (incluindo o detalhe do QoS 1 que quase todo mundo deixa passar), um subscriber robusto, TLS com credenciais, Last Will e como testar cada parte.

Instalação

O php-mqtt/client exige PHP 8 e Composer:

Instalar a biblioteca
composer require php-mqtt/client

Existem duas formas muito diferentes de o PHP conversar com um broker, e confundi-las causa a maioria dos problemas:

PapelOnde rodaDuraçãoExemplo
PublisherRequisição web (PHP-FPM, Apache), cron, job de filaConectar, publicar e desconectar em milissegundosEnviar um comando quando o usuário clica em um botão
SubscriberProcesso CLI (php subscriber.php)Horas ou dias, supervisionadoGravar leituras de sensores em um banco de dados

Workers do PHP-FPM são reciclados e requisições expiram, então nunca tente manter uma assinatura aberta dentro de uma requisição web.

Publicar uma mensagem

publish.php
<?php
require __DIR__ . '/vendor/autoload.php';

use PhpMqtt\Client\ConnectionSettings;
use PhpMqtt\Client\MqttClient;

$clientId = 'testmqtt-php-' . bin2hex(random_bytes(4));
$mqtt = new MqttClient('broker.connectmq.com', 1883, $clientId, MqttClient::MQTT_3_1_1);

$settings = (new ConnectionSettings)
    ->setConnectTimeout(5)
    ->setKeepAliveInterval(30);

$mqtt->connect($settings, true);          // true = clean session

$payload = json_encode(['temp' => 21.5, 'ts' => time()]);
$mqtt->publish('testmqtt/php/sensors/room1', $payload, MqttClient::QOS_AT_LEAST_ONCE);

$mqtt->loop(true, true);                  // wait for the PUBACK, then return
$mqtt->disconnect();
echo "published\n";

Dois detalhes importam:

  • O client ID aleatório. Cada requisição é uma conexão separada. Se duas requisições simultâneas usassem o mesmo ID fixo, o broker desconectaria a primeira — veja client ID duplicado. O gerador de client ID mostra bons formatos.
  • loop(true, true) depois de publicar com QoS 1 ou 2. publish() apenas enfileira a mensagem; a confirmação é tratada dentro do loop. O segundo argumento faz o loop() retornar assim que nada estiver pendente. Com QoS 0 você pode desconectar direto. Os níveis de QoS do MQTT explicam o trade-off.

Abrir uma conexão por requisição é ok para eventos ocasionais. Se uma página publica a cada acesso, ou você envia rajadas de mensagens, passe o trabalho para um worker de fila que mantém uma conexão aberta — o padrão mostrado em MQTT no Laravel.

Mensagens retidas para estado

O quarto argumento de publish() é a flag retain. Uma mensagem retida fica armazenada no broker e é entregue imediatamente a cada novo subscriber, o que a torna ideal para topics de "estado atual", como a configuração de um dispositivo editada em um painel administrativo em PHP:

Publicar e limpar estado retido
$config = ['interval' => 60, 'unit' => 'C'];
$mqtt->publish('testmqtt/php/devices/42/config', json_encode($config), MqttClient::QOS_AT_LEAST_ONCE, true);

// An empty retained payload deletes the stored message
$mqtt->publish('testmqtt/php/devices/42/config', '', MqttClient::QOS_AT_LEAST_ONCE, true);
$mqtt->loop(true, true);

Use retain para estado, não para eventos: uma mensagem retida de "botão pressionado" seria reenviada a todo cliente que assinasse depois. Mantenha os payloads como JSON compacto com um timestamp, para que os consumidores saibam a idade do estado.

Assinar em um worker CLI

Um subscriber registra uma closure por filtro de topic e então chama loop(true), que bloqueia e despacha mensagens até você interrompê-lo:

subscriber.php
<?php
require __DIR__ . '/vendor/autoload.php';

use PhpMqtt\Client\ConnectionSettings;
use PhpMqtt\Client\MqttClient;

$mqtt = new MqttClient('broker.connectmq.com', 1883, 'testmqtt-php-worker', MqttClient::MQTT_3_1_1);
$mqtt->connect((new ConnectionSettings)->setKeepAliveInterval(30), true);

$mqtt->subscribe('testmqtt/php/sensors/+', function (string $topic, string $message, bool $retained) {
    $data = json_decode($message, true);
    if (!is_array($data)) {
        fwrite(STDERR, "invalid JSON on $topic\n");
        return;
    }
    printf("%s%s temp=%s\n", $topic, $retained ? ' (retained)' : '', $data['temp'] ?? '?');
}, MqttClient::QOS_AT_LEAST_ONCE);

// Stop cleanly on Ctrl+C or a SIGTERM from systemd / Docker
pcntl_async_signals(true);
pcntl_signal(SIGINT, fn () => $mqtt->interrupt());
pcntl_signal(SIGTERM, fn () => $mqtt->interrupt());

$mqtt->loop(true);
$mqtt->disconnect();

O curinga + corresponde a um nível de topic, então esse filtro recebe testmqtt/php/sensors/room1, mas não testmqtt/php/sensors/room1/battery. Confira os filtros com o topic matcher antes de implantar.

Rode php subscriber.php em um terminal e php publish.php em outro. Você deve ver a leitura impressa em milissegundos.

Sobreviver a desconexões

Um worker que roda por semanas uma hora vai perder a conexão: reinícios do broker, timeouts de NAT, deploys. Quando isso acontece, a biblioteca lança uma exceção a partir do loop(). O padrão robusto mais simples é um loop externo que reconecta com back-off e assina de novo:

worker.php
<?php
require __DIR__ . '/vendor/autoload.php';

use PhpMqtt\Client\ConnectionSettings;
use PhpMqtt\Client\Exceptions\MqttClientException;
use PhpMqtt\Client\MqttClient;

$settings = (new ConnectionSettings)
    ->setKeepAliveInterval(30)
    ->setLastWillTopic('testmqtt/php/worker/status')
    ->setLastWillMessage('offline')
    ->setLastWillQualityOfService(MqttClient::QOS_AT_LEAST_ONCE)
    ->setRetainLastWill(true);

$delay = 1;
while (true) {
    try {
        $mqtt = new MqttClient('broker.connectmq.com', 1883, 'testmqtt-php-worker', MqttClient::MQTT_3_1_1);
        $mqtt->connect($settings, true);
        $mqtt->publish('testmqtt/php/worker/status', 'online', MqttClient::QOS_AT_LEAST_ONCE, true);
        $mqtt->subscribe('testmqtt/php/sensors/+', function (string $topic, string $message) {
            echo "$topic $message\n";
        }, MqttClient::QOS_AT_LEAST_ONCE);

        $delay = 1;                       // connected: reset back-off
        $mqtt->loop(true);
    } catch (MqttClientException $e) {
        fwrite(STDERR, 'MQTT error: ' . $e->getMessage() . "\n");
        sleep($delay);
        $delay = min($delay * 2, 30);
    }
}

O Last Will publica offline (retido) se o worker morrer sem uma desconexão limpa, e o worker o sobrescreve com online após cada conexão — um indicador de saúde barato que outros serviços podem assinar. Last Will no MQTT cobre os detalhes.

Usar uma sessão limpa aqui é proposital: o worker assina de novo a cada conexão, então não depende de o broker guardar nada. Se você precisa que mensagens fiquem enfileiradas enquanto o worker está offline, mude para connect($settings, false) com um client ID fixo e teste contra o seu próprio broker privado — o endpoint compartilhado broker.connectmq.com não guarda sessões offline. Veja sessões persistentes.

O php-mqtt/client também tem setReconnectAutomatically(true) com setMaxReconnectAttempts() e setDelayBetweenReconnectAttempts() (em milissegundos), útil para publishers que mantêm uma conexão aberta entre jobs.

TLS e autenticação

Brokers de produção exigem TLS na porta 8883 e credenciais. Só mudam a porta e as configurações:

tls.php
<?php
require __DIR__ . '/vendor/autoload.php';

use PhpMqtt\Client\ConnectionSettings;
use PhpMqtt\Client\MqttClient;

$settings = (new ConnectionSettings)
    ->setUsername(getenv('MQTT_USER'))
    ->setPassword(getenv('MQTT_PASS'))
    ->setUseTls(true)
    ->setTlsVerifyPeer(true)
    ->setTlsVerifyPeerName(true)
    ->setTlsSelfSignedAllowed(false);
    // ->setTlsCertificateAuthorityFile('/path/to/ca.pem') for a private CA

$mqtt = new MqttClient('broker.connectmq.com', 8883, 'testmqtt-php-' . bin2hex(random_bytes(4)), MqttClient::MQTT_3_1_1);
$mqtt->connect($settings, true);
$mqtt->publish('testmqtt/php/secure', 'hello over TLS', MqttClient::QOS_AT_LEAST_ONCE);
$mqtt->loop(true, true);
$mqtt->disconnect();

Sem um arquivo de CA explícito, o OpenSSL do PHP usa o repositório de confiança do sistema (ou openssl.cafile no php.ini). Mantenha a verificação do peer ligada; desativá-la esconde problemas reais de certificado.

Usando no Laravel

Os mesmos autores mantêm o php-mqtt/laravel-client, que lê as configurações de conexão de config/mqtt-client.php e do .env e expõe uma facade:

Instalar o pacote para Laravel
composer require php-mqtt/laravel-client
php artisan vendor:publish --provider="PhpMqtt\Client\MqttClientServiceProvider" --tag="config"
Em qualquer lugar da sua aplicação
use PhpMqtt\Client\Facades\MQTT;

MQTT::publish('testmqtt/php/laravel/hello', 'Hello from Laravel');

Para assinar, você chama MQTT::connection() dentro de um comando Artisan e roda loop(true) ali. O padrão completo — jobs em fila para publicar, um comando supervisionado para assinar, e testes — está em MQTT no Laravel.

Erros comuns

SintomaCausaCorreção
ConnectingToBrokerFailedException com "Establishing a connection to the MQTT broker failed"Host ou porta errados, firewall, TLS em uma porta sem criptografia (ou o contrário)Combine porta e setUseTls(): 1883 sem TLS, 8883 com TLS
Conexão recusada após o CONNECTCredenciais erradas ou sem autorizaçãoVerifique usuário, senha e ACLs
O worker desconecta a cada poucos segundosDois processos compartilham um client IDIDs únicos por processo, IDs aleatórios por requisição
Mensagem QoS 1 sumiuO script desconectou antes do PUBACKloop(true, true) antes de disconnect()
Call to undefined function pcntl_async_signals()pcntl indisponível (Windows ou SAPI web)Use tratamento de sinais só no CLI em Unix

Teste

Veja o que o seu código PHP publica com um subscriber no terminal ou com o cliente MQTT online:

Acompanhar os topics de PHP
mosquitto_sub -h broker.connectmq.com -p 1883 -t 'testmqtt/php/#' -v

Depois envie uma mensagem para o seu subscriber, incluindo uma inválida para provar que a validação de JSON funciona:

Enviar mensagens de teste ao worker
mosquitto_pub -h broker.connectmq.com -p 1883 -t 'testmqtt/php/sensors/room1' -m '{"temp":22.1}' -q 1
mosquitto_pub -h broker.connectmq.com -p 1883 -t 'testmqtt/php/sensors/room1' -m 'not json' -q 1

Por fim, mate o worker com kill -9 e confirme que o status retido offline aparece em testmqtt/php/worker/status. Há mais receitas de linha de comando em exemplos de mosquitto_pub e mosquitto_sub.

Veja os topics de PHP no cliente online

O php-mqtt/client torna o MQTT em PHP simples assim que você respeita o modelo de processos do PHP: publique a partir das requisições com client IDs únicos e um loop(true, true) para QoS 1, assine apenas em workers CLI supervisionados que reconectam sozinhos e ative TLS e credenciais antes de ir para produção.

Perguntas frequentes

O PHP consegue assinar topics MQTT?

Sim, mas não dentro de uma requisição web comum. Um subscriber precisa ficar conectado, então rode-o como um script CLI de longa duração (php subscriber.php) que chama $mqtt->loop(true), supervisionado por systemd, Supervisor ou Docker. Requisições web devem apenas publicar.

O php-mqtt/client suporta MQTT 5?

No momento em que escrevemos, ele implementa MQTT 3.1 e 3.1.1, aceitos por qualquer broker comum. Se você precisa de recursos do MQTT 5 como reason codes ou user properties, trate essa parte do sistema em outra linguagem ou verifique se o projeto tem versões mais novas.

Por que minha mensagem QoS 1 enviada pelo PHP nunca chega?

publish() apenas enfileira mensagens QoS 1 e 2; a confirmação é processada dentro do loop(). Chame $mqtt->loop(true, true) depois de publicar para que o cliente espere o PUBACK antes de desconectar; caso contrário, o script pode terminar antes.