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
@@ -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);
}
}
+301
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]);
}
}
}
}
+78
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;
}
}
@@ -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.');
}
}
}
@@ -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;
}
}