MqttSubscriber.php 3.7 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
  1. <?php
  2. namespace app\process;
  3. use app\service\MqttTopic;
  4. use Workerman\Mqtt\Client as MqttClient;
  5. use Webman\Channel\Client as ChannelClient;
  6. use Webman\Event\Event;
  7. use support\Log;
  8. class MqttSubscriber
  9. {
  10. private ?MqttClient $mqtt = null;
  11. public function onWorkerStart(): void
  12. {
  13. $config = config('mqtt');
  14. if (empty($config['enabled'])) {
  15. Log::channel('default')->info('MQTT process is disabled.');
  16. return;
  17. }
  18. ChannelClient::connect();
  19. ChannelClient::on('mqtt.publish', function (array $message): void {
  20. $this->publish($message);
  21. });
  22. $this->mqtt = new MqttClient($config['address'], $config['client']);
  23. $this->mqtt->onConnect = function (MqttClient $mqtt) use ($config): void {
  24. if (empty($config['subscriptions'])) {
  25. Log::channel('default')->warning('MQTT connected without configured subscriptions.');
  26. return;
  27. }
  28. $mqtt->subscribe($config['subscriptions'], [], function (?\Exception $exception, array $granted): void {
  29. if ($exception) {
  30. Log::channel('default')->error('MQTT subscription failed.', ['exception' => $exception->getMessage()]);
  31. return;
  32. }
  33. Log::channel('default')->info('MQTT subscriptions are active.', ['granted' => $granted]);
  34. });
  35. };
  36. $this->mqtt->onMessage = function (string $topic, string $payload, MqttClient $mqtt, array $properties = []) use ($config): void {
  37. if (strlen($payload) > $config['max_payload_bytes']) {
  38. Log::channel('default')->warning('MQTT message dropped because it exceeds the size limit.', ['topic' => $topic]);
  39. return;
  40. }
  41. $decoded = json_decode($payload, true);
  42. Log::channel('default')->info('MQTT message received.', ['topic' => $topic, 'bytes' => strlen($payload)]);
  43. Event::emit('mqtt.message', [
  44. 'topic' => $topic,
  45. 'payload' => $payload,
  46. 'data' => json_last_error() === JSON_ERROR_NONE ? $decoded : null,
  47. 'properties' => $properties,
  48. 'received_at' => date(DATE_ATOM),
  49. ]);
  50. };
  51. $this->mqtt->onError = function (\Exception $exception): void {
  52. Log::channel('default')->error('MQTT client error.', ['exception' => $exception->getMessage()]);
  53. };
  54. $this->mqtt->onClose = function (): void {
  55. Log::channel('default')->warning('MQTT connection closed; automatic reconnect is active.');
  56. };
  57. $this->mqtt->connect();
  58. }
  59. private function publish(array $message): void
  60. {
  61. if (!$this->mqtt) {
  62. Log::channel('default')->warning('MQTT publish rejected because the client is unavailable.', ['trace_id' => $message['traceId'] ?? null]);
  63. return;
  64. }
  65. try {
  66. MqttTopic::assertPublishable((string) ($message['topic'] ?? ''));
  67. $this->mqtt->publish(
  68. $message['topic'],
  69. $message['payload'] ?? '',
  70. ['qos' => $message['qos'] ?? 1, 'retain' => $message['retain'] ?? false],
  71. function (?\Exception $exception) use ($message): void {
  72. if ($exception) {
  73. Log::channel('default')->error('MQTT publish failed.', ['trace_id' => $message['traceId'] ?? null, 'exception' => $exception->getMessage()]);
  74. }
  75. }
  76. );
  77. } catch (\Throwable $exception) {
  78. Log::channel('default')->error('MQTT publish rejected.', ['trace_id' => $message['traceId'] ?? null, 'exception' => $exception->getMessage()]);
  79. }
  80. }
  81. }