PHPackages                             ultraembeddedlab/php-iot - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [HTTP &amp; Networking](/categories/http)
4. /
5. ultraembeddedlab/php-iot

Abandoned → [ultraembeddedlab/mqtt-client](/?search=ultraembeddedlab%2Fmqtt-client)Library[HTTP &amp; Networking](/categories/http)

ultraembeddedlab/php-iot
========================

MQTT 3.1.1 and MQTT 5.0 client for PHP 8.4+ — QoS 0/1/2, TLS and mutual TLS, WebSocket transport, auto-reconnect, persistent sessions

v1.3.0(4mo ago)0189[4 PRs](https://github.com/UltraEmbeddedLab/mqtt-client/pulls)MITPHPPHP ^8.4CI passing

Since Jan 18Pushed 1w agoCompare

[ Source](https://github.com/UltraEmbeddedLab/mqtt-client)[ Packagist](https://packagist.org/packages/ultraembeddedlab/php-iot)[ Docs](https://github.com/UltraEmbeddedLab/php-iot)[ RSS](/packages/ultraembeddedlab-php-iot/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (5)Dependencies (18)Versions (5)Used By (0)

PHP IoT MQTT Client
===================

[](#php-iot-mqtt-client)

[![CI](https://github.com/UltraEmbeddedLab/mqtt-client/actions/workflows/ci.yml/badge.svg)](https://github.com/UltraEmbeddedLab/mqtt-client/actions/workflows/ci.yml)[![Latest Stable Version](https://camo.githubusercontent.com/895632d5ebb9148247341365c1b92e09fadaa09a1cd9b135676a4f9a73482a54/68747470733a2f2f706f7365722e707567782e6f72672f756c747261656d6265646465646c61622f6d7174742d636c69656e742f76)](https://packagist.org/packages/ultraembeddedlab/mqtt-client)[![License](https://camo.githubusercontent.com/9302da6a6b6a2c0ec3b1672de3afa8aad2f86dfedb4202214ddae442c559c127/68747470733a2f2f706f7365722e707567782e6f72672f756c747261656d6265646465646c61622f6d7174742d636c69656e742f6c6963656e7365)](https://packagist.org/packages/ultraembeddedlab/mqtt-client)[![PHP Version](https://camo.githubusercontent.com/96b2f437cd2e8d57b2ca61ea39eeffa794d70bf0b96b0fc764838219ba18fbf8/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f7068702d762f756c747261656d6265646465646c61622f6d7174742d636c69656e74)](https://packagist.org/packages/ultraembeddedlab/mqtt-client)[![PHPStan](https://camo.githubusercontent.com/14995ff65edea59395c224e37e4fc66f91c1e601c1a58311e3c6f38c4fe37feb/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048505374616e2d6c6576656c2532306d61782d627269676874677265656e)](https://phpstan.org/)

Modern, production-grade MQTT 3.1.1 &amp; 5.0 client for PHP 8.4+

Features
--------

[](#features)

- **Modern PHP 8.4+** with strict types and modern syntax
- **MQTT 3.1.1 &amp; 5.0** protocol support
- **TLS 1.2+ &amp; mutual TLS (mTLS)** — TLS 1.0/1.1 refused by default (RFC 8996); typed `TlsOptions` for client certificates, CA verification and ALPN
- **WebSocket transport** (`ws://`) with RFC 6455 framing — `wss://` is not functional yet, see [Known Limitations](#known-limitations)
- **Auto-reconnect** with exponential backoff and jitter
- **QoS 0, 1, 2** with automatic resend on ACK timeout
- **Session persistence** for reliable message delivery
- **Rate limiter** (token bucket) to prevent broker flooding
- **Offline message queue** with automatic drain on reconnect
- **Topic aliases** (MQTT 5.0)
- **Flow control** (MQTT 5.0)
- **Shared subscriptions** (MQTT 5.0)
- **Byte counters** for traffic monitoring (`bytesSent()` / `bytesReceived()`)
- **PSR-3** logging support
- **PSR-14** event dispatcher support

Requirements
------------

[](#requirements)

- PHP 8.4 or higher
- `ext-json` (bundled with PHP and not removable since 8.0)
- `ext-openssl` — only for TLS (`mqtts://`, `wss://`). Plain TCP works without it.

No other extensions are needed: all I/O goes through PHP's stream functions.

Installation
------------

[](#installation)

Install via Composer:

```
composer require ultraembeddedlab/mqtt-client
```

> Upgrading from `ultraembeddedlab/php-iot` 1.x? The package was renamed in 2.0 — the PHP namespace is unchanged, so no `use` statement moves. See [UPGRADE.md](UPGRADE.md).

Quick Start
-----------

[](#quick-start)

### Simple Publish (Fire and Forget)

[](#simple-publish-fire-and-forget)

The easiest way to publish a message:

```
use ScienceStories\Mqtt\Easy\Mqtt;

Mqtt::publish(
    host: 'broker.example.com',
    topic: 'sensors/temperature',
    payload: '23.5',
);
```

### Publish with TLS and Authentication

[](#publish-with-tls-and-authentication)

```
use ScienceStories\Mqtt\Easy\Mqtt;

Mqtt::publish(
    host: 'broker.example.com',
    topic: 'sensors/temperature',
    payload: '23.5',
    tls: true,
    username: 'user',
    password: 'secret',
);
```

### Using MQTT 5.0

[](#using-mqtt-50)

```
use ScienceStories\Mqtt\Easy\Mqtt;
use ScienceStories\Mqtt\Protocol\QoS;

Mqtt::publish(
    host: 'broker.example.com',
    topic: 'sensors/temperature',
    payload: '23.5',
    version: 'v5',
    qos: QoS::AtLeastOnce,
    properties: [
        'message_expiry_interval' => 3600,
        'content_type' => 'text/plain',
    ],
);
```

### Subscribe to Topics

[](#subscribe-to-topics)

For more complex use cases, use the full client:

```
use ScienceStories\Mqtt\Client\Client;
use ScienceStories\Mqtt\Client\Options;
use ScienceStories\Mqtt\Protocol\MqttVersion;
use ScienceStories\Mqtt\Transport\TcpTransport;

$options = new Options(
    host: 'broker.example.com',
    port: 1883,
    version: MqttVersion::V5_0,
);

$options = $options
    ->withClientId('my-client')
    ->withKeepAlive(60)
    ->withCleanSession(true);

$client = new Client($options, new TcpTransport());
$client->connect();

// Subscribe to topics
$client->subscribe(['sensors/#'], qos: 1);

// ...or subscribeWith() when each filter needs its own QoS
$client->subscribeWith([
    ['filter' => 'sensors/#', 'qos' => 1],
    ['filter' => 'commands/+', 'qos' => 2],
]);

// Handle incoming messages
$client->onMessage(function ($message) {
    echo "Received: {$message->payload} on {$message->topic}\n";
});

// Listen for messages
while (true) {
    $client->loopOnce(1.0);
}
```

### Long-Running Connection

[](#long-running-connection)

Use the `Mqtt::connect()` method for sessions that need to publish multiple messages:

```
use ScienceStories\Mqtt\Easy\Mqtt;
use ScienceStories\Mqtt\Client\PublishOptions;
use ScienceStories\Mqtt\Protocol\QoS;

$client = Mqtt::connect(
    host: 'broker.example.com',
    port: 1883,
    version: 'v5',
);

// Publish multiple messages
$client->publish('sensors/temp', '23.5', new PublishOptions(qos: QoS::AtLeastOnce));
$client->publish('sensors/humidity', '65', new PublishOptions(qos: QoS::AtLeastOnce));

$client->disconnect();
```

Configuration Options
---------------------

[](#configuration-options)

### Client Options

[](#client-options)

OptionTypeDefaultDescription`host`stringrequiredMQTT broker hostname`port`int`1883`Broker port. **Not** derived from TLS — pass `8883` yourself. (`Easy\Mqtt::publish()` auto-detects; `Options` does not.)`version`MqttVersionV3\_1\_1MQTT protocol version`clientId`string`''`Client identifier. Empty means the broker assigns one, which MQTT 3.1.1 allows only with `cleanSession: true`. `Easy\Mqtt` generates one for you.`keepAlive`int60Keep alive interval in seconds (0–65535). A broker that sends Server Keep Alive in CONNACK overrides this.`pingResponseTimeout`float10.0How long to wait for a PINGRESP before treating the connection as dead and closing it`cleanSession`booltrueStart with clean session`username`stringnullAuthentication username`password`stringnullAuthentication password`will`?WillOptionsnullLast Will and Testament`autoReconnect`boolfalseReconnect with exponential backoff — see `withAutoReconnect()``offlineQueueSize`int0Publishes buffered while disconnected, drained on reconnect (0 = off)`rateLimiter`?RateLimiternullToken-bucket throttle for outbound publishes`sessionStore`?SessionStoreInterfacenullPersist subscriptions across restarts (use with `cleanSession: false`)`topicAliasMaximum`int0MQTT 5 topic aliases to request (0 = disabled)`receiveMaximum`int65535MQTT 5 flow-control window`ackTimeout`float5.0Timeout (seconds) waiting for QoS 1/2 ACK before resend`maxResendAttempts`int3Max resend attempts for unacknowledged QoS 1/2 messages`maximumPacketSize`int16 MiBLargest accepted inbound packet; also sent as MQTT 5 property `0x27``inboundQueueSize`int1000Bound on the `awaitMessage()`/`messages()` queue (0 = unlimited)### Publish Options

[](#publish-options)

OptionTypeDefaultDescription`qos`QoSAtMostOnceQuality of Service level`retain`boolfalseRetain message on broker`properties`arraynullMQTT 5.0 properties### TLS Configuration

[](#tls-configuration)

Simple TLS (server verification only):

```
use ScienceStories\Mqtt\Client\TlsOptions;

// Note the explicit port: withTls() does not change it, and the default is 1883.
$options = (new Options('broker.example.com', 8883))->withTls(new TlsOptions());
```

> `withHost('other-broker')` resets the port to 1883. When changing host on an existing `Options`, pass both: `withHost('other-broker', 8883)`.

Mutual TLS with client certificate (AWS IoT, Azure IoT Hub):

```
use ScienceStories\Mqtt\Client\TlsOptions;

$tls = (new TlsOptions())
    ->withCaFile('/etc/mqtt/certs/ca.pem')
    ->withClientCertificate(
        certFile: '/etc/mqtt/certs/client.pem',
        keyFile: '/etc/mqtt/certs/client.key',
        passphrase: 'optional-passphrase',
    );

$options = $options->withTls($tls);
```

MQTT over port 443 with ALPN (when 8883 is blocked):

```
$tls = (new TlsOptions())
    ->withCaFile('/etc/mqtt/certs/ca.pem')
    ->withClientCertificate('/etc/mqtt/certs/client.pem', '/etc/mqtt/certs/client.key')
    ->withAlpn('mqtt');

$options = (new Options('broker.example.com', 443))->withTls($tls);
```

Self-signed certificates (development):

```
$tls = (new TlsOptions())
    ->withCaFile('/path/to/my-ca.pem')
    ->withAllowSelfSigned(true);

$options = $options->withTls($tls);
```

TlsOptions MethodDescription`withCaFile(?string)`CA certificate file for server verification`withCaPath(?string)`Directory of CA certificates`withClientCertificate(?string, ?string, ?string)`Client cert, key, and optional passphrase`withAlpn(?string)`ALPN protocol (e.g., `'mqtt'` for port 443)`withVerifyPeer(bool)`Verify server certificate (default: `true`)`withVerifyPeerName(bool)`Verify server hostname (default: `true`)`withAllowSelfSigned(bool)`Allow self-signed certs (default: `false`)`withPeerName(?string)`Override peer name for SNI`withSni(bool)`Enable/disable SNI (default: `true`)`withCryptoMethod(int)`Override negotiated TLS versions (bitmask of `STREAM_CRYPTO_METHOD_*_CLIENT`)> **TLS 1.2 and 1.3 only, by default.** PHP's own `STREAM_CRYPTO_METHOD_TLS_CLIENT` still enables TLS 1.0 and 1.1, which RFC 8996 marks MUST NOT and PCI-DSS prohibits. This client refuses them unless you widen the policy explicitly:
>
> ```
> // Only for a broker that genuinely cannot do TLS 1.2.
> $tls = (new TlsOptions())->withCryptoMethod(
>     STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT | TlsOptions::DEFAULT_CRYPTO_METHOD,
> );
> ```

> Legacy `array` syntax is still supported for backward compatibility: `$options->withTls(['ssl' => ['verify_peer' => true]])`

MQTT 5.0 Features
-----------------

[](#mqtt-50-features)

### Topic Aliases

[](#topic-aliases)

Topic aliases replace a repeated topic string with a two-byte integer. They are managed automatically — request a budget on `Options`, then publish normally:

```
$options = (new Options('broker.example.com', 1883, version: MqttVersion::V5_0))
    ->withTopicAliasMaximum(10);

$client = new Client($options, new TcpTransport());
$client->connect();

// First publish sends the topic and establishes the alias.
$client->publish('factory/line-3/press/temperature', '218.4');
// Later publishes to the same topic send two bytes instead of thirty-four.
$client->publish('factory/line-3/press/temperature', '218.9');
```

The broker's `topic_alias_maximum` in CONNACK overrides your request; if it advertises 0, aliasing is disabled and publishes fall back to full topic strings.

> Do not set the `topic_alias` property by hand on `PublishOptions`. Aliases are connection-scoped and negotiated — a hand-set value is either overwritten by the client or rejected by the broker with reason code 0x94 (Topic Alias Invalid).

### Message Expiry

[](#message-expiry)

Set expiration time for messages:

```
$client->publish('alerts/warning', 'Alert!', new PublishOptions(
    properties: ['message_expiry_interval' => 300], // 5 minutes
));
```

### User Properties

[](#user-properties)

Attach custom metadata to messages:

```
$client->publish('events/user', $payload, new PublishOptions(
    properties: [
        'user_properties' => [
            'source' => 'web-app',
            'version' => '1.0',
        ],
    ],
));
```

Error Handling
--------------

[](#error-handling)

Every error this library raises extends `ScienceStories\Mqtt\Exception\MqttException`, which extends `RuntimeException` — so a single `catch` covers the whole surface.

ExceptionRaised when`AuthenticationError`The broker refused the credentials (CONNACK 4/5 on 3.1.1, 0x86/0x87 on MQTT 5)`ServerError`The broker is unavailable, busy, or shutting down`QuotaExceeded`A broker rate or quota limit was hit`ProtocolError`A malformed packet, an oversized packet, or an invalid local configuration`Timeout`No ACK or data arrived within the deadline`TransportError`Socket or TLS failure — connection refused, closed by peer, handshake failed`connect()` throws rather than returning a failed result, so a refused connection cannot be mistaken for a live one:

```
use ScienceStories\Mqtt\Exception\AuthenticationError;
use ScienceStories\Mqtt\Exception\MqttException;
use ScienceStories\Mqtt\Exception\TransportError;

try {
    $client->connect();
} catch (AuthenticationError $e) {
    // Bad credentials — retrying will not help.
    throw $e;
} catch (TransportError|Timeout $e) {
    // Network-level; safe to retry with backoff.
    $logger->warning('Broker unreachable', ['error' => $e->getMessage()]);
}
```

A long-running loop should survive transient failures rather than dying on the first one:

```
$client->onMessage(fn ($message) => handle($message));

while (true) {
    try {
        $client->loopOnce(1.0);
    } catch (MqttException $e) {
        $logger->error('MQTT loop error', ['error' => $e->getMessage()]);
        usleep(500_000);
    }
}
```

With `withAutoReconnect()` enabled, `loopOnce()` re-establishes the connection and re-subscribes on its own; it returns `false` while disconnected rather than throwing.

Known Limitations
-----------------

[](#known-limitations)

Stated up front rather than discovered in production:

- **`wss://` does not work.** `WsTransport` performs the HTTP upgrade before TLS can be enabled, so only `ws://` completes a handshake. `ws://` itself has no test coverage yet.
- **Inbound MQTT 5 topic aliases are not resolved.** If a broker sends aliased PUBLISH packets, the topic arrives empty. Leave `withTopicAliasMaximum()` at 0 (the default).
- **Acknowledgement reason codes are not acted on.** A PUBACK or SUBACK carrying a failure code is logged, not raised — a rejected subscription looks successful.
- **The client is blocking.** There is no ReactPHP/Amp adapter yet; run it in a dedicated process or worker.
- **No Laravel or Symfony bridge yet.**

See [CHANGELOG.md](CHANGELOG.md) for what changed and what is planned.

Documentation
-------------

[](#documentation)

- [Upgrading from 1.x](UPGRADE.md) — what changed in 2.0 and how to migrate
- [Backward Compatibility Promise](docs/backward-compatibility.md) — what semver covers here
- [Roadmap](ROADMAP.md) — what is planned, and where help is wanted

Feature guides in `docs/`:

- [Testing](docs/testing.md) — unit-test your MQTT code with `InMemoryTransport`, no broker needed
- [Flow Control](docs/flow-control.md) — MQTT 5 receive-maximum and in-flight limits
- [Session Persistence](docs/session-persistence.md) — surviving restarts with `cleanSession: false`
- [Shared Subscriptions](docs/shared-subscriptions.md) — `$share/` load balancing across consumers
- [Topic Aliases](docs/topic-aliases.md) — MQTT 5 bandwidth optimisation
- [Server Disconnect](docs/server-disconnect.md) — reacting to a broker-initiated DISCONNECT

Examples
--------

[](#examples)

Check the `examples/` directory for complete working examples:

- Basic connect/publish/subscribe (MQTT 3.1.1 and 5.0)
- QoS 0, 1, 2 demonstrations
- **mTLS with client certificates** (`tls_mtls_example.php` + cert generation script)
- Session persistence, shared subscriptions, topic aliases
- Flow control, server disconnect handling

Testing
-------

[](#testing)

```
# Run tests
composer test

# Run tests with coverage
composer test:coverage

# Static analysis
composer stan

# Code style
composer pint
```

Contributing
------------

[](#contributing)

Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details.

License
-------

[](#license)

PHP IoT MQTT Client is open-sourced software licensed under the [MIT license](LICENSE.md).

Credits
-------

[](#credits)

Developed by [Bogdan Gewald](mailto:gewaldb@gmail.com).

UltraEmbeddedLab is the publishing organisation for this package; copyright is held by Bogdan Gewald, as stated in [LICENSE.md](LICENSE.md). Contributions are accepted under the [Developer Certificate of Origin](DCO) — inbound licence equals outbound licence, MIT. There is no copyright assignment and no CLA.

The PHP namespace is `ScienceStories\Mqtt\` for historical reasons: the package was originally published as `science-stories/php-iot`. It is unchanged for backwards compatibility and is scheduled to be renamed in 3.0 with `class_alias()` shims — see [ROADMAP.md](ROADMAP.md).

###  Health Score

43

—

FairBetter than 89% of packages

Maintenance88

Actively maintained with recent releases

Popularity10

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity56

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~25 days

Total

4

Last Release

136d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/105855972?v=4)[Science Stories](/maintainers/science-stories)[@Science-Stories](https://github.com/Science-Stories)

---

Top Contributors

[![electronic-club](https://avatars.githubusercontent.com/u/87986804?v=4)](https://github.com/electronic-club "electronic-club (30 commits)")

---

Tags

aws-iotemqxhivemqiotmessage-brokermessagingmosquittomqttmqtt-clientmqtt5mtlsphpphp8psr-14pubsubsmart-hometelemetrywebsocketclientiotmqttpub-subnetworkingphp84message-brokermqtt3mqtt5

###  Code Quality

TestsPest

Static AnalysisPHPStan, Rector

Code StyleLaravel Pint

Type Coverage Yes

### Embed Badge

![Health badge](/badges/ultraembeddedlab-php-iot/health.svg)

```
[![Health](https://phpackages.com/badges/ultraembeddedlab-php-iot/health.svg)](https://phpackages.com/packages/ultraembeddedlab-php-iot)
```

###  Alternatives

[symfony/symfony

The Symfony PHP framework

31.4k87.4M2.2k](/packages/symfony-symfony)[symfony/mailer

Helps sending emails

1.6k424.2M1.7k](/packages/symfony-mailer)[shopware/platform

The Shopware e-commerce core

3.4k1.5M3](/packages/shopware-platform)[drupal/core-recommended

Locked core dependencies; require this project INSTEAD OF drupal/core.

6943.5M449](/packages/drupal-core-recommended)[hyperf/hyperf

A coroutine framework that focuses on hyperspeed and flexibility. Building microservice or middleware with ease.

6.9k3.5k2](/packages/hyperf-hyperf)[shopware/core

Shopware platform is the core for all Shopware ecommerce products.

595.8M672](/packages/shopware-core)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
