This commit is contained in:
2026-01-06 10:02:25 +01:00
parent f685c2490a
commit b52d3a11be
111 changed files with 12830 additions and 76 deletions

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Concerns;
/**
* Provides common methods used to generate random client ids.
*
* @package PhpMqtt\Client\Concerns
*/
trait GeneratesRandomClientIds
{
/**
* Generates a random client id in the form of an md5 hash.
*/
protected function generateRandomClientId(): string
{
return substr(md5(uniqid((string) random_int(0, PHP_INT_MAX), true)), 0, 20);
}
}

View File

@@ -0,0 +1,301 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Concerns;
use PhpMqtt\Client\Contracts\MqttClient;
/**
* Contains common methods and properties necessary to offer hooks.
*
* @mixin MqttClient
* @package PhpMqtt\Client\Concerns
*/
trait OffersHooks
{
/** @var \SplObjectStorage|array<\Closure> */
private $loopEventHandlers;
/** @var \SplObjectStorage|array<\Closure> */
private $publishEventHandlers;
/** @var \SplObjectStorage|array<\Closure> */
private $messageReceivedEventHandlers;
/** @var \SplObjectStorage|array<\Closure> */
private $connectedEventHandlers;
/**
* Needs to be called in order to initialize the trait.
*/
protected function initializeEventHandlers(): void
{
$this->loopEventHandlers = new \SplObjectStorage();
$this->publishEventHandlers = new \SplObjectStorage();
$this->messageReceivedEventHandlers = new \SplObjectStorage();
$this->connectedEventHandlers = new \SplObjectStorage();
}
/**
* Registers a loop event handler which is called each iteration of the loop.
* This event handler can be used for example to interrupt the loop under
* certain conditions.
*
* The loop event handler is passed the MQTT client instance as first and
* the elapsed time which the loop is already running for as second
* parameter. The elapsed time is a float containing seconds.
*
* Example:
* ```php
* $mqtt->registerLoopEventHandler(function (
* MqttClient $mqtt,
* float $elapsedTime
* ) use ($logger) {
* $logger->info("Running for [{$elapsedTime}] seconds already.");
* });
* ```
*
* Multiple event handlers can be registered at the same time.
*/
public function registerLoopEventHandler(\Closure $callback): MqttClient
{
$this->loopEventHandlers->attach($callback);
/** @var MqttClient $this */
return $this;
}
/**
* Unregisters a loop event handler which prevents it from being called
* in the future.
*
* This does not affect other registered event handlers. It is possible
* to unregister all registered event handlers by passing null as callback.
*/
public function unregisterLoopEventHandler(?\Closure $callback = null): MqttClient
{
if ($callback === null) {
$this->loopEventHandlers->removeAll($this->loopEventHandlers);
} else {
$this->loopEventHandlers->detach($callback);
}
/** @var MqttClient $this */
return $this;
}
/**
* Runs all registered loop event handlers with the given parameters.
* Each event handler is executed in a try-catch block to avoid spilling exceptions.
*/
private function runLoopEventHandlers(float $elapsedTime): void
{
foreach ($this->loopEventHandlers as $handler) {
try {
call_user_func($handler, $this, $elapsedTime);
} catch (\Throwable $e) {
$this->logger->error('Loop hook callback threw exception.', ['exception' => $e]);
}
}
}
/**
* Registers a loop event handler which is called when a message is published.
*
* The loop event handler is passed the MQTT client as first, the topic as
* second and the message as third parameter. As fourth parameter, the message identifier
* will be passed, which can be null in case of QoS 0. The QoS level as well as the retained
* flag will also be passed as fifth and sixth parameters.
*
* Example:
* ```php
* $mqtt->registerPublishEventHandler(function (
* MqttClient $mqtt,
* string $topic,
* string $message,
* ?int $messageId,
* int $qualityOfService,
* bool $retain
* ) use ($logger) {
* $logger->info("Sending message on topic [{$topic}]: {$message}");
* });
* ```
*
* Multiple event handlers can be registered at the same time.
*/
public function registerPublishEventHandler(\Closure $callback): MqttClient
{
$this->publishEventHandlers->attach($callback);
/** @var MqttClient $this */
return $this;
}
/**
* Unregisters a publish event handler which prevents it from being called
* in the future.
*
* This does not affect other registered event handlers. It is possible
* to unregister all registered event handlers by passing null as callback.
*/
public function unregisterPublishEventHandler(?\Closure $callback = null): MqttClient
{
if ($callback === null) {
$this->publishEventHandlers->removeAll($this->publishEventHandlers);
} else {
$this->publishEventHandlers->detach($callback);
}
/** @var MqttClient $this */
return $this;
}
/**
* Runs all the registered publish event handlers with the given parameters.
* Each event handler is executed in a try-catch block to avoid spilling exceptions.
*/
private function runPublishEventHandlers(string $topic, string $message, ?int $messageId, int $qualityOfService, bool $retain): void
{
foreach ($this->publishEventHandlers as $handler) {
try {
call_user_func($handler, $this, $topic, $message, $messageId, $qualityOfService, $retain);
} catch (\Throwable $e) {
$this->logger->error('Publish hook callback threw exception for published message on topic [{topic}].', [
'topic' => $topic,
'exception' => $e,
]);
}
}
}
/**
* Registers an event handler which is called when a message is received from the broker.
*
* The message received event handler is passed the MQTT client as first, the topic as
* second and the message as third parameter. As fourth parameter, the QoS level will be
* passed and the retained flag as fifth.
*
* Example:
* ```php
* $mqtt->registerReceivedMessageEventHandler(function (
* MqttClient $mqtt,
* string $topic,
* string $message,
* int $qualityOfService,
* bool $retained
* ) use ($logger) {
* $logger->info("Received message on topic [{$topic}]: {$message}");
* });
* ```
*
* Multiple event handlers can be registered at the same time.
*/
public function registerMessageReceivedEventHandler(\Closure $callback): MqttClient
{
$this->messageReceivedEventHandlers->attach($callback);
/** @var MqttClient $this */
return $this;
}
/**
* Unregisters a message received event handler which prevents it from being called in the future.
*
* This does not affect other registered event handlers. It is possible
* to unregister all registered event handlers by passing null as callback.
*/
public function unregisterMessageReceivedEventHandler(?\Closure $callback = null): MqttClient
{
if ($callback === null) {
$this->messageReceivedEventHandlers->removeAll($this->messageReceivedEventHandlers);
} else {
$this->messageReceivedEventHandlers->detach($callback);
}
/** @var MqttClient $this */
return $this;
}
/**
* Runs all the registered message received event handlers with the given parameters.
* Each event handler is executed in a try-catch block to avoid spilling exceptions.
*/
private function runMessageReceivedEventHandlers(string $topic, string $message, int $qualityOfService, bool $retained): void
{
foreach ($this->messageReceivedEventHandlers as $handler) {
try {
call_user_func($handler, $this, $topic, $message, $qualityOfService, $retained);
} catch (\Throwable $e) {
$this->logger->error('Received message hook callback threw exception for received message on topic [{topic}].', [
'topic' => $topic,
'exception' => $e,
]);
}
}
}
/**
* Registers an event handler which is called when the client established a connection to the broker.
* This also includes manual reconnects as well as auto-reconnects by the client itself.
*
* The event handler is passed the MQTT client as first argument,
* followed by a flag which indicates whether an auto-reconnect occurred as second argument.
*
* Example:
* ```php
* $mqtt->registerConnectedEventHandler(function (
* MqttClient $mqtt,
* bool $isAutoReconnect
* ) use ($logger) {
* if ($isAutoReconnect) {
* $logger->info("Client successfully auto-reconnected to the broker.);
* } else {
* $logger->info("Client successfully connected to the broker.");
* }
* });
* ```
*
* Multiple event handlers can be registered at the same time.
*/
public function registerConnectedEventHandler(\Closure $callback): MqttClient
{
$this->connectedEventHandlers->attach($callback);
/** @var MqttClient $this */
return $this;
}
/**
* Unregisters a connected event handler which prevents it from being called in the future.
*
* This does not affect other registered event handlers. It is possible
* to unregister all registered event handlers by passing null as callback.
*/
public function unregisterConnectedEventHandler(?\Closure $callback = null): MqttClient
{
if ($callback === null) {
$this->connectedEventHandlers->removeAll($this->connectedEventHandlers);
} else {
$this->connectedEventHandlers->detach($callback);
}
/** @var MqttClient $this */
return $this;
}
/**
* Runs all the registered connected event handlers.
* Each event handler is executed in a try-catch block to avoid spilling exceptions.
*/
private function runConnectedEventHandlers(bool $isAutoReconnect): void
{
foreach ($this->connectedEventHandlers as $handler) {
try {
call_user_func($handler, $this, $isAutoReconnect);
} catch (\Throwable $e) {
$this->logger->error('Connected hook callback threw exception.', ['exception' => $e]);
}
}
}
}

View File

@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Concerns;
/**
* Provides common methods to encode data before sending it to a broker
* and to decode data received from a broker.
*
* @package PhpMqtt\Client\Concerns
*/
trait TranscodesData
{
/**
* Creates a string which is prefixed with its own length as bytes.
* This means a string like 'hello world' will become
*
* \x00\x0bhello world
*
* where \x00\0x0b is the hex representation of 00000000 00001011 = 11
*/
protected function buildLengthPrefixedString(string $data): string
{
$length = strlen($data);
$msb = $length >> 8;
$lsb = $length % 256;
return chr($msb) . chr($lsb) . $data;
}
/**
* Converts the given string to a number, assuming it is an MSB encoded message id.
* MSB means preceding characters have higher value.
*/
protected function decodeMessageId(string $encodedMessageId): int
{
$length = strlen($encodedMessageId);
$result = 0;
foreach (str_split($encodedMessageId) as $index => $char) {
$result += ord($char) << (($length - 1) * 8 - ($index * 8));
}
return $result;
}
/**
* Encodes the given message identifier as string.
*/
protected function encodeMessageId(int $messageId): string
{
return chr($messageId >> 8) . chr($messageId % 256);
}
/**
* Encodes the length of a message as string, so it can be transmitted
* over the wire.
*/
protected function encodeMessageLength(int $length): string
{
$result = '';
do {
$digit = $length % 128;
$length = $length >> 7;
// if there are more digits to encode, set the top bit of this digit
if ($length > 0) {
$digit = ($digit | 0x80);
}
$result .= chr($digit);
} while ($length > 0);
return $result;
}
}

View File

@@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Concerns;
use PhpMqtt\Client\ConnectionSettings;
use PhpMqtt\Client\Exceptions\ConfigurationInvalidException;
use PhpMqtt\Client\MqttClient;
/**
* Provides methods to validate the configuration of an {@see MqttClient} and
* the {@see ConnectionSettings} being used to connect to a broker.
*
* @package PhpMqtt\Client\Concerns
*/
trait ValidatesConfiguration
{
/**
* Ensures the given connection settings are valid. If they are not valid,
* which means they are misconfigured, an exception containing information about
* the configuration error is thrown.
*
* @throws ConfigurationInvalidException
*/
protected function ensureConnectionSettingsAreValid(ConnectionSettings $settings): void
{
if ($settings->getConnectTimeout() < 1) {
throw new ConfigurationInvalidException('The connect timeout cannot be less than 1 second.');
}
if ($settings->getSocketTimeout() < 1) {
throw new ConfigurationInvalidException('The socket timeout cannot be less than 1 second.');
}
if ($settings->getResendTimeout() < 1) {
throw new ConfigurationInvalidException('The resend timeout cannot be less than 1 second.');
}
if ($settings->getKeepAliveInterval() < 1 || $settings->getKeepAliveInterval() > 65535) {
throw new ConfigurationInvalidException('The keep alive interval must be a value in the range of 1 to 65535 seconds.');
}
if ($settings->getMaxReconnectAttempts() < 1) {
throw new ConfigurationInvalidException('The maximum reconnect attempts cannot be fewer than 1.');
}
if ($settings->getDelayBetweenReconnectAttempts() < 0) {
throw new ConfigurationInvalidException('The delay between reconnect attempts cannot be lower than 0.');
}
if ($settings->getUsername() !== null && trim($settings->getUsername()) === '') {
throw new ConfigurationInvalidException('The username may not consist of white space only.');
}
if ($settings->getLastWillTopic() !== null && trim($settings->getLastWillTopic()) === '') {
throw new ConfigurationInvalidException('The last will topic may not consist of white space only.');
}
if ($settings->getLastWillQualityOfService() < MqttClient::QOS_AT_MOST_ONCE
|| $settings->getLastWillQualityOfService() > MqttClient::QOS_EXACTLY_ONCE) {
throw new ConfigurationInvalidException('The QoS for the last will must be a value in the range of 0 to 2.');
}
if ($settings->getTlsCertificateAuthorityFile() !== null && !is_file($settings->getTlsCertificateAuthorityFile())) {
throw new ConfigurationInvalidException('The Certificate Authority file setting must contain the path to a regular file.');
}
if ($settings->getTlsCertificateAuthorityPath() !== null && !is_dir($settings->getTlsCertificateAuthorityPath())) {
throw new ConfigurationInvalidException('The Certificate Authority path setting must contain the path to a directory.');
}
if ($settings->getTlsClientCertificateFile() !== null && !is_file($settings->getTlsClientCertificateFile())) {
throw new ConfigurationInvalidException('The client certificate file setting must contain the path to a regular file.');
}
if ($settings->getTlsClientCertificateKeyFile() !== null && !is_file($settings->getTlsClientCertificateKeyFile())) {
throw new ConfigurationInvalidException('The client certificate key file setting must contain the path to a regular file.');
}
if ($settings->getTlsClientCertificateKeyFile() !== null && $settings->getTlsClientCertificateFile() === null) {
throw new ConfigurationInvalidException('Using a client certificate key file without certificate does not work.');
}
if ($settings->getTlsClientCertificateKeyPassphrase() !== null && $settings->getTlsClientCertificateKeyFile() === null) {
throw new ConfigurationInvalidException('Using a client certificate key passphrase without key file does not work.');
}
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Concerns;
/**
* Provides common methods to work with buffers.
*
* @package PhpMqtt\Client\Concerns
*/
trait WorksWithBuffers
{
/**
* Pops the first $limit bytes from the given buffer and returns them.
*/
protected function pop(string &$buffer, int $limit): string
{
$limit = min(strlen($buffer), $limit);
$result = substr($buffer, 0, $limit);
$buffer = substr($buffer, $limit);
return $result;
}
}

View File

@@ -0,0 +1,555 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client;
/**
* The settings used during connection to a broker.
*
* This class is immutable and all setters return a clone of the original class because
* connection settings must not change once passed to MqttClient.
*
* @package PhpMqtt\Client
*/
class ConnectionSettings
{
private ?string $username = null;
private ?string $password = null;
private bool $useBlockingSocket = false;
private int $connectTimeout = 60;
private int $socketTimeout = 5;
private int $resendTimeout = 10;
private int $keepAliveInterval = 10;
private bool $reconnectAutomatically = false;
private int $maxReconnectAttempts = 3;
private int $delayBetweenReconnectAttempts = 0;
private ?string $lastWillTopic = null;
private ?string $lastWillMessage = null;
private int $lastWillQualityOfService = 0;
private bool $lastWillRetain = false;
private bool $useTls = false;
private bool $tlsVerifyPeer = true;
private bool $tlsVerifyPeerName = true;
private bool $tlsSelfSignedAllowed = false;
private ?string $tlsCertificateAuthorityFile = null;
private ?string $tlsCertificateAuthorityPath = null;
private ?string $tlsClientCertificateFile = null;
private ?string $tlsClientCertificateKeyFile = null;
private ?string $tlsClientCertificateKeyPassphrase = null;
private ?string $tlsAlpn = null;
/**
* The username used for authentication when connecting to the broker.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setUsername(?string $username): ConnectionSettings
{
$copy = clone $this;
$copy->username = $username;
return $copy;
}
public function getUsername(): ?string
{
return $this->username;
}
/**
* The password used for authentication when connecting to the broker.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setPassword(?string $password): ConnectionSettings
{
$copy = clone $this;
$copy->password = $password;
return $copy;
}
public function getPassword(): ?string
{
return $this->password;
}
/**
* Whether to use a blocking socket when publishing messages or not.
* Normally, this setting can be ignored. When publishing large messages with multiple kilobytes in size,
* a blocking socket may be required if the receipt buffer of the broker is not large enough.
*
* Note: This setting has no effect on subscriptions, only on the publishing of messages.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function useBlockingSocket(bool $useBlockingSocket): ConnectionSettings
{
$copy = clone $this;
$copy->useBlockingSocket = $useBlockingSocket;
return $copy;
}
public function shouldUseBlockingSocket(): bool
{
return $this->useBlockingSocket;
}
/**
* The connect timeout is the maximum amount of seconds the client will try to establish
* a socket connection with the broker. The value cannot be less than 1 second.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setConnectTimeout(int $connectTimeout): ConnectionSettings
{
$copy = clone $this;
$copy->connectTimeout = $connectTimeout;
return $copy;
}
public function getConnectTimeout(): int
{
return $this->connectTimeout;
}
/**
* The socket timeout is the maximum amount of idle time in seconds for the socket connection.
* If no data is read or sent for the given amount of seconds, the socket will be closed.
* The value cannot be less than 1 second.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setSocketTimeout(int $socketTimeout): ConnectionSettings
{
$copy = clone $this;
$copy->socketTimeout = $socketTimeout;
return $copy;
}
public function getSocketTimeout(): int
{
return $this->socketTimeout;
}
/**
* The resend timeout is the number of seconds the client will wait before sending a duplicate
* of pending messages without acknowledgement. The value cannot be less than 1 second.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setResendTimeout(int $resendTimeout): ConnectionSettings
{
$copy = clone $this;
$copy->resendTimeout = $resendTimeout;
return $copy;
}
public function getResendTimeout(): int
{
return $this->resendTimeout;
}
/**
* The keep alive interval is the number of seconds the client will wait without sending a message
* until it sends a keep alive signal (ping) to the broker. The value cannot be less than 1 second
* and may not be higher than 65535 seconds. A reasonable value is 10 seconds (the default).
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setKeepAliveInterval(int $keepAliveInterval): ConnectionSettings
{
$copy = clone $this;
$copy->keepAliveInterval = $keepAliveInterval;
return $copy;
}
public function getKeepAliveInterval(): int
{
return $this->keepAliveInterval;
}
/**
* This flag determines whether the client will try to reconnect automatically,
* if it notices a disconnect while sending data.
* The setting cannot be used together with the clean session flag.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setReconnectAutomatically(bool $reconnectAutomatically): ConnectionSettings
{
$copy = clone $this;
$copy->reconnectAutomatically = $reconnectAutomatically;
return $copy;
}
public function shouldReconnectAutomatically(): bool
{
return $this->reconnectAutomatically;
}
/**
* Defines the maximum number of reconnect attempts until the client gives up. This setting
* is only relevant if {@see setReconnectAutomatically()} is set to true.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setMaxReconnectAttempts(int $maxReconnectAttempts): ConnectionSettings
{
$copy = clone $this;
$copy->maxReconnectAttempts = $maxReconnectAttempts;
return $copy;
}
public function getMaxReconnectAttempts(): int
{
return $this->maxReconnectAttempts;
}
/**
* Defines the delay between reconnect attempts in milliseconds.
* This setting is only relevant if {@see setReconnectAutomatically()} is set to true.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setDelayBetweenReconnectAttempts(int $delayBetweenReconnectAttempts): ConnectionSettings
{
$copy = clone $this;
$copy->delayBetweenReconnectAttempts = $delayBetweenReconnectAttempts;
return $copy;
}
public function getDelayBetweenReconnectAttempts(): int
{
return $this->delayBetweenReconnectAttempts;
}
/**
* If the broker should publish a last will message in the name of the client when the client
* disconnects abruptly, this setting defines the topic on which the message will be published.
*
* A last will message will only be published if both this setting as well as the last will
* message are configured.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setLastWillTopic(?string $lastWillTopic): ConnectionSettings
{
$copy = clone $this;
$copy->lastWillTopic = $lastWillTopic;
return $copy;
}
public function getLastWillTopic(): ?string
{
return $this->lastWillTopic;
}
/**
* If the broker should publish a last will message in the name of the client when the client
* disconnects abruptly, this setting defines the message which will be published.
*
* A last will message will only be published if both this setting as well as the last will
* topic are configured.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setLastWillMessage(?string $lastWillMessage): ConnectionSettings
{
$copy = clone $this;
$copy->lastWillMessage = $lastWillMessage;
return $copy;
}
public function getLastWillMessage(): ?string
{
return $this->lastWillMessage;
}
/**
* Determines whether the client has a last will.
*/
public function hasLastWill(): bool
{
return $this->lastWillTopic !== null && $this->lastWillMessage !== null;
}
/**
* The quality of service level the last will message of the client will be published with,
* if it gets triggered.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setLastWillQualityOfService(int $lastWillQualityOfService): ConnectionSettings
{
$copy = clone $this;
$copy->lastWillQualityOfService = $lastWillQualityOfService;
return $copy;
}
public function getLastWillQualityOfService(): int
{
return $this->lastWillQualityOfService;
}
/**
* This flag determines if the last will message of the client will be retained, if it gets
* triggered. Using this setting can be handy to signal that a client is offline by publishing
* a retained offline state in the last will and an online state as first message on connect.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setRetainLastWill(bool $lastWillRetain): ConnectionSettings
{
$copy = clone $this;
$copy->lastWillRetain = $lastWillRetain;
return $copy;
}
public function shouldRetainLastWill(): bool
{
return $this->lastWillRetain;
}
/**
* This flag determines if TLS should be used for the connection. The port which is used to
* connect to the broker must support TLS connections.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setUseTls(bool $useTls): ConnectionSettings
{
$copy = clone $this;
$copy->useTls = $useTls;
return $copy;
}
public function shouldUseTls(): bool
{
return $this->useTls;
}
/**
* This flag determines if the peer certificate is verified, if TLS is used.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setTlsVerifyPeer(bool $tlsVerifyPeer): ConnectionSettings
{
$copy = clone $this;
$copy->tlsVerifyPeer = $tlsVerifyPeer;
return $copy;
}
public function shouldTlsVerifyPeer(): bool
{
return $this->tlsVerifyPeer;
}
/**
* This flag determines if the peer name is verified, if TLS is used.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setTlsVerifyPeerName(bool $tlsVerifyPeerName): ConnectionSettings
{
$copy = clone $this;
$copy->tlsVerifyPeerName = $tlsVerifyPeerName;
return $copy;
}
public function shouldTlsVerifyPeerName(): bool
{
return $this->tlsVerifyPeerName;
}
/**
* This flag determines if self signed certificates of the peer should be accepted.
* Setting this to TRUE implies a security risk and should be avoided for production
* scenarios and public services.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setTlsSelfSignedAllowed(bool $tlsSelfSignedAllowed): ConnectionSettings
{
$copy = clone $this;
$copy->tlsSelfSignedAllowed = $tlsSelfSignedAllowed;
return $copy;
}
public function isTlsSelfSignedAllowed(): bool
{
return $this->tlsSelfSignedAllowed;
}
/**
* The path to a Certificate Authority certificate which is used to verify the peer
* certificate, if TLS is used.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setTlsCertificateAuthorityFile(?string $tlsCertificateAuthorityFile): ConnectionSettings
{
$copy = clone $this;
$copy->tlsCertificateAuthorityFile = $tlsCertificateAuthorityFile;
return $copy;
}
public function getTlsCertificateAuthorityFile(): ?string
{
return $this->tlsCertificateAuthorityFile;
}
/**
* The path to a directory containing Certificate Authority certificates which are
* used to verify the peer certificate, if TLS is used.
*
* Certificate files in this directory must be named by the hash of the certificate,
* ending with ".0" (without quotes). The certificate hash can be retrieved using the
* openssl_x509_parse() function, which returns an array. The hash can be found in the
* array under the key "hash".
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setTlsCertificateAuthorityPath(?string $tlsCertificateAuthorityPath): ConnectionSettings
{
$copy = clone $this;
$copy->tlsCertificateAuthorityPath = $tlsCertificateAuthorityPath;
return $copy;
}
public function getTlsCertificateAuthorityPath(): ?string
{
return $this->tlsCertificateAuthorityPath;
}
/**
* The path to a client certificate file used for authentication, if TLS is used.
*
* The client certificate must be PEM encoded. It may optionally contain the
* certificate chain of issuers. The certificate key can be included in this certificate
* file or in a separate file ({@see ConnectionSettings::setTlsClientCertificateKeyFile()}).
* A passphrase can be configured using {@see ConnectionSettings::setTlsClientCertificateKeyPassphrase()}.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setTlsClientCertificateFile(?string $tlsClientCertificateFile): ConnectionSettings
{
$copy = clone $this;
$copy->tlsClientCertificateFile = $tlsClientCertificateFile;
return $copy;
}
public function getTlsClientCertificateFile(): ?string
{
return $this->tlsClientCertificateFile;
}
/**
* The path to a client certificate key file used for authentication, if TLS is used.
*
* This option requires {@see ConnectionSettings::setTlsClientCertificateFile()}
* to be used as well.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setTlsClientCertificateKeyFile(?string $tlsClientCertificateKeyFile): ConnectionSettings
{
$copy = clone $this;
$copy->tlsClientCertificateKeyFile = $tlsClientCertificateKeyFile;
return $copy;
}
public function getTlsClientCertificateKeyFile(): ?string
{
return $this->tlsClientCertificateKeyFile;
}
/**
* The passphrase used to decrypt the private key of the client certificate,
* which in return is used for authentication, if TLS is used.
*
* This option requires {@see ConnectionSettings::setTlsClientCertificateFile()}
* and {@see ConnectionSettings::setTlsClientCertificateKeyFile()} to be used as well.
*
* Please be aware that your passphrase is not stored in secure memory when using this option.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setTlsClientCertificateKeyPassphrase(?string $tlsClientCertificateKeyPassphrase): ConnectionSettings
{
$copy = clone $this;
$copy->tlsClientCertificateKeyPassphrase = $tlsClientCertificateKeyPassphrase;
return $copy;
}
public function getTlsClientCertificateKeyPassphrase(): ?string
{
return $this->tlsClientCertificateKeyPassphrase;
}
/**
* The TLS ALPN is used to establish a TLS encrypted mqtt connection on port 443,
* which usually is reserved for TLS encrypted HTTP traffic.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setTlsAlpn(?string $tlsAlpn): ConnectionSettings
{
$copy = clone $this;
$copy->tlsAlpn = $tlsAlpn;
return $copy;
}
public function getTlsAlpn(): ?string
{
return $this->tlsAlpn;
}
}

View File

@@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Contracts;
use PhpMqtt\Client\ConnectionSettings;
use PhpMqtt\Client\Exceptions\ConnectingToBrokerFailedException;
use PhpMqtt\Client\Exceptions\InvalidMessageException;
use PhpMqtt\Client\Exceptions\MqttClientException;
use PhpMqtt\Client\Exceptions\ProtocolViolationException;
use PhpMqtt\Client\Message;
use PhpMqtt\Client\Subscription;
/**
* Implementations of this interface provide message parsing capabilities.
* Services of this type are used by the {@see MqttClient} to implement multiple protocol versions.
*
* @package PhpMqtt\Client\Contracts
*/
interface MessageProcessor
{
/**
* Try to parse a message from the incoming buffer. If a message could be parsed successfully,
* the given message parameter is set to the parsed message and the result is true.
* If no message could be parsed, the result is false and the required bytes parameter indicates
* how many bytes are missing for the message to be complete. If this parameter is set to -1,
* it means we have no (or not yet) knowledge about the required bytes.
*/
public function tryFindMessageInBuffer(string $buffer, int $bufferLength, ?string &$message = null, int &$requiredBytes = -1): bool;
/**
* Parses and validates the given message based on its message type and contents.
* If no valid message could be found in the data, and no further action is required by the caller,
* null is returned.
*
* @throws InvalidMessageException
* @throws ProtocolViolationException
* @throws MqttClientException
*/
public function parseAndValidateMessage(string $message): ?Message;
/**
* Builds a connect message from the given connection settings, taking the protocol
* specifics into account.
*/
public function buildConnectMessage(ConnectionSettings $connectionSettings, bool $useCleanSession = false): string;
/**
* Builds a ping request message.
*/
public function buildPingRequestMessage(): string;
/**
* Builds a ping response message.
*/
public function buildPingResponseMessage(): string;
/**
* Builds a disconnect message.
*/
public function buildDisconnectMessage(): string;
/**
* Builds a subscribe message from the given parameters.
*
* @param Subscription[] $subscriptions
*/
public function buildSubscribeMessage(int $messageId, array $subscriptions, bool $isDuplicate = false): string;
/**
* Builds an unsubscribe message from the given parameters.
*
* @param string[] $topics
*/
public function buildUnsubscribeMessage(int $messageId, array $topics, bool $isDuplicate = false): string;
/**
* Builds a publish message based on the given parameters.
*/
public function buildPublishMessage(
string $topic,
string $message,
int $qualityOfService,
bool $retain,
?int $messageId = null,
bool $isDuplicate = false,
): string;
/**
* Builds a publish acknowledgement for the given message identifier.
*/
public function buildPublishAcknowledgementMessage(int $messageId): string;
/**
* Builds a publish received message for the given message identifier.
*/
public function buildPublishReceivedMessage(int $messageId): string;
/**
* Builds a publish release message for the given message identifier.
*/
public function buildPublishReleaseMessage(int $messageId): string;
/**
* Builds a publish complete message for the given message identifier.
*/
public function buildPublishCompleteMessage(int $messageId): string;
/**
* Handles the connect acknowledgement received from the broker. Exits normally if the
* connection could be established successfully according to the response. Throws an
* exception if the broker responded with an error.
*
* @throws ConnectingToBrokerFailedException
*/
public function handleConnectAcknowledgement(string $message): void;
}

View File

@@ -0,0 +1,266 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Contracts;
use PhpMqtt\Client\ConnectionSettings;
use PhpMqtt\Client\Exceptions\ConfigurationInvalidException;
use PhpMqtt\Client\Exceptions\ConnectingToBrokerFailedException;
use PhpMqtt\Client\Exceptions\DataTransferException;
use PhpMqtt\Client\Exceptions\InvalidMessageException;
use PhpMqtt\Client\Exceptions\MqttClientException;
use PhpMqtt\Client\Exceptions\ProtocolViolationException;
use PhpMqtt\Client\Exceptions\RepositoryException;
/**
* An interface for the MQTT client.
*
* @package PhpMqtt\Client\Contracts
*/
interface MqttClient
{
/**
* Connect to the MQTT broker using the given settings.
* If no custom settings are passed, the client will use the default settings.
* See {@see ConnectionSettings} for more details about the defaults.
*
* @throws ConfigurationInvalidException
* @throws ConnectingToBrokerFailedException
*/
public function connect(?ConnectionSettings $settings = null, bool $useCleanSession = false): void;
/**
* Sends a disconnect message to the broker and closes the socket.
*
* @throws DataTransferException
*/
public function disconnect(): void;
/**
* Returns an indication, whether the client is supposed to be connected already or not.
*
* Note: the result of this method should be used carefully, since we can only detect a
* closed socket once we try to send or receive data. Therefore, this method only gives
* an indication whether the client is in a connected state or not.
*
* This information may be useful in applications where multiple parts use the client.
*/
public function isConnected(): bool;
/**
* Publishes the given message on the given topic. If the additional quality of service
* and retention flags are set, the message will be published using these settings.
*
* @throws DataTransferException
* @throws RepositoryException
*/
public function publish(string $topic, string $message, int $qualityOfService = 0, bool $retain = false): void;
/**
* Subscribe to the given topic with the given quality of service.
*
* The subscription callback is passed the topic as first and the message as second
* parameter. A third parameter indicates whether the received message has been sent
* because it was retained by the broker. A fourth parameter contains matched topic wildcards.
*
* Example:
* ```php
* $mqtt->subscribe(
* '/foo/bar/+',
* function (string $topic, string $message, bool $retained, array $matchedWildcards) use ($logger) {
* $logger->info("Received {retained} message on topic [{topic}]: {message}", [
* 'topic' => $topic,
* 'message' => $message,
* 'retained' => $retained ? 'retained' : 'live'
* ]);
* }
* );
* ```
*
* If no callback is passed, a subscription will still be made. Received messages are delivered only to
* event handlers for received messages though.
*
* @throws DataTransferException
* @throws RepositoryException
*/
public function subscribe(string $topicFilter, ?callable $callback = null, int $qualityOfService = 0): void;
/**
* Unsubscribe from the given topic.
*
* @throws DataTransferException
* @throws RepositoryException
*/
public function unsubscribe(string $topicFilter): void;
/**
* Sets the interrupted signal. Doing so instructs the client to exit the loop, if it is
* actually looping.
*
* Sending multiple interrupt signals has no effect, unless the client exits the loop,
* which resets the signal for another loop.
*/
public function interrupt(): void;
/**
* Runs an event loop that handles messages from the server and calls the registered
* callbacks for published messages.
*
* If the second parameter is provided, the loop will exit as soon as all
* queues are empty. This means there may be no open subscriptions,
* no pending messages as well as acknowledgments and no pending unsubscribe requests.
*
* The third parameter will, if set, lead to a forceful exit after the specified
* amount of seconds, but only if the second parameter is set to true. This basically
* means that if we wait for all pending messages to be acknowledged, we only wait
* a maximum of $queueWaitLimit seconds until we give up. We do not exit after the
* given amount of time if there are open topic subscriptions though.
*
* @throws DataTransferException
* @throws InvalidMessageException
* @throws MqttClientException
* @throws ProtocolViolationException
*/
public function loop(bool $allowSleep = true, bool $exitWhenQueuesEmpty = false, ?int $queueWaitLimit = null): void;
/**
* Runs an event loop iteration that handles messages from the server and calls the registered
* callbacks for published messages. Also resends pending messages and calls loop event handlers.
*
* This method can be used to integrate the MQTT client in another event loop (like ReactPHP or Ratchet).
*
* Note: To ensure the event handlers called by this method will receive the correct elapsed time,
* the caller is responsible to provide the correct starting time of the loop as returned by `microtime(true)`.
*
* @throws DataTransferException
* @throws InvalidMessageException
* @throws MqttClientException
* @throws ProtocolViolationException
*/
public function loopOnce(float $loopStartedAt, bool $allowSleep = false, int $sleepMicroseconds = 100000): void;
/**
* Returns the host used by the client to connect to.
*/
public function getHost(): string;
/**
* Returns the port used by the client to connect to.
*/
public function getPort(): int;
/**
* Returns the identifier used by the client.
*/
public function getClientId(): string;
/**
* Returns the total number of received bytes, across reconnects.
*/
public function getReceivedBytes(): int;
/**
* Returns the total number of sent bytes, across reconnects.
*/
public function getSentBytes(): int;
/**
* Registers a loop event handler which is called each iteration of the loop.
* This event handler can be used for example to interrupt the loop under
* certain conditions.
*
* The loop event handler is passed the MQTT client instance as first and
* the elapsed time which the loop is already running for as second
* parameter. The elapsed time is a float containing seconds.
*
* Example:
* ```php
* $mqtt->registerLoopEventHandler(function (
* MqttClient $mqtt,
* float $elapsedTime
* ) use ($logger) {
* $logger->info("Running for [{$elapsedTime}] seconds already.");
* });
* ```
*
* Multiple event handlers can be registered at the same time.
*/
public function registerLoopEventHandler(\Closure $callback): MqttClient;
/**
* Unregisters a loop event handler which prevents it from being called
* in the future.
*
* This does not affect other registered event handlers. It is possible
* to unregister all registered event handlers by passing null as callback.
*/
public function unregisterLoopEventHandler(?\Closure $callback = null): MqttClient;
/**
* Registers a loop event handler which is called when a message is published.
*
* The loop event handler is passed the MQTT client as first, the topic as
* second and the message as third parameter. As fourth parameter, the
* message identifier will be passed. The QoS level as well as the retained
* flag will also be passed as fifth and sixth parameters.
*
* Example:
* ```php
* $mqtt->registerPublishEventHandler(function (
* MqttClient $mqtt,
* string $topic,
* string $message,
* int $messageId,
* int $qualityOfService,
* bool $retain
* ) use ($logger) {
* $logger->info("Received message on topic [{$topic}]: {$message}");
* });
* ```
*
* Multiple event handlers can be registered at the same time.
*/
public function registerPublishEventHandler(\Closure $callback): MqttClient;
/**
* Unregisters a publish event handler which prevents it from being called
* in the future.
*
* This does not affect other registered event handlers. It is possible
* to unregister all registered event handlers by passing null as callback.
*/
public function unregisterPublishEventHandler(?\Closure $callback = null): MqttClient;
/**
* Registers an event handler which is called when a message is received from the broker.
*
* The message received event handler is passed the MQTT client as first, the topic as
* second and the message as third parameter. As fourth parameter, the QoS level will be
* passed and the retained flag as fifth.
*
* Example:
* ```php
* $mqtt->registerReceivedMessageEventHandler(function (
* MqttClient $mqtt,
* string $topic,
* string $message,
* int $qualityOfService,
* bool $retained
* ) use ($logger) {
* $logger->info("Received message on topic [{$topic}]: {$message}");
* });
* ```
*
* Multiple event handlers can be registered at the same time.
*/
public function registerMessageReceivedEventHandler(\Closure $callback): MqttClient;
/**
* Unregisters a message received event handler which prevents it from being called in the future.
*
* This does not affect other registered event handlers. It is possible
* to unregister all registered event handlers by passing null as callback.
*/
public function unregisterMessageReceivedEventHandler(?\Closure $callback = null): MqttClient;
}

View File

@@ -0,0 +1,143 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Contracts;
use DateTime;
use PhpMqtt\Client\Exceptions\PendingMessageAlreadyExistsException;
use PhpMqtt\Client\Exceptions\PendingMessageNotFoundException;
use PhpMqtt\Client\Exceptions\RepositoryException;
use PhpMqtt\Client\PendingMessage;
use PhpMqtt\Client\Subscription;
/**
* Implementations of this interface provide storage capabilities to an MQTT client.
*
* Services of this type have three primary goals:
* 1. Providing and keeping track of message identifiers, since they must be unique
* within the message flow (i.e. there may not be duplicates of different messages
* at the same time).
* 2. Storing and keeping track of subscriptions, which is especially necessary in case
* of persisted sessions.
* 3. Storing and keeping track of pending messages (i.e. sent messages, which have not
* been acknowledged yet by the broker).
*
* @package PhpMqtt\Client\Contracts
*/
interface Repository
{
/**
* Re-initializes the repository by deleting all persisted data and restoring the original state,
* which was given when the repository was first created. This is used when a clean session
* is requested by a client during connection.
*/
public function reset(): void;
/**
* Returns a new message id. The message id might have been used before,
* but it is currently not being used (i.e. in a resend queue).
*
* @throws RepositoryException
*/
public function newMessageId(): int;
/**
* Returns the number of pending outgoing messages.
*/
public function countPendingOutgoingMessages(): int;
/**
* Gets a pending outgoing message with the given message identifier, if found.
*/
public function getPendingOutgoingMessage(int $messageId): ?PendingMessage;
/**
* Gets a list of pending outgoing messages last sent before the given date time.
*
* If date time is `null`, all pending messages are returned.
*
* The messages are returned in the same order they were added to the repository.
*
* @return PendingMessage[]
*/
public function getPendingOutgoingMessagesLastSentBefore(?DateTime $dateTime = null): array;
/**
* Adds a pending outgoing message to the repository.
*
* @throws PendingMessageAlreadyExistsException
*/
public function addPendingOutgoingMessage(PendingMessage $message): void;
/**
* Marks an existing pending outgoing published message as received in the repository.
*
* If the message does not exists, an exception is thrown,
* otherwise `true` is returned if the message was marked as received, and `false`
* in case it was already marked as received.
*
* @throws PendingMessageNotFoundException
*/
public function markPendingOutgoingPublishedMessageAsReceived(int $messageId): bool;
/**
* Removes a pending outgoing message from the repository.
*
* If a pending message with the given identifier is found and
* successfully removed from the repository, `true` is returned.
* Otherwise `false` will be returned.
*/
public function removePendingOutgoingMessage(int $messageId): bool;
/**
* Returns the number of pending incoming messages.
*/
public function countPendingIncomingMessages(): int;
/**
* Gets a pending incoming message with the given message identifier, if found.
*/
public function getPendingIncomingMessage(int $messageId): ?PendingMessage;
/**
* Adds a pending outgoing message to the repository.
*
* @throws PendingMessageAlreadyExistsException
*/
public function addPendingIncomingMessage(PendingMessage $message): void;
/**
* Removes a pending incoming message from the repository.
*
* If a pending message with the given identifier is found and
* successfully removed from the repository, `true` is returned.
* Otherwise `false` will be returned.
*/
public function removePendingIncomingMessage(int $messageId): bool;
/**
* Returns the number of registered subscriptions.
*/
public function countSubscriptions(): int;
/**
* Adds a subscription to the repository.
*/
public function addSubscription(Subscription $subscription): void;
/**
* Gets all subscriptions matching the given topic.
*
* @return Subscription[]
*/
public function getSubscriptionsMatchingTopic(string $topicName): array;
/**
* Removes the subscription with the given topic filter from the repository.
*
* Returns `true` if a topic subscription existed and has been removed.
* Otherwise, `false` is returned.
*/
public function removeSubscription(string $topicFilter): bool;
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Exceptions;
/**
* Exception to be thrown if an MQTT client is not connected to a broker and tries
* to perform an action which requires a connection (e.g. publish or subscribe).
*
* @package PhpMqtt\Client\Exceptions
*/
class ClientNotConnectedToBrokerException extends DataTransferException
{
public const EXCEPTION_CONNECTION_LOST = 0300;
/**
* ClientNotConnectedToBrokerException constructor.
*/
public function __construct(string $error)
{
parent::__construct(self::EXCEPTION_CONNECTION_LOST, $error);
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Exceptions;
/**
* Exception to be thrown if an MQTT client has been misconfigured or wrong connection
* settings are being used.
*
* @package PhpMqtt\Client\Exceptions
*/
class ConfigurationInvalidException extends MqttClientException
{
}

View File

@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Exceptions;
/**
* Exception to be thrown if an MQTT client could not connect to the broker.
*
* @package PhpMqtt\Client\Exceptions
*/
class ConnectingToBrokerFailedException extends MqttClientException
{
public const EXCEPTION_CONNECTION_FAILED = 0001;
public const EXCEPTION_CONNECTION_PROTOCOL_VERSION = 0002;
public const EXCEPTION_CONNECTION_IDENTIFIER_REJECTED = 0003;
public const EXCEPTION_CONNECTION_BROKER_UNAVAILABLE = 0004;
public const EXCEPTION_CONNECTION_INVALID_CREDENTIALS = 0005;
public const EXCEPTION_CONNECTION_UNAUTHORIZED = 0006;
public const EXCEPTION_CONNECTION_SOCKET_ERROR = 1000;
public const EXCEPTION_CONNECTION_TLS_ERROR = 2000;
/**
* ConnectingToBrokerFailedException constructor.
*/
public function __construct(
int $code,
string $error,
private ?string $connectionErrorCode = null,
private ?string $connectionErrorMessage = null,
)
{
parent::__construct(
sprintf('[%s] Establishing a connection to the MQTT broker failed: %s', $code, $error),
$code
);
}
/**
* Retrieves the connection error code.
*/
public function getConnectionErrorCode(): ?string
{
return $this->connectionErrorCode;
}
/**
* Retrieves the connection error message.
*/
public function getConnectionErrorMessage(): ?string
{
return $this->connectionErrorMessage;
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Exceptions;
/**
* Exception to be thrown if an MQTT client encountered an error while transferring data.
*
* @package PhpMqtt\Client\Exceptions
*/
class DataTransferException extends MqttClientException
{
public const EXCEPTION_TX_DATA = 0101;
public const EXCEPTION_RX_DATA = 0102;
/**
* DataTransferException constructor.
*/
public function __construct(int $code, string $error)
{
parent::__construct(
sprintf('[%s] Transferring data over socket failed: %s', $code, $error),
$code
);
}
}

View File

@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Exceptions;
/**
* Exception to be thrown if an MQTT client encounters an invalid message.
*
* @package PhpMqtt\Client\Exceptions
*/
class InvalidMessageException extends MqttClientException
{
}

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Exceptions;
/**
* Exception to be thrown if an MQTT client error occurs.
*
* @package PhpMqtt\Client\Exceptions
*/
class MqttClientException extends \Exception
{
/**
* MqttClientException constructor.
*/
public function __construct(string $message = '', int $code = 0, ?\Throwable $parentException = null)
{
if (empty($message)) {
parent::__construct(
sprintf('[%s] The MQTT client encountered an error.', $code),
$code,
$parentException
);
} else {
parent::__construct($message, $code, $parentException);
}
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Exceptions;
/**
* Exception to be thrown if a pending message with the same packet identifier is still pending.
*
* @package PhpMqtt\Client\Exceptions
*/
class PendingMessageAlreadyExistsException extends RepositoryException
{
/**
* PendingMessageAlreadyExistsException constructor.
*/
public function __construct(int $messageId)
{
parent::__construct(sprintf('A pending message with the message identifier [%s] exists already.', $messageId));
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Exceptions;
/**
* Exception to be thrown if a pending message with the same packet identifier is not found.
*
* @package PhpMqtt\Client\Exceptions
*/
class PendingMessageNotFoundException extends RepositoryException
{
/**
* PendingMessageNotFoundException constructor.
*/
public function __construct(int $messageId)
{
parent::__construct(sprintf('No pending message with the message identifier [%s].', $messageId));
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Exceptions;
/**
* Exception to be thrown if an invalid MQTT version is given.
*
* @package PhpMqtt\Client\Exceptions
*/
class ProtocolNotSupportedException extends MqttClientException
{
/**
* ProtocolNotSupportedException constructor.
*/
public function __construct(string $protocol)
{
parent::__construct(sprintf('The given protocol version [%s] is not supported.', $protocol));
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Exceptions;
/**
* Exception to be thrown if an MQTT client encountered a protocol violation.
*
* @package PhpMqtt\Client\Exceptions
*/
class ProtocolViolationException extends MqttClientException
{
/**
* ProtocolViolationException constructor.
*/
public function __construct(string $error)
{
parent::__construct(sprintf('Protocol violation: %s.', $error));
}
}

View File

@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Exceptions;
/**
* Exception to be thrown if an MQTT client repository encounters an error.
*
* @package PhpMqtt\Client\Exceptions
*/
class RepositoryException extends MqttClientException
{
}

166
vendor/php-mqtt/client/src/Logger.php vendored Normal file
View File

@@ -0,0 +1,166 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
/**
* Wrapper for another logger. Drops logged messages if no logger is available.
*
* @internal This class is not part of the public API of the library and used internally only.
* @package PhpMqtt\Client
*/
class Logger implements LoggerInterface
{
/**
* Logger constructor.
*
* @param LoggerInterface|null $logger
*/
public function __construct(
private string $host,
private int $port,
private string $clientId,
private ?LoggerInterface $logger = null,
)
{
}
/**
* System is unusable.
*
* @param string $message
* @param array $context
*/
public function emergency($message, array $context = []): void
{
$this->log(LogLevel::EMERGENCY, $message, $context);
}
/**
* Action must be taken immediately.
*
* Example: Entire website down, database unavailable, etc. This should
* trigger the SMS alerts and wake you up.
*
* @param string $message
* @param array $context
*/
public function alert($message, array $context = []): void
{
$this->log(LogLevel::ALERT, $message, $context);
}
/**
* Critical conditions.
*
* Example: Application component unavailable, unexpected exception.
*
* @param string $message
* @param array $context
*/
public function critical($message, array $context = []): void
{
$this->log(LogLevel::CRITICAL, $message, $context);
}
/**
* Runtime errors that do not require immediate action but should typically
* be logged and monitored.
*
* @param string $message
* @param array $context
*/
public function error($message, array $context = []): void
{
$this->log(LogLevel::ERROR, $message, $context);
}
/**
* Exceptional occurrences that are not errors.
*
* Example: Use of deprecated APIs, poor use of an API, undesirable things
* that are not necessarily wrong.
*
* @param string $message
* @param array $context
*/
public function warning($message, array $context = []): void
{
$this->log(LogLevel::WARNING, $message, $context);
}
/**
* Normal but significant events.
*
* @param string $message
* @param array $context
*/
public function notice($message, array $context = []): void
{
$this->log(LogLevel::NOTICE, $message, $context);
}
/**
* Interesting events.
*
* Example: User logs in, SQL logs.
*
* @param string $message
* @param array $context
*/
public function info($message, array $context = []): void
{
$this->log(LogLevel::INFO, $message, $context);
}
/**
* Detailed debug information.
*
* @param string $message
* @param array $context
*/
public function debug($message, array $context = []): void
{
$this->log(LogLevel::DEBUG, $message, $context);
}
/**
* Logs with an arbitrary level.
*
* @param mixed $level
* @param string $message
* @param array $context
*/
public function log($level, $message, array $context = []): void
{
if ($this->logger === null) {
return;
}
$this->logger->log($level, $this->wrapLogMessage($message), $this->mergeContext($context));
}
/**
* Wraps the given log message by prepending the client id and broker.
*/
protected function wrapLogMessage(string $message): string
{
return 'MQTT [{host}:{port}] [{clientId}] ' . $message;
}
/**
* Adds global context like host, port and client id to the log context.
*/
protected function mergeContext(array $context): array
{
return array_merge([
'host' => $this->host,
'port' => $this->port,
'clientId' => $this->clientId,
], $context);
}
}

105
vendor/php-mqtt/client/src/Message.php vendored Normal file
View File

@@ -0,0 +1,105 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client;
use PhpMqtt\Client\Contracts\MessageProcessor;
use PhpMqtt\Client\Contracts\MqttClient;
/**
* Describes an action which is supposed to be performed after receiving a message.
* Objects of this type are used by the {@see MessageProcessor} to instruct the
* {@see MqttClient} about required steps to take.
*
* @package PhpMqtt\Client
*/
class Message
{
private ?int $messageId = null;
private ?string $topic = null;
private ?string $content = null;
/** @var int[] */
private array $acknowledgedQualityOfServices = [];
/**
* Message constructor.
*/
public function __construct(
private MessageType $type,
private int $qualityOfService = 0,
private bool $retained = false,
)
{
}
public function getType(): MessageType
{
return $this->type;
}
public function getQualityOfService(): int
{
return $this->qualityOfService;
}
public function getRetained(): bool
{
return $this->retained;
}
public function getMessageId(): ?int
{
return $this->messageId;
}
public function setMessageId(?int $messageId): Message
{
$this->messageId = $messageId;
return $this;
}
public function getTopic(): ?string
{
return $this->topic;
}
public function setTopic(?string $topic): Message
{
$this->topic = $topic;
return $this;
}
public function getContent(): ?string
{
return $this->content;
}
public function setContent(?string $content): Message
{
$this->content = $content;
return $this;
}
/**
* @return int[]
*/
public function getAcknowledgedQualityOfServices(): array
{
return $this->acknowledgedQualityOfServices;
}
/**
* @param int[] $acknowledgedQualityOfServices
*/
public function setAcknowledgedQualityOfServices(array $acknowledgedQualityOfServices): Message
{
$this->acknowledgedQualityOfServices = $acknowledgedQualityOfServices;
return $this;
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\MessageProcessors;
use PhpMqtt\Client\Concerns\TranscodesData;
use PhpMqtt\Client\Concerns\WorksWithBuffers;
use Psr\Log\LoggerInterface;
/**
* This message processor serves as base for other message processors, providing
* default implementations for some methods.
*
* @package PhpMqtt\Client\MessageProcessors
*/
abstract class BaseMessageProcessor
{
use TranscodesData;
use WorksWithBuffers;
public const QOS_AT_MOST_ONCE = 0;
public const QOS_AT_LEAST_ONCE = 1;
public const QOS_EXACTLY_ONCE = 2;
/**
* BaseMessageProcessor constructor.
*/
public function __construct(protected LoggerInterface $logger)
{
}
}

View File

@@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\MessageProcessors;
use PhpMqtt\Client\Exceptions\InvalidMessageException;
use PhpMqtt\Client\Exceptions\ProtocolViolationException;
use PhpMqtt\Client\Message;
use PhpMqtt\Client\MessageType;
/**
* This message processor implements the MQTT protocol version 3.1.1.
*
* @package PhpMqtt\Client\MessageProcessors
*/
class Mqtt311MessageProcessor extends Mqtt31MessageProcessor
{
/**
* {@inheritDoc}
*/
protected function getEncodedProtocolNameAndVersion(): string
{
return $this->buildLengthPrefixedString('MQTT') . chr(0x04); // protocol version (4)
}
/**
* {@inheritDoc}
*/
public function parseAndValidateMessage(string $message): ?Message
{
$result = parent::parseAndValidateMessage($message);
if ($this->isPublishMessageWithNullCharacter($result)) {
throw new ProtocolViolationException('The broker sent us a message with the forbidden unicode character U+0000.');
}
return $result;
}
/**
* {@inheritDoc}
*/
protected function parseAndValidateSubscribeAcknowledgementMessage(string $data): Message
{
if (strlen($data) < 3) {
$this->logger->notice('Received invalid subscribe acknowledgement from the broker.');
throw new InvalidMessageException('Received invalid subscribe acknowledgement from the broker.');
}
$messageId = $this->decodeMessageId($this->pop($data, 2));
// Parse and validate the QoS acknowledgements.
$acknowledgements = array_map('ord', str_split($data));
foreach ($acknowledgements as $acknowledgement) {
if (!in_array($acknowledgement, [0, 1, 2, 128])) {
throw new InvalidMessageException('Received subscribe acknowledgement with invalid QoS values from the broker.');
}
}
return (new Message(MessageType::SUBSCRIBE_ACKNOWLEDGEMENT()))
->setMessageId($messageId)
->setAcknowledgedQualityOfServices($acknowledgements);
}
/**
* Determines if the given message is a PUBLISH message and contains the unicode null character U+0000.
*/
private function isPublishMessageWithNullCharacter(?Message $message): bool
{
return $message !== null
&& $message->getType()->equals(MessageType::PUBLISH())
&& $message->getContent() !== null
&& preg_match('/\x{0000}/u', $message->getContent());
}
}

View File

@@ -0,0 +1,712 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\MessageProcessors;
use PhpMqtt\Client\ConnectionSettings;
use PhpMqtt\Client\Contracts\MessageProcessor;
use PhpMqtt\Client\Exceptions\ConnectingToBrokerFailedException;
use PhpMqtt\Client\Exceptions\InvalidMessageException;
use PhpMqtt\Client\Exceptions\ProtocolViolationException;
use PhpMqtt\Client\Message;
use PhpMqtt\Client\MessageType;
use Psr\Log\LoggerInterface;
/**
* This message processor implements the MQTT protocol version 3.1.
*
* @package PhpMqtt\Client\MessageProcessors
*/
class Mqtt31MessageProcessor extends BaseMessageProcessor implements MessageProcessor
{
/**
* Creates a new message processor instance which supports version 3.1 of the MQTT protocol.
*/
public function __construct(private string $clientId, LoggerInterface $logger)
{
parent::__construct($logger);
}
/**
* {@inheritDoc}
*/
public function tryFindMessageInBuffer(string $buffer, int $bufferLength, ?string &$message = null, int &$requiredBytes = -1): bool
{
// If we received no input, we can return immediately without doing work.
if ($bufferLength === 0) {
return false;
}
// If we received not at least the fixed header with one length indicating byte,
// we know that there can't be a valid message in the buffer. So we return early.
if ($bufferLength < 2) {
return false;
}
// Read the second byte of the message to get the remaining length.
// If the continuation bit (8) is set on the length byte, another byte will be read as length.
$byteIndex = 1;
$remainingLength = 0;
$multiplier = 1;
do {
// If the buffer has no more data, but we need to read more for the length header,
// we cannot give useful information about the remaining length and exit early.
if ($byteIndex + 1 > $bufferLength) {
return false;
}
// There can me a maximum of four bytes for the package length, which means we cann opt-out
// when reaching the 6th byte in the buffer. This is only a safety measure in case the broker
// is sending invalid messages. Normally, the loop exits on its own.
if ($byteIndex >= 6) {
break;
}
// Otherwise, we can take seven bits to calculate the length and the remaining eighth bit
// as continuation bit.
$digit = ord($buffer[$byteIndex]);
$remainingLength += ($digit & 127) * $multiplier;
$multiplier *= 128;
$byteIndex++;
} while (($digit & 128) !== 0);
// At this point, we can now tell whether the remaining length amount of bytes are available
// or not. If not, we return the amount of bytes required for the message to be complete.
$requiredBufferLength = $byteIndex + $remainingLength;
if ($requiredBufferLength > $bufferLength) {
$requiredBytes = $requiredBufferLength;
return false;
}
// Now that we have a full message in the buffer, we can set the output and return.
$message = substr($buffer, 0, $requiredBufferLength);
return true;
}
/**
* {@inheritDoc}
*/
public function buildConnectMessage(ConnectionSettings $connectionSettings, bool $useCleanSession = false): string
{
// The protocol name and version.
$buffer = $this->getEncodedProtocolNameAndVersion();
// Build connection flags based on the connection settings.
$buffer .= chr($this->buildConnectionFlags($connectionSettings, $useCleanSession));
// Encode and add the keep alive interval.
$buffer .= chr($connectionSettings->getKeepAliveInterval() >> 8);
$buffer .= chr($connectionSettings->getKeepAliveInterval() & 0xff);
// Encode and add the client identifier.
$buffer .= $this->buildLengthPrefixedString($this->clientId);
// Encode and add the last will topic and message, if configured.
if ($connectionSettings->hasLastWill()) {
$buffer .= $this->buildLengthPrefixedString($connectionSettings->getLastWillTopic());
$buffer .= $this->buildLengthPrefixedString($connectionSettings->getLastWillMessage());
}
// Encode and add the credentials, if configured.
if ($connectionSettings->getUsername() !== null) {
$buffer .= $this->buildLengthPrefixedString($connectionSettings->getUsername());
}
if ($connectionSettings->getPassword() !== null) {
$buffer .= $this->buildLengthPrefixedString($connectionSettings->getPassword());
}
// The header consists of the message type 0x10 and the length.
$header = chr(0x10) . $this->encodeMessageLength(strlen($buffer));
return $header . $buffer;
}
/**
* Returns the encoded protocol name and version, ready to be sent as part of the CONNECT message.
*/
protected function getEncodedProtocolNameAndVersion(): string
{
return $this->buildLengthPrefixedString('MQIsdp') . chr(0x03); // protocol version (3)
}
/**
* Builds the connection flags from the inputs and settings.
*
* The bit structure of the connection flags is as follows:
* 0 - reserved
* 1 - clean session flag
* 2 - last will flag
* 3 - QoS flag (1)
* 4 - QoS flag (2)
* 5 - retain last will flag
* 6 - password flag
* 7 - username flag
*
* @link http://public.dhe.ibm.com/software/dw/webservices/ws-mqtt/mqtt-v3r1.html#connect MQTT 3.1 Spec
*/
protected function buildConnectionFlags(ConnectionSettings $connectionSettings, bool $useCleanSession = false): int
{
$flags = 0;
if ($useCleanSession) {
$this->logger->debug('Using the [clean session] flag for the connection.');
$flags += 1 << 1;
}
if ($connectionSettings->hasLastWill()) {
$this->logger->debug('Using the [will] flag for the connection.');
$flags += 1 << 2;
if ($connectionSettings->getLastWillQualityOfService() > self::QOS_AT_MOST_ONCE) {
$this->logger->debug('Using last will QoS level [{qos}] for the connection.', [
'qos' => $connectionSettings->getLastWillQualityOfService(),
]);
$flags += $connectionSettings->getLastWillQualityOfService() << 3;
}
if ($connectionSettings->shouldRetainLastWill()) {
$this->logger->debug('Using the [retain last will] flag for the connection.');
$flags += 1 << 5;
}
}
if ($connectionSettings->getPassword() !== null) {
$this->logger->debug('Using the [password] flag for the connection.');
$flags += 1 << 6;
}
if ($connectionSettings->getUsername() !== null) {
$this->logger->debug('Using the [username] flag for the connection.');
$flags += 1 << 7;
}
return $flags;
}
/**
* {@inheritDoc}
*/
public function handleConnectAcknowledgement(string $message): void
{
if (strlen($message) !== 4 || ($messageType = ord($message[0]) >> 4) !== 2) {
$this->logger->error('Expected connect acknowledgement; received a different response.', ['messageType' => $messageType ?? null]);
throw new ConnectingToBrokerFailedException(
ConnectingToBrokerFailedException::EXCEPTION_CONNECTION_FAILED,
'A connection could not be established. Expected connect acknowledgement; received a different response else.'
);
}
$errorCode = ord($message[3]);
$logContext = ['errorCode' => sprintf('0x%02X', $errorCode)];
switch ($errorCode) {
case 0x00:
$this->logger->info('Connection with broker established successfully.', $logContext);
break;
case 0x01:
$this->logger->error('The broker does not support MQTT v3.1.', $logContext);
throw new ConnectingToBrokerFailedException(
ConnectingToBrokerFailedException::EXCEPTION_CONNECTION_PROTOCOL_VERSION,
'The configured broker does not support MQTT v3.1.'
);
case 0x02:
$this->logger->error('The broker rejected the sent identifier.', $logContext);
throw new ConnectingToBrokerFailedException(
ConnectingToBrokerFailedException::EXCEPTION_CONNECTION_IDENTIFIER_REJECTED,
'The configured broker rejected the sent identifier.'
);
case 0x03:
$this->logger->error('The broker is currently unavailable.', $logContext);
throw new ConnectingToBrokerFailedException(
ConnectingToBrokerFailedException::EXCEPTION_CONNECTION_BROKER_UNAVAILABLE,
'The configured broker is currently unavailable.'
);
case 0x04:
$this->logger->error('The broker reported the credentials as invalid.', $logContext);
throw new ConnectingToBrokerFailedException(
ConnectingToBrokerFailedException::EXCEPTION_CONNECTION_INVALID_CREDENTIALS,
'The configured broker reported the credentials as invalid.'
);
case 0x05:
$this->logger->error('The broker responded with unauthorized.', $logContext);
throw new ConnectingToBrokerFailedException(
ConnectingToBrokerFailedException::EXCEPTION_CONNECTION_UNAUTHORIZED,
'The configured broker responded with unauthorized.'
);
default:
$this->logger->error('The broker responded with an invalid error code [{errorCode}].', $logContext);
throw new ConnectingToBrokerFailedException(
ConnectingToBrokerFailedException::EXCEPTION_CONNECTION_FAILED,
'The configured broker responded with an invalid error code. A connection could not be established.'
);
}
}
/**
* Builds a ping request message.
*/
public function buildPingRequestMessage(): string
{
// The message consists of the command 0xc0 and the length 0.
return chr(0xc0) . chr(0x00);
}
/**
* Builds a ping response message.
*/
public function buildPingResponseMessage(): string
{
// The message consists of the command 0xd0 and the length 0.
return chr(0xd0) . chr(0x00);
}
/**
* Builds a disconnect message.
*/
public function buildDisconnectMessage(): string
{
// The message consists of the command 0xe0 and the length 0.
return chr(0xe0) . chr(0x00);
}
/**
* {@inheritDoc}
*/
public function buildSubscribeMessage(int $messageId, array $subscriptions, bool $isDuplicate = false): string
{
// Encode the message id, it always consists of two bytes.
$buffer = $this->encodeMessageId($messageId);
foreach ($subscriptions as $subscription) {
// Encode the topic as length prefixed string.
$buffer .= $this->buildLengthPrefixedString($subscription->getTopicFilter());
// Encode the quality of service level.
$buffer .= chr($subscription->getQualityOfServiceLevel());
}
// The header consists of the message type 0x82 and the length.
$header = chr(0x82) . $this->encodeMessageLength(strlen($buffer));
return $header . $buffer;
}
/**
* {@inheritDoc}
*/
public function buildUnsubscribeMessage(int $messageId, array $topics, bool $isDuplicate = false): string
{
// Encode the message id, it always consists of two bytes.
$buffer = $this->encodeMessageId($messageId);
foreach ($topics as $topic) {
// Encode the topic as length prefixed string.
$buffer .= $this->buildLengthPrefixedString($topic);
}
// The header consists of the message type 0xa2 and the length.
// Additionally, the first byte may contain the duplicate flag.
$command = 0xa2 | ($isDuplicate ? 1 << 3 : 0);
$header = chr($command) . $this->encodeMessageLength(strlen($buffer));
return $header . $buffer;
}
/**
* {@inheritDoc}
*/
public function buildPublishMessage(
string $topic,
string $message,
int $qualityOfService,
bool $retain,
?int $messageId = null,
bool $isDuplicate = false,
): string
{
// Encode the topic as length prefixed string.
$buffer = $this->buildLengthPrefixedString($topic);
// Encode the message id, if given. It always consists of two bytes.
if ($messageId !== null)
{
$buffer .= $this->encodeMessageId($messageId);
}
// Add the message without encoding.
$buffer .= $message;
// Encode the command with supported flags.
$command = 0x30;
if ($retain) {
$command += 1 << 0;
}
if ($qualityOfService > self::QOS_AT_MOST_ONCE) {
$command += $qualityOfService << 1;
}
if ($qualityOfService > self::QOS_AT_MOST_ONCE && $isDuplicate) {
$command += 1 << 3;
}
// Build the header from the command and the encoded message length.
$header = chr($command) . $this->encodeMessageLength(strlen($buffer));
return $header . $buffer;
}
/**
* {@inheritDoc}
*/
public function buildPublishAcknowledgementMessage(int $messageId): string
{
return chr(0x40) . chr(0x02) . $this->encodeMessageId($messageId);
}
/**
* {@inheritDoc}
*/
public function buildPublishReceivedMessage(int $messageId): string
{
return chr(0x50) . chr(0x02) . $this->encodeMessageId($messageId);
}
/**
* {@inheritDoc}
*/
public function buildPublishReleaseMessage(int $messageId): string
{
return chr(0x62) . chr(0x02) . $this->encodeMessageId($messageId);
}
/**
* {@inheritDoc}
*/
public function buildPublishCompleteMessage(int $messageId): string
{
return chr(0x70) . chr(0x02) . $this->encodeMessageId($messageId);
}
/**
* {@inheritDoc}
*/
public function parseAndValidateMessage(string $message): ?Message
{
$qualityOfService = 0;
$retained = false;
$data = '';
$result = $this->tryDecodeMessage($message, $command, $qualityOfService, $retained, $data);
if ($result === false) {
throw new InvalidMessageException('The passed message could not be decoded.');
}
// Ensure the command is supported by this version of the protocol.
if ($command <= 0 || $command >= 15) {
$this->logger->error('Reserved command received from the broker. Supported are commands (including) 1-14.', [
'command' => $command,
]);
throw new InvalidMessageException('A reserved command has been used in the message.');
}
// Then handle the command accordingly.
switch ($command) {
case 0x02:
throw new ProtocolViolationException('Unexpected connection acknowledgement.');
case 0x03:
return $this->parseAndValidatePublishMessage($data, $qualityOfService, $retained);
case 0x04:
return $this->parseAndValidatePublishAcknowledgementMessage($data);
case 0x05:
return $this->parseAndValidatePublishReceiptMessage($data);
case 0x06:
return $this->parseAndValidatePublishReleaseMessage($data);
case 0x07:
return $this->parseAndValidatePublishCompleteMessage($data);
case 0x09:
return $this->parseAndValidateSubscribeAcknowledgementMessage($data);
case 0x0b:
return $this->parseAndValidateUnsubscribeAcknowledgementMessage($data);
case 0x0c:
return $this->parseAndValidatePingRequestMessage();
case 0x0d:
return $this->parseAndValidatePingAcknowledgementMessage();
default:
$this->logger->debug('Received message with unsupported command [{command}]. Skipping.', ['command' => $command]);
break;
}
// If we arrive here, we must have parsed a message with an unsupported type, and it cannot be
// very relevant for us. So we return an empty result without information to skip processing.
return null;
}
/**
* Attempt to decode the given message. If successful, the result is true and the reference
* parameters are set accordingly. Otherwise, false is returned and the reference parameters
* remain untouched.
*/
protected function tryDecodeMessage(
string $message,
?int &$command = null,
?int &$qualityOfService = null,
?bool &$retained = null,
?string &$data = null
): bool
{
// If we received no input, we can return immediately without doing work.
if (strlen($message) === 0) {
return false;
}
// If we received not at least the fixed header with one length indicating byte,
// we know that there can't be a valid message in the buffer. So we return early.
if (strlen($message) < 2) {
return false;
}
// Read the first byte of a message (command and flags).
$byte = $message[0];
$command = (int) (ord($byte) / 16);
$qualityOfService = (ord($byte) & 0x06) >> 1;
$retained = (bool) (ord($byte) & 0x01);
// Read the second byte of a message (remaining length).
// If the continuation bit (8) is set on the length byte, another byte will be read as length.
$byteIndex = 1;
$remainingLength = 0;
$multiplier = 1;
do {
// If the buffer has no more data, but we need to read more for the length header,
// we cannot give useful information about the remaining length and exit early.
if ($byteIndex + 1 > strlen($message)) {
return false;
}
// Otherwise, we can take seven bits to calculate the length and the remaining eighth bit
// as continuation bit.
$digit = ord($message[$byteIndex]);
$remainingLength += ($digit & 127) * $multiplier;
$multiplier *= 128;
$byteIndex++;
} while (($digit & 128) !== 0);
// At this point, we can now tell whether the remaining length amount of bytes are available
// or not. If not, the message is incomplete.
$requiredBytes = $byteIndex + $remainingLength;
if ($requiredBytes > strlen($message)) {
return false;
}
// Set the output data based on the calculated bytes.
$data = substr($message, $byteIndex, $remainingLength);
return true;
}
/**
* Parses a received published message. The data contains the whole message except the
* fixed header with command and length. The message structure is:
*
* [topic-length:topic:message]+
*/
protected function parseAndValidatePublishMessage(string $data, int $qualityOfServiceLevel, bool $retained): ?Message
{
$topicLength = (ord($data[0]) << 8) + ord($data[1]);
$topic = substr($data, 2, $topicLength);
$content = substr($data, ($topicLength + 2));
$message = new Message(MessageType::PUBLISH(), $qualityOfServiceLevel, $retained);
if ($qualityOfServiceLevel > self::QOS_AT_MOST_ONCE) {
if (strlen($content) < 2) {
$this->logger->error('Received a message with QoS level [{qos}] without message identifier. Waiting for retransmission.', [
'qos' => $qualityOfServiceLevel,
]);
// This message seems to be incomplete or damaged. We ignore it and wait for a retransmission,
// which will occur at some point due to QoS level > 0.
return null;
}
// Publish messages with a quality of service level > 0 require acknowledgement and therefore
// also a message identifier.
$messageId = $this->decodeMessageId($this->pop($content, 2));
$message->setMessageId($messageId);
}
return $message
->setTopic($topic)
->setContent($content);
}
/**
* Parses a received publish acknowledgement. The data contains the whole message except
* the fixed header with command and length. The message structure is:
*
* [message-identifier]
*
* @throws InvalidMessageException
*/
protected function parseAndValidatePublishAcknowledgementMessage(string $data): Message
{
if (strlen($data) !== 2) {
$this->logger->notice('Received invalid publish acknowledgement from the broker.');
throw new InvalidMessageException('Received invalid publish acknowledgement from the broker.');
}
$messageId = $this->decodeMessageId($this->pop($data, 2));
return (new Message(MessageType::PUBLISH_ACKNOWLEDGEMENT()))
->setMessageId($messageId);
}
/**
* Parses a received publish receipt. The data contains the whole message except the
* fixed header with command and length. The message structure is:
*
* [message-identifier]
*
* @throws InvalidMessageException
*/
protected function parseAndValidatePublishReceiptMessage(string $data): Message
{
if (strlen($data) !== 2) {
$this->logger->notice('Received invalid publish receipt from the broker.');
throw new InvalidMessageException('Received invalid publish receipt from the broker.');
}
$messageId = $this->decodeMessageId($this->pop($data, 2));
return (new Message(MessageType::PUBLISH_RECEIPT()))
->setMessageId($messageId);
}
/**
* Parses a received publish release message. The data contains the whole message except the
* fixed header with command and length. The message structure is:
*
* [message-identifier]
*
* @throws InvalidMessageException
*/
protected function parseAndValidatePublishReleaseMessage(string $data): Message
{
if (strlen($data) !== 2) {
$this->logger->notice('Received invalid publish release from the broker.');
throw new InvalidMessageException('Received invalid publish release from the broker.');
}
$messageId = $this->decodeMessageId($this->pop($data, 2));
return (new Message(MessageType::PUBLISH_RELEASE()))
->setMessageId($messageId);
}
/**
* Parses a received publish confirmation message. The data contains the whole message except the
* fixed header with command and length. The message structure is:
*
* [message-identifier]
*
* @throws InvalidMessageException
*/
protected function parseAndValidatePublishCompleteMessage(string $data): Message
{
if (strlen($data) !== 2) {
$this->logger->notice('Received invalid publish complete from the broker.');
throw new InvalidMessageException('Received invalid complete release from the broker.');
}
$messageId = $this->decodeMessageId($this->pop($data, 2));
return (new Message(MessageType::PUBLISH_COMPLETE()))
->setMessageId($messageId);
}
/**
* Parses a received subscription acknowledgement. The data contains the whole message except the
* fixed header with command and length. The message structure is:
*
* [message-identifier:[qos-level]+]
*
* The order of the received QoS levels matches the order of the sent subscriptions.
*
* @throws InvalidMessageException
*/
protected function parseAndValidateSubscribeAcknowledgementMessage(string $data): Message
{
if (strlen($data) < 3) {
$this->logger->notice('Received invalid subscribe acknowledgement from the broker.');
throw new InvalidMessageException('Received invalid subscribe acknowledgement from the broker.');
}
$messageId = $this->decodeMessageId($this->pop($data, 2));
// Parse and validate the QoS acknowledgements.
$acknowledgements = array_map('ord', str_split($data));
foreach ($acknowledgements as $acknowledgement) {
if (!in_array($acknowledgement, [0, 1, 2])) {
throw new InvalidMessageException('Received subscribe acknowledgement with invalid QoS values from the broker.');
}
}
return (new Message(MessageType::SUBSCRIBE_ACKNOWLEDGEMENT()))
->setMessageId($messageId)
->setAcknowledgedQualityOfServices($acknowledgements);
}
/**
* Parses a received unsubscribe acknowledgement. The data contains the whole message except the
* fixed header with command and length. The message structure is:
*
* [message-identifier]
*
* @throws InvalidMessageException
*/
protected function parseAndValidateUnsubscribeAcknowledgementMessage(string $data): Message
{
if (strlen($data) !== 2) {
$this->logger->notice('Received invalid unsubscribe acknowledgement from the broker.');
throw new InvalidMessageException('Received invalid unsubscribe acknowledgement from the broker.');
}
$messageId = $this->decodeMessageId($this->pop($data, 2));
return (new Message(MessageType::UNSUBSCRIBE_ACKNOWLEDGEMENT()))
->setMessageId($messageId);
}
/**
* Parses a received ping request.
*/
protected function parseAndValidatePingRequestMessage(): Message
{
return new Message(MessageType::PING_REQUEST());
}
/**
* Parses a received ping acknowledgement.
*/
protected function parseAndValidatePingAcknowledgementMessage(): Message
{
return new Message(MessageType::PING_RESPONSE());
}
}

View File

@@ -0,0 +1,37 @@
<?php
/** @noinspection PhpUnusedPrivateFieldInspection */
declare(strict_types=1);
namespace PhpMqtt\Client;
use MyCLabs\Enum\Enum;
/**
* An enumeration describing types of messages.
*
* @method static MessageType PUBLISH()
* @method static MessageType PUBLISH_ACKNOWLEDGEMENT()
* @method static MessageType PUBLISH_RECEIPT()
* @method static MessageType PUBLISH_RELEASE()
* @method static MessageType PUBLISH_COMPLETE()
* @method static MessageType SUBSCRIBE_ACKNOWLEDGEMENT()
* @method static MessageType UNSUBSCRIBE_ACKNOWLEDGEMENT()
* @method static MessageType PING_REQUEST()
* @method static MessageType PING_RESPONSE()
*
* @package PhpMqtt\Client
*/
class MessageType extends Enum
{
private const PUBLISH = 'PUBLISH';
private const PUBLISH_ACKNOWLEDGEMENT = 'PUBACK';
private const PUBLISH_RECEIPT = 'PUBREC';
private const PUBLISH_RELEASE = 'PUBREL';
private const PUBLISH_COMPLETE = 'PUBCOMP';
private const SUBSCRIBE_ACKNOWLEDGEMENT = 'SUBACK';
private const UNSUBSCRIBE_ACKNOWLEDGEMENT = 'UNSUBACK';
private const PING_REQUEST = 'PINGREQ';
private const PING_RESPONSE = 'PINGRESP';
}

1284
vendor/php-mqtt/client/src/MqttClient.php vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client;
use DateTime;
/**
* Represents a pending message.
*
* For messages with QoS 1 and 2 the client is responsible to resend the message if no
* acknowledgement is received from the broker within a given time period.
*
* This class serves as common base for message objects which need to be resent if no
* acknowledgement is received.
*
* @package PhpMqtt\Client
*/
abstract class PendingMessage
{
private int $sendingAttempts = 1;
private DateTime $lastSentAt;
/**
* Creates a new pending message object.
*/
protected function __construct(private int $messageId, ?DateTime $sentAt = null)
{
$this->lastSentAt = $sentAt ?? new DateTime();
}
/**
* Returns the message identifier.
*/
public function getMessageId(): int
{
return $this->messageId;
}
/**
* Returns the date time when the message was last sent.
*/
public function getLastSentAt(): DateTime
{
return $this->lastSentAt;
}
/**
* Returns the number of times the message has been sent.
*/
public function getSendingAttempts(): int
{
return $this->sendingAttempts;
}
/**
* Sets the date time when the message was last sent.
*/
public function setLastSentAt(?DateTime $value = null): self
{
$this->lastSentAt = $value ?? new DateTime();
return $this;
}
/**
* Increments the sending attempts by one.
*/
public function incrementSendingAttempts(): self
{
$this->sendingAttempts++;
return $this;
}
}

View File

@@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client;
/**
* A simple DTO for published messages which need to be stored in a repository
* while waiting for the confirmation to be deliverable.
*
* @package PhpMqtt\Client
*/
class PublishedMessage extends PendingMessage
{
private bool $received = false;
/**
* Creates a new published message object.
*/
public function __construct(
int $messageId,
private string $topicName,
private string $message,
private int $qualityOfService,
private bool $retain,
)
{
parent::__construct($messageId);
}
/**
* Returns the topic name of the published message.
*/
public function getTopicName(): string
{
return $this->topicName;
}
/**
* Returns the content of the published message.
*/
public function getMessage(): string
{
return $this->message;
}
/**
* Returns the requested quality of service level.
*/
public function getQualityOfServiceLevel(): int
{
return $this->qualityOfService;
}
/**
* Determines whether this message wants to be retained.
*/
public function wantsToBeRetained(): bool
{
return $this->retain;
}
/**
* Determines whether the message has been confirmed as received.
*/
public function hasBeenReceived(): bool
{
return $this->received;
}
/**
* Marks the published message as received (QoS level 2).
*
* Returns `true` if the message was not previously received. Otherwise `false` will be returned.
*/
public function markAsReceived(): bool
{
$result = !$this->received;
$this->received = true;
return $result;
}
}

View File

@@ -0,0 +1,232 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Repositories;
use PhpMqtt\Client\Contracts\Repository;
use PhpMqtt\Client\Exceptions\PendingMessageAlreadyExistsException;
use PhpMqtt\Client\Exceptions\PendingMessageNotFoundException;
use PhpMqtt\Client\Exceptions\RepositoryException;
use PhpMqtt\Client\PendingMessage;
use PhpMqtt\Client\PublishedMessage;
use PhpMqtt\Client\Subscription;
/**
* Provides an in-memory implementation which manages message ids, subscriptions and pending messages.
* Instances of this type do not persist any data and are only meant for simple uses cases.
*
* @package PhpMqtt\Client\Repositories
*/
class MemoryRepository implements Repository
{
private int $nextMessageId = 1;
/** @var array<int, PendingMessage> */
private array $pendingOutgoingMessages = [];
/** @var array<int, PendingMessage> */
private array $pendingIncomingMessages = [];
/** @var array<int, Subscription> */
private array $subscriptions = [];
/**
* {@inheritDoc}
*/
public function reset(): void
{
$this->nextMessageId = 1;
$this->pendingOutgoingMessages = [];
$this->pendingIncomingMessages = [];
$this->subscriptions = [];
}
/**
* {@inheritDoc}
*/
public function newMessageId(): int
{
if (count($this->pendingOutgoingMessages) >= 65535) {
// This should never happen, as the server receive queue is
// normally smaller than the actual total number of message ids.
// Also, when using MQTT 5.0 the server can specify a smaller
// receive queue size (mosquitto for example has 20 by default),
// so the client has to implement the logic to honor this
// restriction and fallback to the protocol limit.
throw new RepositoryException('No more message identifiers available. The queue is full.');
}
while (isset($this->pendingOutgoingMessages[$this->nextMessageId])) {
$this->nextMessageId++;
if ($this->nextMessageId > 65535) {
$this->nextMessageId = 1;
}
}
return $this->nextMessageId;
}
/**
* {@inheritDoc}
*/
public function countPendingOutgoingMessages(): int
{
return count($this->pendingOutgoingMessages);
}
/**
* {@inheritDoc}
*/
public function getPendingOutgoingMessage(int $messageId): ?PendingMessage
{
return $this->pendingOutgoingMessages[$messageId] ?? null;
}
/**
* {@inheritDoc}
*/
public function getPendingOutgoingMessagesLastSentBefore(?\DateTime $dateTime = null): array
{
$result = [];
foreach ($this->pendingOutgoingMessages as $pendingMessage) {
if ($pendingMessage->getLastSentAt() < $dateTime) {
$result[] = $pendingMessage;
}
}
return $result;
}
/**
* {@inheritDoc}
*/
public function addPendingOutgoingMessage(PendingMessage $message): void
{
if (isset($this->pendingOutgoingMessages[$message->getMessageId()])) {
throw new PendingMessageAlreadyExistsException($message->getMessageId());
}
$this->pendingOutgoingMessages[$message->getMessageId()] = $message;
}
/**
* {@inheritDoc}
*/
public function markPendingOutgoingPublishedMessageAsReceived(int $messageId): bool
{
if (!isset($this->pendingOutgoingMessages[$messageId]) ||
!$this->pendingOutgoingMessages[$messageId] instanceof PublishedMessage) {
throw new PendingMessageNotFoundException($messageId);
}
return $this->pendingOutgoingMessages[$messageId]->markAsReceived();
}
/**
* {@inheritDoc}
*/
public function removePendingOutgoingMessage(int $messageId): bool
{
if (!isset($this->pendingOutgoingMessages[$messageId])) {
return false;
}
unset($this->pendingOutgoingMessages[$messageId]);
return true;
}
/**
* {@inheritDoc}
*/
public function countPendingIncomingMessages(): int
{
return count($this->pendingIncomingMessages);
}
/**
* {@inheritDoc}
*/
public function getPendingIncomingMessage(int $messageId): ?PendingMessage
{
return $this->pendingIncomingMessages[$messageId] ?? null;
}
/**
* {@inheritDoc}
*/
public function addPendingIncomingMessage(PendingMessage $message): void
{
if (isset($this->pendingIncomingMessages[$message->getMessageId()])) {
throw new PendingMessageAlreadyExistsException($message->getMessageId());
}
$this->pendingIncomingMessages[$message->getMessageId()] = $message;
}
/**
* {@inheritDoc}
*/
public function removePendingIncomingMessage(int $messageId): bool
{
if (!isset($this->pendingIncomingMessages[$messageId])) {
return false;
}
unset($this->pendingIncomingMessages[$messageId]);
return true;
}
/**
* {@inheritDoc}
*/
public function countSubscriptions(): int
{
return count($this->subscriptions);
}
/**
* {@inheritDoc}
*/
public function addSubscription(Subscription $subscription): void
{
// Remove a potentially existing subscription for this topic filter.
$this->removeSubscription($subscription->getTopicFilter());
$this->subscriptions[] = $subscription;
}
/**
* {@inheritDoc}
*/
public function getSubscriptionsMatchingTopic(string $topicName): array
{
$result = [];
foreach ($this->subscriptions as $subscription) {
if (!$subscription->matchesTopic($topicName)) {
continue;
}
$result[] = $subscription;
}
return $result;
}
/**
* {@inheritDoc}
*/
public function removeSubscription(string $topicFilter): bool
{
foreach ($this->subscriptions as $index => $subscription) {
if ($subscription->getTopicFilter() === $topicFilter) {
unset($this->subscriptions[$index]);
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client;
/**
* Represents a pending subscribe request.
*
* @package PhpMqtt\Client
*/
class SubscribeRequest extends PendingMessage
{
/** @var Subscription[] */
private array $subscriptions;
/**
* Creates a new subscribe request message.
*
* @param Subscription[] $subscriptions
*/
public function __construct(int $messageId, array $subscriptions)
{
parent::__construct($messageId);
$this->subscriptions = array_values($subscriptions);
}
/**
* Returns the subscriptions in this request.
*
* @return Subscription[]
*/
public function getSubscriptions(): array
{
return $this->subscriptions;
}
}

View File

@@ -0,0 +1,106 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client;
/**
* A simple DTO for subscriptions to a topic which need to be stored in a repository.
*
* @package PhpMqtt\Client
*/
class Subscription
{
private string $regexifiedTopicFilter;
/**
* Creates a new subscription object.
*/
public function __construct(
private string $topicFilter,
private int $qualityOfService = 0,
private ?\Closure $callback = null,
)
{
$this->regexifyTopicFilter();
}
/**
* Converts the topic filter into a regular expression.
*/
private function regexifyTopicFilter(): void
{
$topicFilter = $this->topicFilter;
// If the topic filter is for a shared subscription, we remove the shared subscription prefix as well as the group name
// from the topic filter. To do so, we look for the $share keyword and then try to find the second topic separator to
// calculate the substring containing the actual topic filter.
// Note: shared subscriptions always have the form: $share/<group>/<topic>
if (str_starts_with($topicFilter, '$share/') && ($separatorIndex = strpos($topicFilter, '/', 7)) !== false) {
$topicFilter = substr($topicFilter, $separatorIndex + 1);
}
$this->regexifiedTopicFilter = '/^' . str_replace(['$', '/', '+', '#'], ['\$', '\/', '([^\/]*)', '(.*)'], $topicFilter) . '$/';
}
/**
* Returns the topic of the subscription.
*/
public function getTopicFilter(): string
{
return $this->topicFilter;
}
/**
* Matches the given topic name matches to the subscription's topic filter.
*/
public function matchesTopic(string $topicName): bool
{
return (bool) preg_match($this->regexifiedTopicFilter, $topicName);
}
/**
* Returns an array which contains all matched wildcards of this subscription, taken from the given topic name.
*
* Example:
* Subscription topic filter: foo/+/bar/+/baz/#
* Result for 'foo/1/bar/2/baz': ['1', '2']
* Result for 'foo/my/bar/subscription/baz/42': ['my', 'subscription', '42']
* Result for 'foo/my/bar/subscription/baz/hello/world/123': ['my', 'subscription', 'hello', 'world', '123']
* Result for invalid topic 'some/topic': []
*
* Note: This method should only be called if {@see matchesTopic} returned true. An empty array will be returned otherwise.
*/
public function getMatchedWildcards(string $topicName): array
{
if (!preg_match($this->regexifiedTopicFilter, $topicName, $matches)) {
return [];
}
return array_slice($matches, 1);
}
/**
* Returns the callback for this subscription.
*/
public function getCallback(): ?\Closure
{
return $this->callback;
}
/**
* Returns the requested quality of service level.
*/
public function getQualityOfServiceLevel(): int
{
return $this->qualityOfService;
}
/**
* Sets the actual quality of service level.
*/
public function setQualityOfServiceLevel(int $qualityOfService): void
{
$this->qualityOfService = $qualityOfService;
}
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client;
/**
* Represents an unsubscribe request.
*
* @package PhpMqtt\Client
*/
class UnsubscribeRequest extends PendingMessage
{
/** @var string[] */
private array $topicFilters;
/**
* Creates a new unsubscribe request object.
*
* @param string[] $topicFilters
*/
public function __construct(int $messageId, array $topicFilters)
{
parent::__construct($messageId);
$this->topicFilters = array_values($topicFilters);
}
/**
* Returns the topic filters in this request.
*
* @return string[]
*/
public function getTopicFilters(): array
{
return $this->topicFilters;
}
}