This commit is contained in:
chenbo 2024-01-21 11:58:32 +08:00
parent f3cff15dd9
commit 503afe913a
77 changed files with 10734 additions and 6 deletions

View File

@ -35,7 +35,8 @@
"w7corp/easywechat": "^6.8", "w7corp/easywechat": "^6.8",
"tencentcloud/sms": "^3.0", "tencentcloud/sms": "^3.0",
"ext-curl": "*", "ext-curl": "*",
"dh2y/think-qrcode": "^2.0" "dh2y/think-qrcode": "^2.0",
"php-mqtt/client": "^2.0"
}, },
"require-dev": { "require-dev": {
"symfony/var-dumper": "^4.2", "symfony/var-dumper": "^4.2",

61
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "ef8fb5f6b7330deffd3410dd813510fc", "content-hash": "4f45bdaca9f5395f58768a17c5a0af9f",
"packages": [ "packages": [
{ {
"name": "adbario/php-dot-notation", "name": "adbario/php-dot-notation",
@ -1730,6 +1730,63 @@
], ],
"time": "2023-01-10T14:29:55+00:00" "time": "2023-01-10T14:29:55+00:00"
}, },
{
"name": "php-mqtt/client",
"version": "v2.0.0",
"source": {
"type": "git",
"url": "https://github.com/php-mqtt/client.git",
"reference": "458afc0bf33075ed8a1ffad72af5e10f2b516220"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-mqtt/client/zipball/458afc0bf33075ed8a1ffad72af5e10f2b516220",
"reference": "458afc0bf33075ed8a1ffad72af5e10f2b516220",
"shasum": ""
},
"require": {
"myclabs/php-enum": "^1.7",
"php": "^8.0",
"psr/log": "^1.1|^2.0|^3.0"
},
"require-dev": {
"phpunit/php-invoker": "^3.0",
"phpunit/phpunit": "^9.0",
"squizlabs/php_codesniffer": "^3.5"
},
"suggest": {
"ext-redis": "Required for the RedisRepository"
},
"type": "library",
"autoload": {
"psr-4": {
"PhpMqtt\\Client\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Marvin Mall",
"email": "marvin-mall@msn.com",
"role": "developer"
}
],
"description": "An MQTT client written in and for PHP.",
"keywords": [
"client",
"mqtt",
"publish",
"subscribe"
],
"support": {
"issues": "https://github.com/php-mqtt/client/issues",
"source": "https://github.com/php-mqtt/client/tree/v2.0.0"
},
"time": "2023-11-25T20:53:47+00:00"
},
{ {
"name": "phpoffice/phpspreadsheet", "name": "phpoffice/phpspreadsheet",
"version": "1.28.0", "version": "1.28.0",
@ -4961,5 +5018,5 @@
"ext-curl": "*" "ext-curl": "*"
}, },
"platform-dev": [], "platform-dev": [],
"plugin-api-version": "2.3.0" "plugin-api-version": "2.6.0"
} }

View File

@ -46,6 +46,7 @@ return array(
'Psr\\Container\\' => array($vendorDir . '/psr/container/src'), 'Psr\\Container\\' => array($vendorDir . '/psr/container/src'),
'Psr\\Cache\\' => array($vendorDir . '/psr/cache/src'), 'Psr\\Cache\\' => array($vendorDir . '/psr/cache/src'),
'PhpOffice\\PhpSpreadsheet\\' => array($vendorDir . '/phpoffice/phpspreadsheet/src/PhpSpreadsheet'), 'PhpOffice\\PhpSpreadsheet\\' => array($vendorDir . '/phpoffice/phpspreadsheet/src/PhpSpreadsheet'),
'PhpMqtt\\Client\\' => array($vendorDir . '/php-mqtt/client/src'),
'Overtrue\\Socialite\\' => array($vendorDir . '/overtrue/socialite/src'), 'Overtrue\\Socialite\\' => array($vendorDir . '/overtrue/socialite/src'),
'OSS\\' => array($vendorDir . '/aliyuncs/oss-sdk-php/src/OSS'), 'OSS\\' => array($vendorDir . '/aliyuncs/oss-sdk-php/src/OSS'),
'Nyholm\\Psr7\\' => array($vendorDir . '/nyholm/psr7/src'), 'Nyholm\\Psr7\\' => array($vendorDir . '/nyholm/psr7/src'),

View File

@ -104,6 +104,7 @@ class ComposerStaticInitd2a74ba94e266cc4f45a64c54a292d7e
'Psr\\Container\\' => 14, 'Psr\\Container\\' => 14,
'Psr\\Cache\\' => 10, 'Psr\\Cache\\' => 10,
'PhpOffice\\PhpSpreadsheet\\' => 25, 'PhpOffice\\PhpSpreadsheet\\' => 25,
'PhpMqtt\\Client\\' => 15,
), ),
'O' => 'O' =>
array ( array (
@ -316,6 +317,10 @@ class ComposerStaticInitd2a74ba94e266cc4f45a64c54a292d7e
array ( array (
0 => __DIR__ . '/..' . '/phpoffice/phpspreadsheet/src/PhpSpreadsheet', 0 => __DIR__ . '/..' . '/phpoffice/phpspreadsheet/src/PhpSpreadsheet',
), ),
'PhpMqtt\\Client\\' =>
array (
0 => __DIR__ . '/..' . '/php-mqtt/client/src',
),
'Overtrue\\Socialite\\' => 'Overtrue\\Socialite\\' =>
array ( array (
0 => __DIR__ . '/..' . '/overtrue/socialite/src', 0 => __DIR__ . '/..' . '/overtrue/socialite/src',

View File

@ -1790,6 +1790,66 @@
], ],
"install-path": "../overtrue/socialite" "install-path": "../overtrue/socialite"
}, },
{
"name": "php-mqtt/client",
"version": "v2.0.0",
"version_normalized": "2.0.0.0",
"source": {
"type": "git",
"url": "https://github.com/php-mqtt/client.git",
"reference": "458afc0bf33075ed8a1ffad72af5e10f2b516220"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-mqtt/client/zipball/458afc0bf33075ed8a1ffad72af5e10f2b516220",
"reference": "458afc0bf33075ed8a1ffad72af5e10f2b516220",
"shasum": ""
},
"require": {
"myclabs/php-enum": "^1.7",
"php": "^8.0",
"psr/log": "^1.1|^2.0|^3.0"
},
"require-dev": {
"phpunit/php-invoker": "^3.0",
"phpunit/phpunit": "^9.0",
"squizlabs/php_codesniffer": "^3.5"
},
"suggest": {
"ext-redis": "Required for the RedisRepository"
},
"time": "2023-11-25T20:53:47+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"PhpMqtt\\Client\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Marvin Mall",
"email": "marvin-mall@msn.com",
"role": "developer"
}
],
"description": "An MQTT client written in and for PHP.",
"keywords": [
"client",
"mqtt",
"publish",
"subscribe"
],
"support": {
"issues": "https://github.com/php-mqtt/client/issues",
"source": "https://github.com/php-mqtt/client/tree/v2.0.0"
},
"install-path": "../php-mqtt/client"
},
{ {
"name": "phpoffice/phpspreadsheet", "name": "phpoffice/phpspreadsheet",
"version": "1.28.0", "version": "1.28.0",

View File

@ -3,7 +3,7 @@
'name' => 'topthink/think', 'name' => 'topthink/think',
'pretty_version' => 'dev-master', 'pretty_version' => 'dev-master',
'version' => 'dev-master', 'version' => 'dev-master',
'reference' => '68951aabafc2dea38eda4ad270d23540f05b1e62', 'reference' => 'f3cff15dd91e4ec87d9ef4e7d8c466a239a3d08b',
'type' => 'project', 'type' => 'project',
'install_path' => __DIR__ . '/../../', 'install_path' => __DIR__ . '/../../',
'aliases' => array(), 'aliases' => array(),
@ -232,6 +232,15 @@
0 => '1.0', 0 => '1.0',
), ),
), ),
'php-mqtt/client' => array(
'pretty_version' => 'v2.0.0',
'version' => '2.0.0.0',
'reference' => '458afc0bf33075ed8a1ffad72af5e10f2b516220',
'type' => 'library',
'install_path' => __DIR__ . '/../php-mqtt/client',
'aliases' => array(),
'dev_requirement' => false,
),
'phpoffice/phpspreadsheet' => array( 'phpoffice/phpspreadsheet' => array(
'pretty_version' => '1.28.0', 'pretty_version' => '1.28.0',
'version' => '1.28.0.0', 'version' => '1.28.0.0',
@ -607,7 +616,7 @@
'topthink/think' => array( 'topthink/think' => array(
'pretty_version' => 'dev-master', 'pretty_version' => 'dev-master',
'version' => 'dev-master', 'version' => 'dev-master',
'reference' => '68951aabafc2dea38eda4ad270d23540f05b1e62', 'reference' => 'f3cff15dd91e4ec87d9ef4e7d8c466a239a3d08b',
'type' => 'project', 'type' => 'project',
'install_path' => __DIR__ . '/../../', 'install_path' => __DIR__ . '/../../',
'aliases' => array(), 'aliases' => array(),

2503
vendor/php-mqtt/client/.ci/emqx.conf vendored Normal file

File diff suppressed because it is too large Load Diff

56
vendor/php-mqtt/client/.ci/hivemq.xml vendored Normal file
View File

@ -0,0 +1,56 @@
<?xml version="1.0"?>
<hivemq xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/peez80/docker-hivemq/master/hivemq-config.xsd"
>
<listeners>
<!-- MQTT port without TLS -->
<tcp-listener>
<port>1883</port>
<bind-address>0.0.0.0</bind-address>
</tcp-listener>
<!-- MQTT port with TLS but without client certificate validation -->
<tls-tcp-listener>
<port>8883</port>
<bind-address>0.0.0.0</bind-address>
<tls>
<keystore>
<path>/hivemq-certs/server.jks</path>
<password>s3cr3t</password>
<private-key-password>s3cr3t</private-key-password>
</keystore>
<protocols>
<protocol>TLSv1.3</protocol>
<protocol>TLSv1.2</protocol>
<protocol>TLSv1.1</protocol>
<protocol>TLSv1</protocol>
</protocols>
</tls>
</tls-tcp-listener>
<!-- MQTT port with TLS and with client certificate validation -->
<tls-tcp-listener>
<port>8884</port>
<bind-address>0.0.0.0</bind-address>
<tls>
<client-authentication-mode>REQUIRED</client-authentication-mode>
<truststore>
<path>/hivemq-certs/ca.jks</path>
<password>s3cr3t</password>
</truststore>
<keystore>
<path>/hivemq-certs/server.jks</path>
<password>s3cr3t</password>
<private-key-password>s3cr3t</private-key-password>
</keystore>
<protocols>
<protocol>TLSv1.3</protocol>
<protocol>TLSv1.2</protocol>
<protocol>TLSv1.1</protocol>
<protocol>TLSv1</protocol>
</protocols>
</tls>
</tls-tcp-listener>
</listeners>
</hivemq>

View File

@ -0,0 +1,31 @@
# Config file for mosquitto
per_listener_settings true
# Port to use for the default listener.
listener 1883
allow_anonymous true
# Port to use for the default listener with authentication.
listener 1884
password_file /mosquitto/config/mosquitto.passwd
allow_anonymous false
# =================================================================
# Extra listeners
# =================================================================
# TLS listener without client certificate requirement
listener 8883
cafile /mosquitto-certs/ca.crt
certfile /mosquitto-certs/server.crt
keyfile /mosquitto-certs/server.key
require_certificate false
allow_anonymous true
# TLS listener with client certificate requirement
listener 8884
cafile /mosquitto-certs/ca.crt
certfile /mosquitto-certs/server.crt
keyfile /mosquitto-certs/server.key
require_certificate true
allow_anonymous true

View File

@ -0,0 +1 @@
ci-test-user:$6$QypQBNSQKE5bg6Ec$nzACfxhQ9qiYFByPPM/6GP/9kOWwDzEftN0EJPkS6M0PWqL55jAbBxUO863oWwhJ2q/YaubfLbe3xwwhBuoStQ==

View File

@ -0,0 +1,11 @@
listeners.tcp.default = 5672
loopback_users.guest = false
mqtt.listeners.tcp.default = 1883
mqtt.listeners.ssl = none
mqtt.allow_anonymous = true
mqtt.default_user = guest
mqtt.default_pass = guest
mqtt.vhost = /
mqtt.exchange = amq.topic
mqtt.subscription_ttl = 1800000

View File

@ -0,0 +1,2 @@
*
!.gitignore

View File

@ -0,0 +1,23 @@
version: 2
updates:
- package-ecosystem: "composer"
directory: "/"
allow:
- dependency-type: "development"
schedule:
interval: "daily"
time: "05:00"
timezone: "Europe/Vienna"
labels:
- "composer dependencies"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "05:00"
timezone: "Europe/Vienna"
labels:
- "github actions"

View File

@ -0,0 +1,25 @@
changelog:
exclude:
labels:
- ignore-for-release
authors:
- octocat
categories:
- title: Added
labels:
- enhancement
- title: Deprecated
labels:
- deprecated
- title: Removed
labels:
- removed
- title: Fixed
labels:
- bug
- title: Security
labels:
- security
- title: Changed
labels:
- "*"

View File

@ -0,0 +1,23 @@
name: 'Dependency Review'
on: [pull_request]
permissions:
contents: read
pull-requests: write
jobs:
dependency-review:
runs-on: ubuntu-latest
steps:
- name: 'Checkout Repository'
uses: actions/checkout@v4
- name: 'Dependency Review'
uses: actions/dependency-review-action@v3
with:
comment-summary-in-pr: true
fail-on-scopes: 'runtime, development, unknown'
fail-on-severity: 'low'
license-check: true
vulnerability-check: true

View File

@ -0,0 +1,140 @@
name: Tests
on:
push:
branches:
- master
pull_request_target:
types: [opened, synchronize, reopened]
jobs:
test-all:
name: Test PHP ${{ matrix.php-version }} using broker [${{ matrix.mqtt-broker }}]
runs-on: ubuntu-latest
strategy:
matrix:
php-version: ['8.0', '8.1', '8.2', '8.3']
mqtt-broker: ['mosquitto-1.6', 'mosquitto-2.0', 'hivemq', 'emqx', 'rabbitmq']
include:
- php-version: '8.3'
mqtt-broker: 'mosquitto-2.0'
run-sonarqube-analysis: true
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup PHP ${{ matrix.php-version }}
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
tools: phpunit:9.5.0
coverage: pcov
- name: Setup problem matchers for PHP
run: echo "::add-matcher::${{ runner.tool_cache }}/php.json"
- name: Setup problem matchers for PHPUnit
run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
- name: Get Composer Cache Directory
id: composer-cache
run: |
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache Composer dependencies
uses: actions/cache@v3
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-
- name: Install Composer dependencies
run: composer install --prefer-dist
- name: Generate certificates for tests
run: |
sh create-certificates.sh
chmod u+rx,g+rx ${{ github.workspace }}/.ci/tls
chmod a+r ${{ github.workspace }}/.ci/tls/*
- name: Start Mosquitto 1.6 message broker
if: matrix.mqtt-broker == 'mosquitto-1.6'
uses: Namoshek/mosquitto-github-action@v1
with:
version: '1.6'
ports: '1883:1883 1884:1884 8883:8883 8884:8884'
certificates: ${{ github.workspace }}/.ci/tls
config: ${{ github.workspace }}/.ci/mosquitto.conf
password-file: ${{ github.workspace}}/.ci/mosquitto.passwd
- name: Start Mosquitto 2.0 message broker
if: matrix.mqtt-broker == 'mosquitto-2.0'
uses: Namoshek/mosquitto-github-action@v1
with:
version: '2.0'
ports: '1883:1883 1884:1884 8883:8883 8884:8884'
certificates: ${{ github.workspace }}/.ci/tls
config: ${{ github.workspace }}/.ci/mosquitto.conf
password-file: ${{ github.workspace}}/.ci/mosquitto.passwd
- name: Start HiveMQ message broker
if: matrix.mqtt-broker == 'hivemq'
uses: Namoshek/hivemq4-github-action@v1
with:
version: '4.8.5'
ports: '1883:1883 8883:8883 8884:8884'
certificates: ${{ github.workspace }}/.ci/tls
config: ${{ github.workspace }}/.ci/hivemq.xml
- name: Start EMQ X message broker
if: matrix.mqtt-broker == 'emqx'
uses: Namoshek/emqx-github-action@v1.0.2
with:
version: '4.4.3'
ports: '1883:1883'
config: ${{ github.workspace }}/.ci/emqx.conf
- name: Start RabbitMQ message broker
if: matrix.mqtt-broker == 'rabbitmq'
uses: namoshek/rabbitmq-github-action@v1.1.0
with:
version: '3.8.9'
ports: '1883:1883'
config: ${{ github.workspace }}/.ci/rabbitmq.conf
plugins: 'rabbitmq_mqtt'
- name: Wait a bit until MQTT broker has started
run: sleep 45
- name: Run phpunit tests
run: composer test
env:
MQTT_BROKER_HOST: 'localhost'
MQTT_BROKER_PORT: 1883
MQTT_BROKER_PORT_WITH_AUTHENTICATION: ${{ (matrix.mqtt-broker == 'mosquitto-1.6' || matrix.mqtt-broker == 'mosquitto-2.0') && 1884 || 1883 }}
MQTT_BROKER_TLS_PORT: 8883
MQTT_BROKER_TLS_WITH_CLIENT_CERT_PORT: 8884
MQTT_BROKER_USERNAME: ${{ (matrix.mqtt-broker == 'mosquitto-1.6' || matrix.mqtt-broker == 'mosquitto-2.0') && 'ci-test-user' || '' }}
MQTT_BROKER_PASSWORD: ${{ (matrix.mqtt-broker == 'mosquitto-1.6' || matrix.mqtt-broker == 'mosquitto-2.0') && secrets.CI_MOSQUITTO_CI_TEST_USER_PASSWORD || '' }}
SKIP_TLS_TESTS: ${{ matrix.mqtt-broker == 'emqx' || matrix.mqtt-broker == 'rabbitmq' }}
- name: Dump Docker logs on failure
if: failure()
uses: jwalton/gh-docker-logs@v2
- name: Prepare paths for SonarQube analysis
if: matrix.run-sonarqube-analysis
run: |
sed -i "s|$GITHUB_WORKSPACE|/github/workspace|g" phpunit.coverage-clover.xml
sed -i "s|$GITHUB_WORKSPACE|/github/workspace|g" phpunit.report-junit.xml
- name: Run SonarQube analysis
uses: sonarsource/sonarcloud-github-action@v2.0.2
if: matrix.run-sonarqube-analysis
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONARCLOUD_TOKEN }}

6
vendor/php-mqtt/client/.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
.idea/
.phpunit.result.cache
composer.lock
phpunit.coverage*.xml
phpunit.report*.xml
/vendor/

88
vendor/php-mqtt/client/.phpcs.xml vendored Normal file
View File

@ -0,0 +1,88 @@
<?xml version="1.0"?>
<ruleset name="php-mqtt Code Style Standard">
<description>php-mqtt Code Style Standard</description>
<rule ref="PSR1"/>
<rule ref="PSR2">
<exclude name="PSR2.Methods.MethodDeclaration.AbstractAfterVisibility"/>
<exclude name="Squiz.ControlStructures.ControlSignature.SpaceAfterCloseParenthesis"/>
</rule>
<rule ref="Generic.Arrays.ArrayIndent">
<exclude name="Generic.Arrays.ArrayIndent.CloseBraceNotNewLine"/>
</rule>
<rule ref="Generic.Classes.DuplicateClassName"/>
<rule ref="Generic.CodeAnalysis.EmptyStatement">
<exclude name="Generic.CodeAnalysis.EmptyStatement.DetectedCatch"/>
</rule>
<rule ref="Generic.CodeAnalysis.ForLoopShouldBeWhileLoop"/>
<rule ref="Generic.CodeAnalysis.ForLoopWithTestFunctionCall"/>
<rule ref="Generic.CodeAnalysis.JumbledIncrementer"/>
<rule ref="Generic.CodeAnalysis.UnconditionalIfStatement"/>
<rule ref="Generic.CodeAnalysis.UnnecessaryFinalModifier"/>
<rule ref="Generic.CodeAnalysis.UselessOverridingMethod"/>
<rule ref="Generic.Commenting.Todo">
<exclude-pattern>src/*</exclude-pattern>
</rule>
<rule ref="Generic.ControlStructures.InlineControlStructure"/>
<rule ref="Generic.Files.ByteOrderMark"/>
<rule ref="Generic.Files.LineEndings"/>
<rule ref="Generic.Files.LineLength">
<properties>
<property name="lineLimit" value="150"/>
<property name="absoluteLineLimit" value="0"/>
</properties>
</rule>
<rule ref="Generic.Formatting.DisallowMultipleStatements"/>
<rule ref="Generic.Formatting.MultipleStatementAlignment"/>
<rule ref="Generic.Formatting.SpaceAfterCast"/>
<rule ref="Generic.Functions.CallTimePassByReference"/>
<rule ref="Generic.Functions.FunctionCallArgumentSpacing"/>
<rule ref="Generic.Functions.OpeningFunctionBraceBsdAllman"/>
<rule ref="Generic.Metrics.CyclomaticComplexity">
<properties>
<property name="complexity" value="50"/>
<property name="absoluteComplexity" value="100"/>
</properties>
</rule>
<rule ref="Generic.Metrics.NestingLevel">
<properties>
<property name="nestingLevel" value="10"/>
<property name="absoluteNestingLevel" value="30"/>
</properties>
</rule>
<rule ref="Generic.NamingConventions.ConstructorName"/>
<rule ref="Generic.PHP.LowerCaseConstant"/>
<rule ref="Generic.PHP.DeprecatedFunctions"/>
<rule ref="Generic.PHP.DisallowShortOpenTag"/>
<rule ref="Generic.PHP.ForbiddenFunctions"/>
<rule ref="Generic.WhiteSpace.DisallowTabIndent"/>
<rule ref="Generic.WhiteSpace.ScopeIndent">
<properties>
<property name="indent" value="4"/>
</properties>
</rule>
<rule ref="MySource.PHP.EvalObjectFactory"/>
<rule ref="PEAR.Commenting.ClassComment">
<exclude name="PEAR.Commenting.ClassComment.MissingAuthorTag"/>
<exclude name="PEAR.Commenting.ClassComment.MissingCategoryTag"/>
<exclude name="PEAR.Commenting.ClassComment.MissingLicenseTag"/>
<exclude name="PEAR.Commenting.ClassComment.MissingLinkTag"/>
</rule>
<rule ref="PEAR.Commenting.ClassComment.Missing"/>
<rule ref="PEAR.Commenting.ClassComment.MissingPackageTag"/>
<rule ref="PEAR.Commenting.InlineComment"/>
<rule ref="PSR1.Classes.ClassDeclaration.MissingNamespace"/>
<rule ref="PSR2.Methods.FunctionClosingBrace.SpacingBeforeClose"/>
<rule ref="Squiz.Arrays.ArrayDeclaration.NoCommaAfterLast"/>
<rule ref="Squiz.Functions.MultiLineFunctionDeclaration.NewlineBeforeOpenBrace">
<exclude-pattern>src/*</exclude-pattern>
</rule>
<rule ref="Zend.Files.ClosingTag"/>
<file>src</file>
<arg name="colors"/>
<arg value="sp"/>
<ini name="memory_limit" value="128M"/>
</ruleset>

72
vendor/php-mqtt/client/CHANGELOG.md vendored Normal file
View File

@ -0,0 +1,72 @@
# Changelog
## Version `v1.0.0`
Significant improvements to the architecture, API and design of the library have been part of `v1.0.0`.
Upgrading should be rather simple for most users though, since the public API has not changed a lot
and only in places which are not used too frequently.
A lot of effort has been put into this summary to document as many changes as possible.
It is impossible to give a guarantee about the completeness of this list though.
You should cover your uses of the library with tests yourself as well.
The following summary compares `v0.3.0` to `v1.0.0`.
### Breaking Changes
- The library does now require PHP 7.4 and supports PHP 8.0. This move was made with the clear intention to drop support for PHP 7.4 at some point.
- The primary interface and class of the library have been renamed to StudlyCaps to follow PSR-2:
- `\PhpMqtt\Client\Contracts\MQTTClient` &rarr; `\PhpMqtt\Client\Contracts\MqttClient`
- `\PhpMqtt\Client\MQTTClient` &rarr; `\PhpMqtt\Client\MqttClient`
- `\PhpMqtt\Client\Exceptions\MQTTClientException` &rarr; `\PhpMqtt\Client\Exceptions\MqttClientException`
- Protocol specific logic has been extracted from the `MQTTClient` class to a new interface `\PhpMqtt\Client\Contracts\MessageProcessor` and the respective implementation for MQTT 3.1, `\PhpMqtt\Client\MessageProcessors\Mqtt31MessageProcessor`.
- The `MessageProcessor` is responsible for parsing and building packages on a byte level.
- Splitting the logic from the main class did not only reduce the overall complexity of the class, it also made testing a lot easier and builds a solid foundation for future development and extension by implementing more protocol versions (like MQTT 5).
- Some `protected` properties have been changed to `private` to ensure they are not manipulated outside the offered scope, which is enforced through getters and setters. This change only affects users which actively inherited their own implementation from the library.
- The QoS 2 message flow is now implemented properly and should just work.
#### Connection Settings
- The `$caFile` parameter of the `MQTTClient` constructor as well as the `$username` and `$password` parameters of the `MQTTClient::connect()` method have been moved to the `ConnectionSettings` class.
- The `ConnectionSettings` use fluent setters for configuration now ([see README](README.md)).
- The `ConnectionSettings`` passed to `MQTTClient::connect()` are now validated and may not contain invalid configuration. In case of invalid configuration, a `\PhpMqtt\Client\Exceptions\ConfigurationInvalidException` is thrown.
- Additional TLS options have been added to the `ConnectionSettings` to support more uses cases with secured connections.
#### Methods
- Most methods can now throw a `\PhpMqtt\Client\Exceptions\RepositoryException` if an interaction with the repository fails. This should happen with the `MemoryRepository` only in exceptional situations, but when implementing persisted repositories, this may happen more frequently and should therefore be considered.
- The `MQTTClient::connect()` method had a parameter called `$sendCleanSessionFlag` while the `MqttClient::connect()` method has the same parameter, but called `$useCleanSession`. The parameters `$username` and `$password` have been removed entirely and are now part of the `ConnectionSettings`.
- The method `MQTTClient::close()` has been renamed to `MqttClient::disconnect()`.
- The parameter `$topic` of `MQTTClient::subscribe()` has been renamed to `$topicFilter` to reflect its meaning (which is a topic, but with wildcards). The `$callback` parameter can be `null` now and has `null` as default.
- The parameter `$topic` of `MQTTClient::unsubscribe()` has been renamed to `$topicFilter` as well.
#### Exceptions
- New exceptions have been introduced and old ones were removed. All exceptions inherit from `\PhpMqtt\Client\Exceptions\MqttClientException` as base. You should ensure your calls to methods of the `MqttClient` handle the exceptions appropriately.
- The exception constants previously defined on the `\PhpMqtt\Client\MQTTClient` class have been moved to the respective exception classes. This change only affects you if you used these constants to render detailed exception information for your users.
#### Repositories
- The `\PhpMqtt\Client\Contracts\Repository` interface has been changed significantly and summarizing all changes would be quite hard anyway. We therefore encourage you to have a look at the interface again and update your own implementation(s) of it, if you have any.
#### Logger
- The `\PhpMqtt\Client\Logger` implementation of `Psr\Log\LoggerInterface` does now decorate the log output with details about the MQTT client (format: `MQTT [{host}:{port}] [{clientId}] {message}`).
### Additions
- It is now possible to register event handlers for received messages. In combination with subscriptions without a callback, this allows to use centralized logic for multiple subscriptions. It also can be used for centralized logging, for example.
- A lot of unit and integration tests have been added which cover most parts of the library, especially the non-exception paths.
- All unit tests, integration tests, and the code style are enforced using a GitHub Actions workflow which runs under Ubuntu against multiple MQTT brokers (currently Mosquitto, HiveMQ and EMQ X). Contributing became easier therefore, but we expect that tests are added for changes and additions.
- To run the tests locally, an MQTT broker without authorization needs to run at `localhost:1883` (or the configuration in `phpunit.xml` is changed instead).
- The project is now analyzed using [sonarcloud.io](https://sonarcloud.io/dashboard?id=php-mqtt_client) which helps us keep up the high standards of the library.
#### Methods
- `MqttClient::isConnected()`: returns `true` if a connection is established (socket opened), and `false` otherwise.
- `MqttClient::getReceivedBytes()`: returns the number of raw bytes received from the broker (this includes meta information and not only message contents).
- `MqttClient::getSentBytes()`: returns the number of raw bytes sent to the broker (this includes meta information and not only message contents).
### Removals
_No functionality has been removed in this version._

21
vendor/php-mqtt/client/LICENSE.md vendored Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) Marvin Mall <marvin-mall@msn.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

315
vendor/php-mqtt/client/README.md vendored Normal file
View File

@ -0,0 +1,315 @@
# php-mqtt/client
[![Latest Stable Version](https://poser.pugx.org/php-mqtt/client/v)](https://packagist.org/packages/php-mqtt/client)
[![Total Downloads](https://poser.pugx.org/php-mqtt/client/downloads)](https://packagist.org/packages/php-mqtt/client)
[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=php-mqtt_client&metric=coverage)](https://sonarcloud.io/dashboard?id=php-mqtt_client)
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=php-mqtt_client&metric=alert_status)](https://sonarcloud.io/dashboard?id=php-mqtt_client)
[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=php-mqtt_client&metric=sqale_rating)](https://sonarcloud.io/dashboard?id=php-mqtt_client)
[![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=php-mqtt_client&metric=reliability_rating)](https://sonarcloud.io/dashboard?id=php-mqtt_client)
[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=php-mqtt_client&metric=security_rating)](https://sonarcloud.io/dashboard?id=php-mqtt_client)
[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=php-mqtt_client&metric=vulnerabilities)](https://sonarcloud.io/dashboard?id=php-mqtt_client)
[![License](https://poser.pugx.org/php-mqtt/client/license)](https://packagist.org/packages/php-mqtt/client)
[`php-mqtt/client`](https://packagist.org/packages/php-mqtt/client) was created by, and is maintained
by [Marvin Mall](https://github.com/namoshek).
It allows you to connect to an MQTT broker where you can publish messages and subscribe to topics.
The current implementation supports all QoS levels ([with limitations](#limitations)).
## Installation
The package is available on [packagist.org](https://packagist.org/packages/php-mqtt/client) and can be installed using `composer`:
```bash
composer require php-mqtt/client
```
The package requires PHP version 7.4 or higher.
## Usage
In the following, only a few very basic examples are given. For more elaborate examples, have a look at the
[`php-mqtt/client-examples` repository](https://github.com/php-mqtt/client-examples).
### Publish
A very basic publish example using QoS 0 requires only three steps: connect, publish and disconnect
```php
$server = 'some-broker.example.com';
$port = 1883;
$clientId = 'test-publisher';
$mqtt = new \PhpMqtt\Client\MqttClient($server, $port, $clientId);
$mqtt->connect();
$mqtt->publish('php-mqtt/client/test', 'Hello World!', 0);
$mqtt->disconnect();
```
If you do not want to pass a `$clientId`, a random one will be generated for you. This will basically force a clean session implicitly.
Be also aware that most of the methods can throw exceptions. The above example does not add any exception handling for brevity.
### Subscribe
Subscribing is a little more complex than publishing as it requires to run an event loop which reads, parses and handles messages from the broker:
```php
$server = 'some-broker.example.com';
$port = 1883;
$clientId = 'test-subscriber';
$mqtt = new \PhpMqtt\Client\MqttClient($server, $port, $clientId);
$mqtt->connect();
$mqtt->subscribe('php-mqtt/client/test', function ($topic, $message, $retained, $matchedWildcards) {
echo sprintf("Received message on topic [%s]: %s\n", $topic, $message);
}, 0);
$mqtt->loop(true);
$mqtt->disconnect();
```
While the loop is active, you can use `$mqtt->interrupt()` to send an interrupt signal to the loop.
This will terminate the loop before it starts its next iteration. You can call this method using `pcntl_signal(SIGINT, $handler)` for example:
```php
pcntl_async_signals(true);
$clientId = 'test-subscriber';
$mqtt = new \PhpMqtt\Client\MqttClient($server, $port, $clientId);
pcntl_signal(SIGINT, function (int $signal, $info) use ($mqtt) {
$mqtt->interrupt();
});
$mqtt->connect();
$mqtt->subscribe('php-mqtt/client/test', function ($topic, $message, $retained, $matchedWildcards) {
echo sprintf("Received message on topic [%s]: %s\n", $topic, $message);
}, 0);
$mqtt->loop(true);
$mqtt->disconnect();
```
### Client Settings
As shown in the examples above, the `MqttClient` takes the server, port and client id as first, second and third parameter.
As fourth parameter, the protocol level can be passed. Currently supported is MQTT v3.1,
available as constant `MqttClient::MQTT_3_1`.
A fifth parameter allows passing a repository (currently, only a `MemoryRepository` is available by default).
Lastly, a logger can be passed as sixth parameter. If none is given, a null logger is used instead.
Example:
```php
$mqtt = new \PhpMqtt\Client\MqttClient(
$server,
$port,
$clientId,
\PhpMqtt\Client\MqttClient::MQTT_3_1,
new \PhpMqtt\Client\Repositories\MemoryRepository(),
new Logger()
);
```
The `Logger` must implement the `Psr\Log\LoggerInterface`.
### Connection Settings
The `connect()` method of the `MqttClient` takes two optional parameters:
1. A `ConnectionSettings` instance
2. A `boolean` flag indicating whether a clean session should be requested (a random client id does this implicitly)
Example:
```php
$mqtt = new \PhpMqtt\Client\MqttClient($server, $port, $clientId);
$connectionSettings = (new \PhpMqtt\Client\ConnectionSettings)
->setConnectTimeout(3)
->setUseTls(true)
->setTlsSelfSignedAllowed(true);
$mqtt->connect($connectionSettings, true);
```
The `ConnectionSettings` class provides a few settings through a fluent interface. The type itself is immutable,
and a new `ConnectionSettings` instance will be created for each added option.
This also prevents changes to the connection settings after a connection has been established.
The following is a complete list of options with their respective default:
```php
$connectionSettings = (new \PhpMqtt\Client\ConnectionSettings)
// The username used for authentication when connecting to the broker.
->setUsername(null)
// The password used for authentication when connecting to the broker.
->setPassword(null)
// 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.
->useBlockingSocket(false)
// The connect timeout defines 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.
->setConnectTimeout(60)
// 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.
->setSocketTimeout(5)
// 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.
->setResendTimeout(10)
// 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.
->setReconnectAutomatically(false)
// Defines the maximum number of reconnect attempts until the client gives up.
// This setting is only relevant if setReconnectAutomatically() is set to true.
->setMaxReconnectAttempts(3)
// Defines the delay between reconnect attempts in milliseconds.
// This setting is only relevant if setReconnectAutomatically() is set to true.
->setDelayBetweenReconnectAttempts(0)
// 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).
->setKeepAliveInterval(10)
// 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.
->setLastWillTopic(null)
// 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.
->setLastWillMessage(null)
// The quality of service level the last will message of the client will be published with,
// if it gets triggered.
->setLastWillQualityOfService(0)
// 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.
->setRetainLastWill(false)
// 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.
->setUseTls(false)
// This flag determines if the peer certificate is verified, if TLS is used.
->setTlsVerifyPeer(true)
// This flag determines if the peer name is verified, if TLS is used.
->setTlsVerifyPeerName(true)
// 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.
->setTlsSelfSignedAllowed(false)
// The path to a Certificate Authority certificate which is used to verify the peer
// certificate, if TLS is used.
->setTlsCertificateAuthorityFile(null)
// The path to a directory containing Certificate Authority certificates which are
// used to verify the peer certificate, if TLS is used.
->setTlsCertificateAuthorityPath(null)
// 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.
->setTlsClientCertificateFile(null)
// The path to a client certificate key file used for authentication, if TLS is used.
//
// This option requires ConnectionSettings::setTlsClientCertificateFile() to be used as well.
->setTlsClientCertificateKeyFile(null)
// 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 ConnectionSettings::setTlsClientCertificateFile() and
// ConnectionSettings::setTlsClientCertificateKeyFile() to be used as well.
->setTlsClientCertificateKeyPassphrase(null);
```
## Features
- Supported MQTT Versions
- [x] v3 (just don't use v3.1 features like username & password)
- [x] v3.1
- [x] v3.1.1
- [ ] v5.0
- Transport
- [x] TCP (unsecured)
- [x] TLS (secured, verifies the peer using a certificate authority file)
- Connect
- [x] Last Will
- [x] Message Retention
- [x] Authentication (username & password)
- [x] TLS encrypted connections
- [ ] Clean Session (can be set and sent, but the client has no persistence for QoS 2 messages)
- Publish
- [x] QoS Level 0
- [x] QoS Level 1 (limitation: no persisted state across sessions)
- [x] QoS Level 2 (limitation: no persisted state across sessions)
- Subscribe
- [x] QoS Level 0
- [x] QoS Level 1
- [x] QoS Level 2 (limitation: no persisted state across sessions)
- Supported Message Length: unlimited _(no limits enforced, although the MQTT protocol supports only up to 256MB which one shouldn't use even remotely anyway)_
- Logging possible (`Psr\Log\LoggerInterface` can be passed to the client)
- Persistence Drivers
- [x] In-Memory Driver
- [ ] Redis Driver
## Limitations
- Message flows with a QoS level higher than 0 are not persisted as the default implementation uses an in-memory repository for data.
To avoid issues with broken message flows, use the clean session flag to indicate that you don't care about old data.
It will not only instruct the broker to consider the connection new (without previous state), but will also reset the registered repository.
## Developing & Testing
### Certificates (TLS)
To run the tests (especially the TLS tests), you will need to create certificates. A command has been provided for this:
```sh
sh create-certificates.sh
```
This will create all required certificates in the `.ci/tls/` directory. The same script is used for continuous integration as well.
### MQTT Broker for Testing
Running the tests expects an MQTT broker to be running. The easiest way to run an MQTT broker is through Docker:
```sh
docker run --rm -it \
-p 1883:1883 \
-p 1884:1884 \
-p 8883:8883 \
-p 8884:8884 \
-v $(pwd)/.ci/tls:/mosquitto-certs \
-v $(pwd)/.ci/mosquitto.conf:/mosquitto/config/mosquitto.conf \
-v $(pwd)/.ci/mosquitto.passwd:/mosquitto/config/mosquitto.passwd \
eclipse-mosquitto:1.6
```
When run from the project directory, this will spawn a Mosquitto MQTT broker configured with the generated TLS certificates and a custom configuration.
In case you intend to run a different broker or using a different method, or use a public broker instead,
you will need to adjust the environment variables defined in `phpunit.xml` accordingly.
## License
`php-mqtt/client` is open-sourced software licensed under the [MIT license](LICENSE.md).

53
vendor/php-mqtt/client/composer.json vendored Normal file
View File

@ -0,0 +1,53 @@
{
"name": "php-mqtt/client",
"description": "An MQTT client written in and for PHP.",
"type": "library",
"keywords": [
"mqtt",
"client",
"publish",
"subscribe"
],
"license": "MIT",
"authors": [
{
"name": "Marvin Mall",
"email": "marvin-mall@msn.com",
"role": "developer"
}
],
"autoload": {
"psr-4": {
"PhpMqtt\\Client\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"require": {
"php": "^8.0",
"psr/log": "^1.1|^2.0|^3.0",
"myclabs/php-enum": "^1.7"
},
"require-dev": {
"phpunit/php-invoker": "^3.0",
"phpunit/phpunit": "^9.0",
"squizlabs/php_codesniffer": "^3.5"
},
"suggest": {
"ext-redis": "Required for the RedisRepository"
},
"scripts": {
"fix:cs": "vendor/bin/phpcbf",
"test": [
"@test:cs",
"@test:all"
],
"test:all": "vendor/bin/phpunit --testdox --log-junit=phpunit.report-junit.xml --coverage-clover=phpunit.coverage-clover.xml --coverage-text",
"test:cs": "vendor/bin/phpcs",
"test:feature": "vendor/bin/phpunit --testsuite=Feature --testdox --log-junit=phpunit.report-junit.xml --coverage-clover=phpunit.coverage-clover.xml --coverage-text",
"test:unit": "vendor/bin/phpunit --testsuite=Unit --testdox --log-junit=phpunit.report-junit.xml --coverage-clover=phpunit.coverage-clover.xml --coverage-text"
}
}

View File

@ -0,0 +1,30 @@
#!/bin/sh
# Generate a new CA certificate and key.
openssl genrsa -out .ci/tls/ca.key 2048
openssl req -x509 -new -nodes -key .ci/tls/ca.key -days 1 -out .ci/tls/ca.crt -subj "/C=AT/ST=Vorarlberg/CN=php-mqtt Test CA"
# Copy ca.crt to a file named by the hashed subject of the certificate. This is required for PHP's capath option to find the certificate.
cp .ci/tls/ca.crt .ci/tls/$(openssl x509 -hash -noout -in .ci/tls/ca.crt).0
# Create a Java Trust Store from the CA certificate. This is used by HiveMQ.
keytool -import -file .ci/tls/ca.crt -alias ca -keystore .ci/tls/ca.jks -storepass s3cr3t -trustcacerts -noprompt
# Generate a new server certificate and key, signed by the created CA.
openssl genrsa -out .ci/tls/server.key 2048
openssl req -new -key .ci/tls/server.key -out .ci/tls/server.csr -sha512 -subj "/C=AT/ST=Vorarlberg/CN=localhost"
openssl x509 -req -in .ci/tls/server.csr -CA .ci/tls/ca.crt -CAkey .ci/tls/ca.key -CAcreateserial -out .ci/tls/server.crt -days 1 -sha512
# Generate a Java Key Store from the server certificate. This is used by HiveMQ.
openssl pkcs12 -export -in .ci/tls/server.crt -inkey .ci/tls/server.key -out .ci/tls/server.p12 -passout pass:s3cr3t
keytool -importkeystore -srckeystore .ci/tls/server.p12 -srcstoretype PKCS12 -destkeystore .ci/tls/server.jks -deststoretype JKS -srcstorepass s3cr3t -deststorepass s3cr3t -noprompt
# Generate a client certificate without passphrase, signed by the created CA.
openssl genrsa -out .ci/tls/client.key 2048
openssl req -new -key .ci/tls/client.key -out .ci/tls/client.csr -sha512 -subj "/C=AT/ST=Vorarlberg/CN=localhost"
openssl x509 -req -in .ci/tls/client.csr -CA .ci/tls/ca.crt -CAkey .ci/tls/ca.key -CAcreateserial -out .ci/tls/client.crt -days 1 -sha256
# Generate a client certificate with passphrase, signed by the created CA.
openssl genrsa -aes128 -passout pass:s3cr3t -out .ci/tls/client2.key 2048
openssl req -new -key .ci/tls/client2.key -passin pass:s3cr3t -out .ci/tls/client2.csr -sha512 -subj "/C=AT/ST=Vorarlberg/CN=localhost"
openssl x509 -req -in .ci/tls/client2.csr -CA .ci/tls/ca.crt -CAkey .ci/tls/ca.key -CAcreateserial -out .ci/tls/client2.crt -days 1 -sha256

34
vendor/php-mqtt/client/phpunit.xml vendored Normal file
View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
enforceTimeLimit="true"
defaultTimeLimit="3"
timeoutForSmallTests="2"
timeoutForMediumTests="5"
timeoutForLargeTests="10"
>
<php>
<env name="MQTT_BROKER_HOST" value="localhost"/>
<env name="MQTT_BROKER_PORT" value="1883"/>
<env name="MQTT_BROKER_PORT_WITH_AUTHENTICATION" value="1884"/>
<env name="MQTT_BROKER_TLS_PORT" value="8883"/>
<env name="MQTT_BROKER_TLS_WITH_CLIENT_CERT_PORT" value="8884"/>
<env name="TLS_CERT_DIR" value=".ci/tls"/>
<env name="SKIP_TLS_TESTS" value="false"/>
</php>
<testsuites>
<testsuite name="Unit">
<directory suffix="Test.php">tests/Unit</directory>
</testsuite>
<testsuite name="Feature">
<directory suffix="Test.php">tests/Feature</directory>
</testsuite>
</testsuites>
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">src</directory>
</include>
</coverage>
</phpunit>

View File

@ -0,0 +1,18 @@
sonar.organization=php-mqtt
sonar.projectKey=php-mqtt_client
# Paths are relative to the sonar-project.properties file.
sonar.sources=src
sonar.tests=tests
# Test report and code coverage related settings.
sonar.php.tests.reportPath=phpunit.report-junit.xml
sonar.php.coverage.reportPaths=phpunit.coverage-clover.xml
# Encoding of the source code. Default is default system encoding.
sonar.sourceEncoding=UTF-8
# Links for sonarcloud.io page.
sonar.links.ci=https://github.com/php-mqtt/client/actions
sonar.links.scm=https://github.com/php-mqtt/client
sonar.links.issue=https://github.com/php-mqtt/client/issues

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,534 @@
<?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;
/**
* 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;
}
}

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';
}

1232
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;
}
}

View File

@ -0,0 +1,51 @@
<?php
/** @noinspection PhpUnhandledExceptionInspection */
declare(strict_types=1);
namespace Tests\Feature;
use PhpMqtt\Client\Exceptions\ClientNotConnectedToBrokerException;
use PhpMqtt\Client\MqttClient;
use Tests\TestCase;
/**
* Tests that the client throws an exception if not connected.
*
* @package Tests\Feature
*/
class ActionsWithoutActiveConnectionTest extends TestCase
{
public function test_throws_exception_when_message_is_published_without_connecting_to_broker(): void
{
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'test-not-connected');
$this->expectException(ClientNotConnectedToBrokerException::class);
$client->publish('foo/bar', 'baz');
}
public function test_throws_exception_when_topic_is_subscribed_without_connecting_to_broker(): void
{
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'test-not-connected');
$this->expectException(ClientNotConnectedToBrokerException::class);
$client->subscribe('foo/bar', fn () => true);
}
public function test_throws_exception_when_topic_is_unsubscribed_without_connecting_to_broker(): void
{
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'test-not-connected');
$this->expectException(ClientNotConnectedToBrokerException::class);
$client->unsubscribe('foo/bar');
}
public function test_throws_exception_when_disconnecting_without_connecting_to_broker_first(): void
{
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'test-not-connected');
$this->expectException(ClientNotConnectedToBrokerException::class);
$client->disconnect();
}
}

View File

@ -0,0 +1,93 @@
<?php
/** @noinspection PhpUnhandledExceptionInspection */
declare(strict_types=1);
namespace Tests\Feature;
use PhpMqtt\Client\MqttClient;
use Tests\TestCase;
/**
* Tests that the client utils (optional methods) work as intended.
*
* @package Tests\Feature
*/
class ClientUtilsTest extends TestCase
{
public function test_counts_sent_and_received_bytes_correctly(): void
{
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'test-byte-count');
$client->connect(null, true);
// Even the connection request and acknowledgement have bytes.
$this->assertGreaterThan(0, $client->getSentBytes());
$this->assertGreaterThan(0, $client->getReceivedBytes());
// We therefore remember the current transfer stats and send some more data.
$sentBytesBeforePublish = $client->getSentBytes();
$receivedBytesBeforePublish = $client->getReceivedBytes();
$client->publish('foo/bar', 'baz-01', MqttClient::QOS_AT_MOST_ONCE);
$client->publish('foo/bar', 'baz-02', MqttClient::QOS_AT_LEAST_ONCE);
$client->publish('foo/bar', 'baz-03', MqttClient::QOS_EXACTLY_ONCE);
$this->assertGreaterThan($sentBytesBeforePublish, $client->getSentBytes());
$this->assertSame($receivedBytesBeforePublish, $client->getReceivedBytes());
// Also we receive all acknowledgements to update our transfer stats correctly.
$client->loop(true, true);
$this->assertGreaterThan($receivedBytesBeforePublish, $client->getReceivedBytes());
$client->disconnect();
}
public function test_is_connected_returns_correct_state(): void
{
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'test-is-connected');
$client->connect(null, true);
$this->assertTrue($client->isConnected());
$client->disconnect();
$this->assertFalse($client->isConnected());
$client->connect(null, true);
$this->assertTrue($client->isConnected());
$client->disconnect();
$this->assertFalse($client->isConnected());
}
public function test_configured_client_id_is_returned_if_client_id_is_passed_to_constructor(): void
{
$clientId = 'test-configured-client-id';
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, $clientId);
$this->assertSame($clientId, $client->getClientId());
}
public function test_generated_client_id_is_returned_if_no_client_id_is_passed_to_constructor(): void
{
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort);
$this->assertNotNull($client->getClientId());
$this->assertNotEmpty($client->getClientId());
}
public function test_configured_broker_host_and_port_are_returned(): void
{
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort);
$this->assertSame($this->mqttBrokerHost, $client->getHost());
$this->assertSame($this->mqttBrokerPort, $client->getPort());
}
}

View File

@ -0,0 +1,83 @@
<?php
/** @noinspection PhpUnhandledExceptionInspection */
declare(strict_types=1);
namespace Tests\Feature;
use PhpMqtt\Client\ConnectionSettings;
use PhpMqtt\Client\MqttClient;
use Tests\TestCase;
/**
* Tests that the client is able to connect to a broker with custom connection settings.
*
* @package Tests\Feature
*/
class ConnectWithCustomConnectionSettingsTest extends TestCase
{
public function test_connecting_using_mqtt31_with_custom_connection_settings_works_as_intended(): void
{
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPortWithAuthentication, 'test-custom-connection-settings', MqttClient::MQTT_3_1);
$connectionSettings = (new ConnectionSettings)
->setLastWillTopic('foo/last/will')
->setLastWillMessage('baz is out!')
->setLastWillQualityOfService(MqttClient::QOS_AT_MOST_ONCE)
->setRetainLastWill(true)
->setConnectTimeout(3)
->setSocketTimeout(3)
->setResendTimeout(3)
->setKeepAliveInterval(30)
->setUsername($this->mqttBrokerUsername)
->setPassword($this->mqttBrokerPassword)
->setUseTls(false)
->setTlsCertificateAuthorityFile(null)
->setTlsCertificateAuthorityPath(null)
->setTlsClientCertificateFile(null)
->setTlsClientCertificateKeyFile(null)
->setTlsClientCertificateKeyPassphrase(null)
->setTlsVerifyPeer(false)
->setTlsVerifyPeerName(false)
->setTlsSelfSignedAllowed(true);
$client->connect($connectionSettings);
$this->assertTrue($client->isConnected());
$client->disconnect();
}
public function test_connecting_using_mqtt311_with_custom_connection_settings_works_as_intended(): void
{
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPortWithAuthentication, 'test-custom-connection-settings', MqttClient::MQTT_3_1_1);
$connectionSettings = (new ConnectionSettings)
->setLastWillTopic('foo/last/will')
->setLastWillMessage('baz is out!')
->setLastWillQualityOfService(MqttClient::QOS_AT_MOST_ONCE)
->setRetainLastWill(true)
->setConnectTimeout(3)
->setSocketTimeout(3)
->setResendTimeout(3)
->setKeepAliveInterval(30)
->setUsername($this->mqttBrokerUsername)
->setPassword($this->mqttBrokerPassword)
->setUseTls(false)
->setTlsCertificateAuthorityFile(null)
->setTlsCertificateAuthorityPath(null)
->setTlsClientCertificateFile(null)
->setTlsClientCertificateKeyFile(null)
->setTlsClientCertificateKeyPassphrase(null)
->setTlsVerifyPeer(false)
->setTlsVerifyPeerName(false)
->setTlsSelfSignedAllowed(true);
$client->connect($connectionSettings);
$this->assertTrue($client->isConnected());
$client->disconnect();
}
}

View File

@ -0,0 +1,200 @@
<?php
/** @noinspection PhpDocSignatureInspection */
/** @noinspection PhpUnhandledExceptionInspection */
declare(strict_types=1);
namespace Tests\Feature;
use PhpMqtt\Client\ConnectionSettings;
use PhpMqtt\Client\Exceptions\ConfigurationInvalidException;
use PhpMqtt\Client\Exceptions\ConnectingToBrokerFailedException;
use PhpMqtt\Client\Exceptions\ProtocolNotSupportedException;
use PhpMqtt\Client\MqttClient;
use Tests\TestCase;
/**
* Tests that the client cannot connect with invalid configuration.
*
* @package Tests\Feature
*/
class ConnectWithInvalidConfigurationTest extends TestCase
{
public function invalidTimeouts(): array
{
return [
[0],
[-1],
[-100],
];
}
/**
* @dataProvider invalidTimeouts
*/
public function test_connect_timeout_cannot_be_below_1_second(int $timeout): void
{
$connectionSettings = (new ConnectionSettings)->setConnectTimeout($timeout);
$this->connectAndExpectConfigurationExceptionUsingSettings($connectionSettings);
}
/**
* @dataProvider invalidTimeouts
*/
public function test_socket_timeout_cannot_be_below_1_second(int $timeout): void
{
$connectionSettings = (new ConnectionSettings)->setSocketTimeout($timeout);
$this->connectAndExpectConfigurationExceptionUsingSettings($connectionSettings);
}
/**
* @dataProvider invalidTimeouts
*/
public function test_resend_timeout_cannot_be_below_1_second(int $timeout): void
{
$connectionSettings = (new ConnectionSettings)->setResendTimeout($timeout);
$this->connectAndExpectConfigurationExceptionUsingSettings($connectionSettings);
}
public function invalidKeepAliveIntervals(): array
{
return [
[0],
[-1],
[-100],
[65536],
[100000],
];
}
/**
* @dataProvider invalidKeepAliveIntervals
*/
public function test_keep_alive_interval_cannot_be_value_below_1_or_greater_than_65535(int $keepAliveInterval): void
{
$connectionSettings = (new ConnectionSettings)->setKeepAliveInterval($keepAliveInterval);
$this->connectAndExpectConfigurationExceptionUsingSettings($connectionSettings);
}
public function invalidUsernames(): array
{
return [
[''],
[' '],
[' '],
[' '],
];
}
/**
* @dataProvider invalidUsernames
*/
public function test_username_cannot_be_empty_or_whitespace(string $username): void
{
$connectionSettings = (new ConnectionSettings)->setUsername($username);
$this->connectAndExpectConfigurationExceptionUsingSettings($connectionSettings);
}
public function invalidLastWillTopics(): array
{
return [
[''],
[' '],
[' '],
[' '],
];
}
/**
* @dataProvider invalidLastWillTopics
*/
public function test_last_will_topic_cannot_be_empty_or_whitespace(string $topic): void
{
$connectionSettings = (new ConnectionSettings)->setLastWillTopic($topic);
$this->connectAndExpectConfigurationExceptionUsingSettings($connectionSettings);
}
public function invalidLastWillQualityOfService(): array
{
return [
[-1],
[3],
];
}
/**
* @dataProvider invalidLastWillQualityOfService
*/
public function test_last_will_quality_of_service_cannot_be_outside_the_0_to_2_range(int $qualityOfService): void
{
$connectionSettings = (new ConnectionSettings)->setLastWillQualityOfService($qualityOfService);
$this->connectAndExpectConfigurationExceptionUsingSettings($connectionSettings);
}
public function test_tls_certificate_authority_file_cannot_be_invalid_file_path(): void
{
$connectionSettings = (new ConnectionSettings)->setTlsCertificateAuthorityFile(__DIR__.'/not_existing_file');
$this->connectAndExpectConfigurationExceptionUsingSettings($connectionSettings);
}
public function test_tls_certificate_authority_path_cannot_be_invalid_directory_path(): void
{
$connectionSettings = (new ConnectionSettings)->setTlsCertificateAuthorityPath(__DIR__.'/not_existing_directory');
$this->connectAndExpectConfigurationExceptionUsingSettings($connectionSettings);
}
public function test_tls_client_certificate_file_cannot_be_invalid_file_path(): void
{
$connectionSettings = (new ConnectionSettings)->setTlsClientCertificateFile(__DIR__.'/not_existing_file');
$this->connectAndExpectConfigurationExceptionUsingSettings($connectionSettings);
}
public function test_tls_client_certificate_key_file_cannot_be_invalid_file_path(): void
{
$connectionSettings = (new ConnectionSettings)->setTlsClientCertificateKeyFile(__DIR__.'/not_existing_file');
$this->connectAndExpectConfigurationExceptionUsingSettings($connectionSettings);
}
public function test_tls_client_certificate_file_must_be_set_if_client_certificate_key_file_is_set(): void
{
$connectionSettings = (new ConnectionSettings)->setTlsClientCertificateKeyFile(__DIR__.'/../resources/invalid-test-certificate.key');
$this->connectAndExpectConfigurationExceptionUsingSettings($connectionSettings);
}
public function test_tls_client_certificate_key_file_must_be_set_if_client_certificate_key_passphrase_is_set(): void
{
$connectionSettings = (new ConnectionSettings)
->setTlsClientCertificateFile(__DIR__.'/../resources/invalid-test-certificate.crt')
->setTlsClientCertificateKeyPassphrase('some');
$this->connectAndExpectConfigurationExceptionUsingSettings($connectionSettings);
}
/**
* Performs the actual connection test using the given connection settings. Expects the settings to be invalid.
*
* @throws ConfigurationInvalidException
* @throws ConnectingToBrokerFailedException
* @throws ProtocolNotSupportedException
*/
private function connectAndExpectConfigurationExceptionUsingSettings(ConnectionSettings $connectionSettings): void
{
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'test-invalid-connection-settings');
$this->expectException(ConfigurationInvalidException::class);
$client->connect($connectionSettings);
}
}

View File

@ -0,0 +1,36 @@
<?php
/** @noinspection PhpUnhandledExceptionInspection */
declare(strict_types=1);
namespace Tests\Feature;
use PhpMqtt\Client\Exceptions\ConnectingToBrokerFailedException;
use PhpMqtt\Client\MqttClient;
use Tests\TestCase;
/**
* Tests that the client throws an exception if connecting using invalid host and port.
*
* @package Tests\Feature
*/
class ConnectWithInvalidHostAndPortTest extends TestCase
{
public function test_throws_exception_when_connecting_using_invalid_host_and_port(): void
{
$client = new MqttClient('127.0.0.1', 56565, 'test-invalid-host');
$this->expectException(ConnectingToBrokerFailedException::class);
$this->expectExceptionCode(ConnectingToBrokerFailedException::EXCEPTION_CONNECTION_SOCKET_ERROR);
try {
$client->connect(null, true);
} catch (ConnectingToBrokerFailedException $e) {
$this->assertGreaterThan(0, $e->getConnectionErrorCode());
$this->assertNotEmpty($e->getConnectionErrorMessage());
throw $e;
}
}
}

View File

@ -0,0 +1,136 @@
<?php
/** @noinspection PhpUnhandledExceptionInspection */
declare(strict_types=1);
namespace Tests\Feature;
use PhpMqtt\Client\ConnectionSettings;
use PhpMqtt\Client\Exceptions\ConnectingToBrokerFailedException;
use PhpMqtt\Client\MqttClient;
use Tests\TestCase;
/**
* Tests that the client is able to connect to a broker using TLS.
*
* @package Tests\Feature
*/
class ConnectWithTlsSettingsTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
if ($this->skipTlsTests) {
$this->markTestSkipped('TLS tests are disabled.');
}
}
public function test_connecting_with_tls_but_without_further_configuration_throws_for_self_signed_certificate(): void
{
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerTlsPort, 'test-tls-settings');
$connectionSettings = (new ConnectionSettings)
->setUseTls(true);
$this->expectException(ConnectingToBrokerFailedException::class);
$this->expectExceptionCode(ConnectingToBrokerFailedException::EXCEPTION_CONNECTION_TLS_ERROR);
$client->connect($connectionSettings, true);
}
public function test_connecting_with_tls_with_ignored_self_signed_certificate_works_as_intended(): void
{
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerTlsPort, 'test-tls-settings');
$connectionSettings = (new ConnectionSettings)
->setUseTls(true)
->setTlsSelfSignedAllowed(true)
->setTlsVerifyPeer(false)
->setTlsVerifyPeerName(false);
$client->connect($connectionSettings, true);
$this->assertTrue($client->isConnected());
$client->disconnect();
}
public function test_connecting_with_tls_with_validated_self_signed_certificate_using_cafile__works_as_intended(): void
{
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerTlsPort, 'test-tls-settings');
$connectionSettings = (new ConnectionSettings)
->setUseTls(true)
->setTlsSelfSignedAllowed(false)
->setTlsVerifyPeer(true)
->setTlsVerifyPeerName(true)
->setTlsCertificateAuthorityFile($this->tlsCertificateDirectory . '/ca.crt');
$client->connect($connectionSettings, true);
$this->assertTrue($client->isConnected());
$client->disconnect();
}
public function test_connecting_with_tls_with_validated_self_signed_certificate_using_capath_works_as_intended(): void
{
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerTlsPort, 'test-tls-settings');
$connectionSettings = (new ConnectionSettings)
->setUseTls(true)
->setTlsSelfSignedAllowed(false)
->setTlsVerifyPeer(true)
->setTlsVerifyPeerName(true)
->setTlsCertificateAuthorityPath($this->tlsCertificateDirectory);
$client->connect($connectionSettings, true);
$this->assertTrue($client->isConnected());
$client->disconnect();
}
public function test_connecting_with_tls_and_client_certificate_with_validated_self_signed_certificate_works_as_intended(): void
{
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerTlsWithClientCertificatePort, 'test-tls-settings');
$connectionSettings = (new ConnectionSettings)
->setUseTls(true)
->setTlsSelfSignedAllowed(false)
->setTlsVerifyPeer(true)
->setTlsVerifyPeerName(true)
->setTlsCertificateAuthorityFile($this->tlsCertificateDirectory . '/ca.crt')
->setTlsClientCertificateFile($this->tlsCertificateDirectory . '/client.crt')
->setTlsClientCertificateKeyFile($this->tlsCertificateDirectory . '/client.key');
$client->connect($connectionSettings, true);
$this->assertTrue($client->isConnected());
$client->disconnect();
}
public function test_connecting_with_tls_and_passphrase_protected_client_certificate_with_validated_self_signed_certificate_works_as_intended(): void
{
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerTlsWithClientCertificatePort, 'test-tls-settings');
$connectionSettings = (new ConnectionSettings)
->setUseTls(true)
->setTlsSelfSignedAllowed(false)
->setTlsVerifyPeer(true)
->setTlsVerifyPeerName(true)
->setTlsCertificateAuthorityFile($this->tlsCertificateDirectory . '/ca.crt')
->setTlsClientCertificateFile($this->tlsCertificateDirectory . '/client2.crt')
->setTlsClientCertificateKeyFile($this->tlsCertificateDirectory . '/client2.key')
->setTlsClientCertificateKeyPassphrase('s3cr3t');
$client->connect($connectionSettings, true);
$this->assertTrue($client->isConnected());
$client->disconnect();
}
}

View File

@ -0,0 +1,116 @@
<?php
/** @noinspection PhpUnhandledExceptionInspection */
declare(strict_types=1);
namespace Tests\Feature;
use PhpMqtt\Client\MqttClient;
use Tests\TestCase;
/**
* Tests that the connected event handler work as intended.
*
* @package Tests\Feature
*/
class ConnectedEventHandlerTest extends TestCase
{
public function test_connected_event_handlers_are_called_every_time_the_client_connects_successfully(): void
{
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'test-connected-event-handler');
$handlerCallCount = 0;
$handler = function () use (&$handlerCallCount) {
$handlerCallCount++;
};
$client->registerConnectedEventHandler($handler);
$client->connect();
$this->assertSame(1, $handlerCallCount);
$client->disconnect();
$client->connect();
$this->assertSame(2, $handlerCallCount);
$client->disconnect();
$client->connect();
$this->assertSame(3, $handlerCallCount);
$client->disconnect();
}
public function test_connected_event_handlers_can_be_unregistered_and_will_not_be_called_anymore(): void
{
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'test-connected-event-handler');
$handlerCallCount = 0;
$handler = function () use (&$handlerCallCount) {
$handlerCallCount++;
};
$client->registerConnectedEventHandler($handler);
$client->connect();
$this->assertSame(1, $handlerCallCount);
$client->unregisterConnectedEventHandler($handler);
$client->disconnect();
$client->connect();
$this->assertSame(1, $handlerCallCount);
$client->registerConnectedEventHandler($handler);
$client->disconnect();
$client->connect();
$this->assertSame(2, $handlerCallCount);
$client->unregisterConnectedEventHandler($handler);
$client->disconnect();
$client->connect();
$this->assertSame(2, $handlerCallCount);
$client->disconnect();
}
public function test_connected_event_handlers_can_throw_exceptions_which_does_not_affect_other_handlers_or_the_application(): void
{
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'test-connected-event-handler');
$handlerCallCount = 0;
$handler1 = function () use (&$handlerCallCount) {
$handlerCallCount++;
};
$handler2 = function () {
throw new \Exception('Something went wrong!');
};
$client->registerConnectedEventHandler($handler1);
$client->registerConnectedEventHandler($handler2);
$client->connect();
$this->assertSame(1, $handlerCallCount);
$client->disconnect();
}
public function test_connected_event_handler_is_passed_the_mqtt_client_and_the_auto_reconnect_flag_as_arguments(): void
{
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'test-connected-event-handler');
$client->registerConnectedEventHandler(function ($mqttClient, $isAutoReconnect) {
$this->assertInstanceOf(MqttClient::class, $mqttClient);
$this->assertIsBool($isAutoReconnect);
$this->assertFalse($isAutoReconnect);
});
$client->connect();
$client->disconnect();
}
}

View File

@ -0,0 +1,143 @@
<?php
/** @noinspection PhpUnhandledExceptionInspection */
declare(strict_types=1);
namespace Tests\Feature;
use PhpMqtt\Client\MqttClient;
use Tests\TestCase;
/**
* Tests that the loop event handler work as intended.
*
* @package Tests\Feature
*/
class LoopEventHandlerTest extends TestCase
{
public function test_loop_event_handlers_are_called_for_each_loop_iteration(): void
{
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'test-loop-event-handler');
$loopCount = 0;
$previousElapsedTime = 0;
$client->registerLoopEventHandler(function (MqttClient $client, float $elapsedTime) use (&$loopCount, &$previousElapsedTime) {
$this->assertGreaterThanOrEqual($previousElapsedTime, $elapsedTime);
$previousElapsedTime = $elapsedTime;
$loopCount++;
if ($loopCount >= 3) {
$client->interrupt();
return;
}
});
$client->connect(null, true);
$client->loop();
$this->assertSame(3, $loopCount);
$client->disconnect();
}
public function test_loop_event_handler_can_be_unregistered_and_will_not_be_called_anymore(): void
{
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'test-loop-event-handler');
$loopCount = 0;
$handler = function (MqttClient $client) use (&$loopCount) {
$loopCount++;
if ($loopCount >= 1) {
$client->interrupt();
return;
}
};
$client->registerLoopEventHandler($handler);
$client->connect(null, true);
$client->loop();
$this->assertSame(1, $loopCount);
$client->unregisterLoopEventHandler($handler);
$client->loop(true, true);
$this->assertSame(1, $loopCount);
$client->disconnect();
}
public function test_all_loop_event_handlers_can_be_unregistered_and_will_not_be_called_anymore(): void
{
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'test-loop-event-handler');
$loopCount1 = 0;
$loopCount2 = 0;
$handler1 = function (MqttClient $client) use (&$loopCount1) {
$loopCount1++;
if ($loopCount1 >= 1) {
$client->interrupt();
return;
}
};
$handler2 = function () use (&$loopCount2) {
$loopCount2++;
};
$client->registerLoopEventHandler($handler1);
$client->registerLoopEventHandler($handler2);
$client->connect(null, true);
$client->loop();
$this->assertSame(1, $loopCount1);
$this->assertSame(1, $loopCount2);
$client->unregisterLoopEventHandler();
$client->loop(true, true);
$this->assertSame(1, $loopCount1);
$this->assertSame(1, $loopCount2);
$client->disconnect();
}
public function test_loop_event_handlers_can_throw_exceptions_which_does_not_affect_other_handlers_or_the_application(): void
{
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'test-publish-event-handler');
$loopCount = 0;
$handler1 = function () {
throw new \Exception('Something went wrong!');
};
$handler2 = function (MqttClient $client) use (&$loopCount) {
$loopCount++;
if ($loopCount >= 1) {
$client->interrupt();
return;
}
};
$client->registerLoopEventHandler($handler1);
$client->registerLoopEventHandler($handler2);
$client->connect(null, true);
$client->loop(true);
$this->assertSame(1, $loopCount);
$client->disconnect();
}
}

View File

@ -0,0 +1,169 @@
<?php
/** @noinspection PhpUnhandledExceptionInspection */
declare(strict_types=1);
namespace Tests\Feature;
use PhpMqtt\Client\MqttClient;
use Tests\TestCase;
/**
* Tests that the message received event handler work as intended.
*
* @package Tests\Feature
*/
class MessageReceivedEventHandlerTest extends TestCase
{
public function test_message_received_event_handlers_are_called_for_each_received_message(): void
{
// We connect and subscribe to a topic using the first client.
$subscriber = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'subscriber');
$subscriber->connect(null, true);
$handlerCallCount = 0;
$handler = function (MqttClient $client, string $topic, string $message, int $qualityOfService, bool $retained) use (&$handlerCallCount) {
$handlerCallCount++;
$this->assertSame('foo/bar/baz', $topic);
$this->assertSame('hello world', $message);
$this->assertSame(0, $qualityOfService);
$this->assertFalse($retained);
$client->interrupt();
};
$subscriber->registerMessageReceivedEventHandler($handler);
$subscriber->subscribe('foo/bar/baz');
// We publish a message from a second client on the same topic.
$publisher = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'publisher');
$publisher->connect(null, true);
$publisher->publish('foo/bar/baz', 'hello world', 0, false);
// Then we loop on the subscriber to (hopefully) receive the published message.
$subscriber->loop(true);
$this->assertSame(1, $handlerCallCount);
// Finally, we disconnect for a graceful shutdown on the broker side.
$publisher->disconnect();
$subscriber->disconnect();
}
public function test_message_received_event_handler_can_be_unregistered_and_will_not_be_called_anymore(): void
{
// We connect and subscribe to a topic using the first client.
$subscriber = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'subscriber');
$subscriber->connect(null, true);
$callCount = 0;
$handler = function (MqttClient $client, string $topic, string $message, int $qualityOfService, bool $retained) use (&$handler, &$callCount) {
$callCount++;
$this->assertSame('foo/bar/baz/01', $topic);
$this->assertSame('hello world', $message);
$this->assertSame(0, $qualityOfService);
$this->assertFalse($retained);
$client->unregisterMessageReceivedEventHandler($handler);
$client->interrupt();
};
$subscriber->registerMessageReceivedEventHandler($handler);
$subscriber->subscribe('foo/bar/baz/+');
// We publish a message from a second client on the same topic.
$publisher = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'publisher');
$publisher->connect(null, true);
$publisher->publish('foo/bar/baz/01', 'hello world', 0, false);
$publisher->publish('foo/bar/baz/02', 'hello world', 0, false);
// Then we loop on the subscriber to (hopefully) receive the published message.
$subscriber->loop(true);
$this->assertSame(1, $callCount);
// Finally, we disconnect for a graceful shutdown on the broker side.
$publisher->disconnect();
$subscriber->disconnect();
}
public function test_message_received_event_handlers_can_be_unregistered_and_will_not_be_called_anymore(): void
{
// We connect and subscribe to a topic using the first client.
$subscriber = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'subscriber');
$subscriber->connect(null, true);
$callCount = 0;
$handler = function (MqttClient $client, string $topic, string $message, int $qualityOfService, bool $retained) use (&$callCount) {
$callCount++;
$this->assertSame('foo/bar/baz/01', $topic);
$this->assertSame('hello world', $message);
$this->assertSame(0, $qualityOfService);
$this->assertFalse($retained);
$client->unregisterMessageReceivedEventHandler();
$client->interrupt();
};
$subscriber->registerMessageReceivedEventHandler($handler);
$subscriber->subscribe('foo/bar/baz/+');
// We publish a message from a second client on the same topic.
$publisher = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'publisher');
$publisher->connect(null, true);
$publisher->publish('foo/bar/baz/01', 'hello world', 0, false);
$publisher->publish('foo/bar/baz/02', 'hello world', 0, false);
// Then we loop on the subscriber to (hopefully) receive the published message.
$subscriber->loop(true);
$this->assertSame(1, $callCount);
// Finally, we disconnect for a graceful shutdown on the broker side.
$publisher->disconnect();
$subscriber->disconnect();
}
public function test_message_received_event_handlers_can_throw_exceptions_which_does_not_affect_other_handlers_or_the_application(): void
{
// We connect and subscribe to a topic using the first client.
$subscriber = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'subscriber');
$subscriber->connect(null, true);
$handlerCallCount = 0;
$handler1 = function () {
throw new \Exception('Something went wrong!');
};
$handler2 = function (MqttClient $client) use (&$handlerCallCount) {
$handlerCallCount++;
$client->interrupt();
};
$subscriber->registerMessageReceivedEventHandler($handler1);
$subscriber->registerMessageReceivedEventHandler($handler2);
$subscriber->subscribe('foo/bar/baz');
// We publish a message from a second client on the same topic.
$publisher = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'publisher');
$publisher->connect(null, true);
$publisher->publish('foo/bar/baz', 'hello world', 0, false);
// Then we loop on the subscriber to (hopefully) receive the published message.
$subscriber->loop(true);
$this->assertSame(1, $handlerCallCount);
// Finally, we disconnect for a graceful shutdown on the broker side.
$publisher->disconnect();
$subscriber->disconnect();
}
}

View File

@ -0,0 +1,96 @@
<?php
/** @noinspection PhpUnhandledExceptionInspection */
declare(strict_types=1);
namespace Tests\Feature;
use PhpMqtt\Client\MqttClient;
use Tests\TestCase;
/**
* Tests that the publish event handler work as intended.
*
* @package Tests\Feature
*/
class PublishEventHandlerTest extends TestCase
{
public function test_publish_event_handlers_are_called_for_each_published_message(): void
{
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'test-publish-event-handler');
$handlerCallCount = 0;
$handler = function () use (&$handlerCallCount) {
$handlerCallCount++;
};
$client->registerPublishEventHandler($handler);
$client->connect(null, true);
$client->publish('foo/bar', 'baz-01');
$client->publish('foo/bar', 'baz-02');
$client->publish('foo/bar', 'baz-03');
$this->assertSame(3, $handlerCallCount);
$client->disconnect();
}
public function test_publish_event_handlers_can_be_unregistered_and_will_not_be_called_anymore(): void
{
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'test-publish-event-handler');
$handlerCallCount = 0;
$handler = function () use (&$handlerCallCount) {
$handlerCallCount++;
};
$client->registerPublishEventHandler($handler);
$client->connect(null, true);
$client->publish('foo/bar', 'baz-01');
$this->assertSame(1, $handlerCallCount);
$client->unregisterPublishEventHandler($handler);
$client->publish('foo/bar', 'baz-02');
$this->assertSame(1, $handlerCallCount);
$client->registerPublishEventHandler($handler);
$client->publish('foo/bar', 'baz-03');
$this->assertSame(2, $handlerCallCount);
$client->unregisterPublishEventHandler();
$client->publish('foo/bar', 'baz-04');
$this->assertSame(2, $handlerCallCount);
$client->disconnect();
}
public function test_publish_event_handlers_can_throw_exceptions_which_does_not_affect_other_handlers_or_the_application(): void
{
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'test-publish-event-handler');
$handlerCallCount = 0;
$handler1 = function () use (&$handlerCallCount) {
$handlerCallCount++;
};
$handler2 = function () {
throw new \Exception('Something went wrong!');
};
$client->registerPublishEventHandler($handler1);
$client->registerPublishEventHandler($handler2);
$client->connect(null, true);
$client->publish('foo/bar', 'baz-01');
$this->assertSame(1, $handlerCallCount);
$client->disconnect();
}
}

View File

@ -0,0 +1,427 @@
<?php
/** @noinspection PhpDocSignatureInspection */
/** @noinspection PhpUnhandledExceptionInspection */
declare(strict_types=1);
namespace Tests\Feature;
use PhpMqtt\Client\ConnectionSettings;
use PhpMqtt\Client\MqttClient;
use Tests\TestCase;
/**
* Tests that publishing messages and subscribing to topics using an MQTT broker works.
*
* @package Tests\Feature
*/
class PublishSubscribeTest extends TestCase
{
public function publishSubscribeData(): array
{
$data = [
[false, 'foo/bar/baz', 'foo/bar/baz', 'hello world', []],
[false, 'foo/bar/+', 'foo/bar/baz', 'hello world', ['baz']],
[false, 'foo/+/baz', 'foo/bar/baz', 'hello world', ['bar']],
[false, 'foo/#', 'foo/bar/baz', 'hello world', ['bar/baz']],
[false, 'foo/+/bar/#', 'foo/my/bar/baz', 'hello world', ['my', 'baz']],
[false, 'foo/+/bar/#', 'foo/my/bar/baz/blub', 'hello world', ['my', 'baz/blub']],
[false, 'foo/bar/baz', 'foo/bar/baz', random_bytes(2 * 1024 * 1024), []], // 2MB message
[true, 'foo/bar/baz', 'foo/bar/baz', 'hello world', []],
[true, 'foo/bar/+', 'foo/bar/baz', 'hello world', ['baz']],
[true, 'foo/+/baz', 'foo/bar/baz', 'hello world', ['bar']],
[true, 'foo/#', 'foo/bar/baz', 'hello world', ['bar/baz']],
[true, 'foo/+/bar/#', 'foo/my/bar/baz', 'hello world', ['my', 'baz']],
[true, 'foo/+/bar/#', 'foo/my/bar/baz/blub', 'hello world', ['my', 'baz/blub']],
[true, 'foo/bar/baz', 'foo/bar/baz', random_bytes(2 * 1024 * 1024), []], // 2MB message
];
// Because our tests are run against a real MQTT broker and some messages are retained,
// we need to prevent false-positives by giving each test case its own 'test space' using a random prefix.
for ($i = 0; $i < count($data); $i++) {
$prefix = 'test/' . uniqid('', true) . '/';
$data[$i][1] = $prefix . $data[$i][1];
$data[$i][2] = $prefix . $data[$i][2];
}
return $data;
}
/**
* @dataProvider publishSubscribeData
*/
public function test_publishing_and_subscribing_using_quality_of_service_0_works_as_intended(
bool $useBlockingSocket,
string $subscriptionTopicFilter,
string $publishTopic,
string $publishMessage,
array $matchedTopicWildcards
): void
{
// We connect and subscribe to a topic using the first client.
$connectionSettings = (new ConnectionSettings())
->useBlockingSocket($useBlockingSocket);
$subscriber = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'subscriber');
$subscriber->connect($connectionSettings, true);
$subscriber->subscribe(
$subscriptionTopicFilter,
function (string $topic, string $message, bool $retained, array $wildcards) use ($subscriber, $publishTopic, $publishMessage, $matchedTopicWildcards) {
// By asserting something here, we will avoid a no-assertions-in-test warning, making the test pass.
$this->assertEquals($publishTopic, $topic);
$this->assertEquals($publishMessage, $message);
$this->assertFalse($retained);
$this->assertEquals($matchedTopicWildcards, $wildcards);
$subscriber->interrupt(); // This allows us to exit the test as soon as possible.
},
0
);
// We publish a message from a second client on the same topic.
$publisher = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'publisher');
$publisher->connect(null, true);
$publisher->publish($publishTopic, $publishMessage, 0, false);
// Then we loop on the subscriber to (hopefully) receive the published message.
$subscriber->loop(true);
// Finally, we disconnect for a graceful shutdown on the broker side.
$publisher->disconnect();
$subscriber->disconnect();
}
/**
* @dataProvider publishSubscribeData
*/
public function test_publishing_and_subscribing_using_quality_of_service_0_with_message_retention_works_as_intended(
bool $useBlockingSocket,
string $subscriptionTopicFilter,
string $publishTopic,
string $publishMessage,
array $matchedTopicWildcards
): void
{
// We publish a message from the first client, which disconnects before the other client even subscribes.
$publisher = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'publisher');
$publisher->connect(null, true);
$publisher->publish($publishTopic, $publishMessage, 0, true);
$publisher->disconnect();
// Because we need to make sure the message reached the broker, we delay the execution for a short period (100ms) intentionally.
// With higher QoS, this is replaced by awaiting delivery of the message.
usleep(100_000);
// We connect and subscribe to a topic using the second client.
$connectionSettings = (new ConnectionSettings())
->useBlockingSocket($useBlockingSocket);
$subscriber = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'subscriber');
$subscriber->connect($connectionSettings, true);
$subscriber->subscribe(
$subscriptionTopicFilter,
function (string $topic, string $message, bool $retained, array $wildcards) use ($subscriber, $publishTopic, $publishMessage, $matchedTopicWildcards) {
// By asserting something here, we will avoid a no-assertions-in-test warning, making the test pass.
$this->assertEquals($publishTopic, $topic);
$this->assertEquals($publishMessage, $message);
$this->assertTrue($retained);
$this->assertEquals($matchedTopicWildcards, $wildcards);
$subscriber->interrupt(); // This allows us to exit the test as soon as possible.
},
0
);
// Then we loop on the subscriber to (hopefully) receive the published message.
$subscriber->loop(true);
// Finally, we disconnect for a graceful shutdown on the broker side.
$subscriber->disconnect();
}
/**
* @dataProvider publishSubscribeData
*/
public function test_publishing_and_subscribing_using_quality_of_service_1_works_as_intended(
bool $useBlockingSocket,
string $subscriptionTopicFilter,
string $publishTopic,
string $publishMessage,
array $matchedTopicWildcards
): void
{
// We connect and subscribe to a topic using the first client.
$connectionSettings = (new ConnectionSettings())
->useBlockingSocket($useBlockingSocket);
$subscriber = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'subscriber');
$subscriber->connect($connectionSettings, true);
$subscriber->subscribe(
$subscriptionTopicFilter,
function (string $topic, string $message, bool $retained, array $wildcards) use ($subscriber, $publishTopic, $publishMessage, $matchedTopicWildcards) {
// By asserting something here, we will avoid a no-assertions-in-test warning, making the test pass.
$this->assertEquals($publishTopic, $topic);
$this->assertEquals($publishMessage, $message);
$this->assertFalse($retained);
$this->assertEquals($matchedTopicWildcards, $wildcards);
$subscriber->interrupt(); // This allows us to exit the test as soon as possible.
},
1
);
// We publish a message from a second client on the same topic.
$publisher = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'publisher');
$publisher->connect(null, true);
$publisher->publish($publishTopic, $publishMessage, 1, false);
// Then we loop on the subscriber to (hopefully) receive the published message.
$subscriber->loop(true);
// Finally, we disconnect for a graceful shutdown on the broker side.
$publisher->disconnect();
$subscriber->disconnect();
}
/**
* @dataProvider publishSubscribeData
*/
public function test_publishing_and_subscribing_using_quality_of_service_1_with_message_retention_works_as_intended(
bool $useBlockingSocket,
string $subscriptionTopicFilter,
string $publishTopic,
string $publishMessage,
array $matchedTopicWildcards
): void
{
// We publish a message from the first client, which disconnects before the other client even subscribes.
$publisher = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'publisher');
$publisher->connect(null, true);
$publisher->publish($publishTopic, $publishMessage, 1, true);
$publisher->loop(true, true);
$publisher->disconnect();
// We connect and subscribe to a topic using the second client.
$connectionSettings = (new ConnectionSettings())
->useBlockingSocket($useBlockingSocket);
$subscriber = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'subscriber');
$subscriber->connect($connectionSettings, true);
$subscriber->subscribe(
$subscriptionTopicFilter,
function (string $topic, string $message, bool $retained, array $wildcards) use ($subscriber, $publishTopic, $publishMessage, $matchedTopicWildcards) {
// By asserting something here, we will avoid a no-assertions-in-test warning, making the test pass.
$this->assertEquals($publishTopic, $topic);
$this->assertEquals($publishMessage, $message);
$this->assertTrue($retained);
$this->assertEquals($matchedTopicWildcards, $wildcards);
$subscriber->interrupt(); // This allows us to exit the test as soon as possible.
},
1
);
// Then we loop on the subscriber to (hopefully) receive the published message.
$subscriber->loop(true);
// Finally, we disconnect for a graceful shutdown on the broker side.
$subscriber->disconnect();
}
/**
* @dataProvider publishSubscribeData
*/
public function test_publishing_and_subscribing_using_quality_of_service_2_works_as_intended(
bool $useBlockingSocket,
string $subscriptionTopicFilter,
string $publishTopic,
string $publishMessage,
array $matchedTopicWildcards
): void
{
// We connect and subscribe to a topic using the first client.
$connectionSettings = (new ConnectionSettings())
->useBlockingSocket($useBlockingSocket);
$subscriber = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'subscriber');
$subscriber->connect($connectionSettings, true);
$subscription = function (string $topic, string $message, bool $retained, array $wildcards) use ($subscriber, $subscriptionTopicFilter, $publishTopic, $publishMessage, $matchedTopicWildcards) {
// By asserting something here, we will avoid a no-assertions-in-test warning, making the test pass.
$this->assertEquals($publishTopic, $topic);
$this->assertEquals($publishMessage, $message);
$this->assertFalse($retained);
$this->assertEquals($matchedTopicWildcards, $wildcards);
$subscriber->unsubscribe($subscriptionTopicFilter);
$subscriber->interrupt(); // This allows us to exit the test as soon as possible.
};
$subscriber->subscribe($subscriptionTopicFilter, $subscription, 2);
// We publish a message from a second client on the same topic. The loop is called until all QoS 2 handshakes are done.
$publisher = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'publisher');
$publisher->connect(null, true);
$publisher->publish($publishTopic, $publishMessage, 2, false);
$publisher->loop(true, true);
// Then we loop on the subscriber to (hopefully) receive the published message until the receive handshake is done.
$subscriber->loop(true, true);
// Finally, we disconnect for a graceful shutdown on the broker side.
$publisher->disconnect();
$subscriber->disconnect();
}
/**
* @dataProvider publishSubscribeData
*/
public function test_publishing_and_subscribing_using_quality_of_service_2_with_message_retention_works_as_intended(
bool $useBlockingSocket,
string $subscriptionTopicFilter,
string $publishTopic,
string $publishMessage,
array $matchedTopicWildcards
): void
{
// We publish a message from the first client. The loop is called until all QoS 2 handshakes are done.
$publisher = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'publisher');
$publisher->connect(null, true);
$publisher->publish($publishTopic, $publishMessage, 2, true);
$publisher->loop(true, true);
$publisher->disconnect();
// We connect and subscribe to a topic using the second client.
$connectionSettings = (new ConnectionSettings())
->useBlockingSocket($useBlockingSocket);
$subscriber = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'subscriber');
$subscriber->connect($connectionSettings, true);
$subscription = function (string $topic, string $message, bool $retained, array $wildcards) use ($subscriber, $subscriptionTopicFilter, $publishTopic, $publishMessage, $matchedTopicWildcards) {
// By asserting something here, we will avoid a no-assertions-in-test warning, making the test pass.
$this->assertEquals($publishTopic, $topic);
$this->assertEquals($publishMessage, $message);
$this->assertTrue($retained);
$this->assertEquals($matchedTopicWildcards, $wildcards);
$subscriber->unsubscribe($subscriptionTopicFilter);
$subscriber->interrupt(); // This allows us to exit the test as soon as possible.
};
$subscriber->subscribe($subscriptionTopicFilter, $subscription, 2);
// Then we loop on the subscriber to (hopefully) receive the published message until the receive handshake is done.
$subscriber->loop(true, true);
// Finally, we disconnect for a graceful shutdown on the broker side.
$subscriber->disconnect();
}
public function test_unsubscribe_stops_receiving_messages_on_topic(): void
{
// We connect and subscribe to a topic using the first client.
$subscriber = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'subscriber');
$subscriber->connect(null, true);
$subscribedTopic = 'test/foo/bar/baz';
$receivedMessageCount = 0;
$subscriber->subscribe(
$subscribedTopic,
function (string $topic, string $message, bool $retained) use ($subscriber, $subscribedTopic, &$receivedMessageCount) {
$receivedMessageCount++;
// By asserting something here, we will avoid a no-assertions-in-test warning, making the test pass.
$this->assertEquals('test/foo/bar/baz', $topic);
$this->assertEquals('hello world', $message);
$this->assertFalse($retained);
$subscriber->unsubscribe($subscribedTopic);
},
0
);
// We publish a message from a second client on the same topic.
$publisher = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'publisher');
$publisher->connect(null, true);
$publisher->publish($subscribedTopic, 'hello world', 0, false);
// Then we loop on the subscriber to (hopefully) receive the published message.
$subscriber->loop(true, true);
$this->assertSame(1, $receivedMessageCount);
$publisher->publish($subscribedTopic, 'hello world #2', 0, false);
$subscriber->loop(true, true);
// Ensure no second message has been received since we are not subscribed anymore.
$this->assertSame(1, $receivedMessageCount);
// Finally, we disconnect for a graceful shutdown on the broker side.
$publisher->disconnect();
$subscriber->disconnect();
}
public function test_shared_subscriptions_using_quality_of_service_0_work_as_intended(): void
{
$subscriptionTopicFilter = '$share/test-shared-subscriptions/foo/+';
$publishTopic = 'foo/bar';
// We connect and subscribe to a topic using the first client with a shared subscription.
$subscriber1 = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'subscriber1');
$subscriber1->connect(null, true);
$subscriber1->subscribe($subscriptionTopicFilter, function (string $topic, string $message, bool $retained) use ($subscriber1, $publishTopic) {
// By asserting something here, we will avoid a no-assertions-in-test warning, making the test pass.
$this->assertEquals($publishTopic, $topic);
$this->assertEquals('hello world #1', $message);
$this->assertFalse($retained);
$subscriber1->interrupt(); // This allows us to exit the test as soon as possible.
}, 0);
// We connect and subscribe to a topic using the second client with a shared subscription.
$subscriber2 = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'subscriber2');
$subscriber2->connect(null, true);
$subscriber2->subscribe($subscriptionTopicFilter, function (string $topic, string $message, bool $retained) use ($subscriber2, $publishTopic) {
// By asserting something here, we will avoid a no-assertions-in-test warning, making the test pass.
$this->assertEquals($publishTopic, $topic);
$this->assertEquals('hello world #2', $message);
$this->assertFalse($retained);
$subscriber2->interrupt(); // This allows us to exit the test as soon as possible.
}, 0);
// We publish a message from a second client on the same topic.
$publisher = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'publisher');
$publisher->connect(null, true);
$publisher->publish($publishTopic, 'hello world #1', 0, false);
$publisher->publish($publishTopic, 'hello world #2', 0, false);
// Then we loop on the subscribers to (hopefully) receive the published messages.
$subscriber1->loop(true);
$subscriber2->loop(true);
// Finally, we disconnect for a graceful shutdown on the broker side.
$publisher->disconnect();
$subscriber1->disconnect();
$subscriber2->disconnect();
}
}

View File

@ -0,0 +1,47 @@
<?php
/** @noinspection PhpUnhandledExceptionInspection */
declare(strict_types=1);
namespace Tests\Feature;
use PhpMqtt\Client\Exceptions\ProtocolNotSupportedException;
use PhpMqtt\Client\MqttClient;
use Tests\TestCase;
/**
* Tests the protocols supported (and not supported) by the client.
*
* @package Tests\Feature
*/
class SupportedProtocolsTest extends TestCase
{
public function test_client_supports_mqtt_3_1_protocol(): void
{
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'test-protocol', MqttClient::MQTT_3_1);
$this->assertInstanceOf(MqttClient::class, $client);
}
public function test_client_supports_mqtt_3_1_1_protocol(): void
{
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'test-protocol', MqttClient::MQTT_3_1_1);
$this->assertInstanceOf(MqttClient::class, $client);
}
public function test_client_does_not_support_mqtt_3_protocol(): void
{
$this->expectException(ProtocolNotSupportedException::class);
new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'test-protocol', '3');
}
public function test_client_does_not_support_mqtt_5_protocol(): void
{
$this->expectException(ProtocolNotSupportedException::class);
new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'test-protocol', '5');
}
}

View File

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Tests;
/**
* A base class for all tests.
*
* @package Tests
*/
abstract class TestCase extends \PHPUnit\Framework\TestCase
{
protected string $mqttBrokerHost;
protected int $mqttBrokerPort;
protected int $mqttBrokerPortWithAuthentication;
protected int $mqttBrokerTlsPort;
protected int $mqttBrokerTlsWithClientCertificatePort;
protected ?string $mqttBrokerUsername = null;
protected ?string $mqttBrokerPassword = null;
protected bool $skipTlsTests;
protected string $tlsCertificateDirectory;
/**
* {@inheritdoc}
*/
protected function setUp(): void
{
parent::setUp();
$this->mqttBrokerHost = getenv('MQTT_BROKER_HOST');
$this->mqttBrokerPort = intval(getenv('MQTT_BROKER_PORT'));
$this->mqttBrokerPortWithAuthentication = intval(getenv('MQTT_BROKER_PORT_WITH_AUTHENTICATION'));
$this->mqttBrokerTlsPort = intval(getenv('MQTT_BROKER_TLS_PORT'));
$this->mqttBrokerTlsWithClientCertificatePort = intval(getenv('MQTT_BROKER_TLS_WITH_CLIENT_CERT_PORT'));
$this->mqttBrokerUsername = getenv('MQTT_BROKER_USERNAME') ?: null;
$this->mqttBrokerPassword = getenv('MQTT_BROKER_PASSWORD') ?: null;
$this->skipTlsTests = getenv('SKIP_TLS_TESTS') === 'true';
$this->tlsCertificateDirectory = rtrim(getenv('TLS_CERT_DIR'), '/');
}
}

View File

@ -0,0 +1,365 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\MessageProcessors;
use PhpMqtt\Client\ConnectionSettings;
use PhpMqtt\Client\Logger;
use PhpMqtt\Client\Subscription;
use PhpMqtt\Client\MessageProcessors\Mqtt311MessageProcessor;
use PHPUnit\Framework\TestCase;
class Mqtt311MessageProcessorTest extends TestCase
{
public const CLIENT_ID = 'test-client';
/** @var Mqtt311MessageProcessor */
protected $messageProcessor;
protected function setUp(): void
{
parent::setUp();
$this->messageProcessor = new Mqtt311MessageProcessor('test-client', new Logger('test.local', 1883, self::CLIENT_ID));
}
public function tryFindMessageInBuffer_testDataProvider(): array
{
return [
// No message and/or no knowledge about the remaining length of the message.
[hex2bin(''), false, null, null],
[hex2bin('20'), false, null, null],
// Incomplete message with knowledge about the remaining length of the message.
[hex2bin('2002'), false, null, 4],
[hex2bin('200200'), false, null, 4],
// Buffer contains only one complete message.
[hex2bin('20020000'), true, hex2bin('20020000'), null],
[hex2bin('800a0a03612f6201632f6402'), true, hex2bin('800a0a03612f6201632f6402'), null],
// Buffer contains more than one complete message.
[hex2bin('2002000044'), true, hex2bin('20020000'), null],
[hex2bin('4002000044'), true, hex2bin('40020000'), null],
[hex2bin('400200004412345678'), true, hex2bin('40020000'), null],
];
}
/**
* @dataProvider tryFindMessageInBuffer_testDataProvider
*
* @param string|null $expectedMessage
* @param int|null $expectedRequiredBytes
*/
public function test_tryFindMessageInBuffer_finds_messages_correctly(
string $buffer,
bool $expectedResult,
?string $expectedMessage,
?int $expectedRequiredBytes
): void
{
$message = null;
$requiredBytes = -1;
$result = $this->messageProcessor->tryFindMessageInBuffer($buffer, strlen($buffer), $message, $requiredBytes);
$this->assertEquals($expectedResult, $result);
$this->assertEquals($expectedMessage, $message);
if ($expectedRequiredBytes !== null) {
$this->assertEquals($expectedRequiredBytes, $requiredBytes);
} else {
$this->assertEquals(-1, $requiredBytes);
}
}
/**
* Message format:
*
* <fixed header><protocol name><protocol version><flags><keep alive><client id><will topic><will message><username><password>
*
* @return array[]
* @throws \Exception
*/
public function buildConnectMessage_testDataProvider(): array
{
return [
// Default parameters
[new ConnectionSettings(), false, hex2bin('101700044d5154540400000a000b') . self::CLIENT_ID],
// Clean Session
[new ConnectionSettings(), true, hex2bin('101700044d5154540402000a000b') . self::CLIENT_ID],
// Username, Password and Clean Session
[
(new ConnectionSettings())
->setUsername('foo')
->setPassword('bar'),
true,
hex2bin('102100044d51545404c2000a000b') . self::CLIENT_ID . hex2bin('0003') . 'foo' . hex2bin('0003') . 'bar',
],
// Last Will Topic, Last Will Message and Clean Session
[
(new ConnectionSettings())
->setLastWillTopic('test/foo')
->setLastWillMessage('bar')
->setLastWillQualityOfService(1),
true,
hex2bin('102600044d515454040e000a000b') . self::CLIENT_ID . hex2bin('0008') . 'test/foo' . hex2bin('0003') . 'bar',
],
// Last Will Topic, Last Will Message, Retain Last Will, Username, Password and Clean Session
[
(new ConnectionSettings())
->setLastWillTopic('test/foo')
->setLastWillMessage('bar')
->setLastWillQualityOfService(2)
->setRetainLastWill(true)
->setUsername('blub')
->setPassword('blubber'),
true,
hex2bin('103500044d51545404f6000a000b') . self::CLIENT_ID . hex2bin('0008') . 'test/foo' . hex2bin('0003') . 'bar'
. hex2bin('0004') . 'blub' . hex2bin('0007') . 'blubber',
],
];
}
/**
* @dataProvider buildConnectMessage_testDataProvider
*/
public function test_buildConnectMessage_builds_correct_message(
ConnectionSettings $connectionSettings,
bool $useCleanSession,
string $expectedResult
): void
{
$result = $this->messageProcessor->buildConnectMessage($connectionSettings, $useCleanSession);
$this->assertEquals($expectedResult, $result);
}
/**
* Message format:
*
* <fixed header><message id><topic><QoS>
*
* @return array[]
* @throws \Exception
*/
public function buildSubscribeMessage_testDataProvider(): array
{
$longTopic = random_bytes(130);
return [
// Simple QoS 0 subscription
[42, [new Subscription('test/foo', 0)], hex2bin('82'.'0d00'.'2a00'.'08') . 'test/foo' . hex2bin('00')],
// Wildcard QoS 2 subscription with high message id
[43764, [new Subscription('test/foo/bar/baz/#', 2)], hex2bin('82'.'17aa'.'f400'.'12') . 'test/foo/bar/baz/#' . hex2bin('02')],
// Long QoS 1 subscription with high message id
[62304, [new Subscription($longTopic, 1)], hex2bin('82'.'8701'.'f360'.'0082') . $longTopic . hex2bin('01')],
];
}
/**
* @dataProvider buildSubscribeMessage_testDataProvider
*
* @param Subscription[] $subscriptions
*/
public function test_buildSubscribeMessage_builds_correct_message(
int $messageId,
array $subscriptions,
string $expectedResult
): void
{
$result = $this->messageProcessor->buildSubscribeMessage($messageId, $subscriptions);
$this->assertEquals($expectedResult, $result);
}
/**
* Message format:
*
* <fixed header><message id><topic>
*
* @return array[]
* @throws \Exception
*/
public function buildUnsubscribeMessage_testDataProvider(): array
{
$longTopic = random_bytes(130);
return [
// Simple unsubscribe without duplicate
[42, ['test/foo'], false, hex2bin('a2'.'0c00'.'2a00'.'08') . 'test/foo'],
// Wildcard unsubscribe with high message id as duplicate
[43764, ['test/foo/bar/baz/#'], true, hex2bin('aa'.'16aa'.'f400'.'12') . 'test/foo/bar/baz/#'],
// Long unsubscribe with high message id as duplicate
[62304, [$longTopic], true, hex2bin('aa'.'8601'.'f360'.'0082') . $longTopic],
];
}
/**
* @dataProvider buildUnsubscribeMessage_testDataProvider
*
* @param string[] $topics
*/
public function test_buildUnsubscribeMessage_builds_correct_message(
int $messageId,
array $topics,
bool $isDuplicate,
string $expectedResult
): void
{
$result = $this->messageProcessor->buildUnsubscribeMessage($messageId, $topics, $isDuplicate);
$this->assertEquals($expectedResult, $result);
}
/**
* Message format:
*
* <fixed header><topic><message id><payload>
*
* @return array[]
* @throws \Exception
*/
public function buildPublishMessage_testDataProvider(): array
{
$longMessage = random_bytes(424242);
return [
// Simple QoS 0 publish
['test/foo', 'hello world', 0, false, 42, false, hex2bin('30'.'17'.'0008') . 'test/foo' . hex2bin('002a') . 'hello world'],
// Retained duplicate QoS 2 publish with long data and high message id
['test/foo', $longMessage, 2, true, 4242, true, hex2bin('3d'.'bef219'.'0008') . 'test/foo' . hex2bin('1092') . $longMessage],
];
}
/**
* @dataProvider buildPublishMessage_testDataProvider
*/
public function test_buildPublishMessage_builds_correct_message(
string $topic,
string $message,
int $qualityOfService,
bool $retain,
int $messageId,
bool $isDuplicate,
string $expectedResult
): void
{
$result = $this->messageProcessor->buildPublishMessage(
$topic,
$message,
$qualityOfService,
$retain,
$messageId,
$isDuplicate
);
$this->assertEquals($expectedResult, $result);
}
/**
* Message format:
*
* <fixed header><message id>
*
* @return array[]
* @throws \Exception
*/
public function buildPublishAcknowledgementMessage_testDataProvider(): array
{
return [
// Simple acknowledgement using small message id
[42, hex2bin('40'.'02'.'002a')],
// Simple acknowledgement using large message id
[4242, hex2bin('40'.'02'.'1092')],
];
}
/**
* @dataProvider buildPublishAcknowledgementMessage_testDataProvider
*/
public function test_buildPublishAcknowledgementMessage_builds_correct_message(int $messageId, string $expectedResult): void
{
$result = $this->messageProcessor->buildPublishAcknowledgementMessage($messageId);
$this->assertEquals($expectedResult, $result);
}
/**
* Message format:
*
* <fixed header><message id>
*
* @return array[]
* @throws \Exception
*/
public function buildPublishReceivedMessage_testDataProvider(): array
{
return [
// Simple acknowledgement using small message id
[42, hex2bin('50'.'02'.'002a')],
// Simple acknowledgement using large message id
[4242, hex2bin('50'.'02'.'1092')],
];
}
/**
* @dataProvider buildPublishReceivedMessage_testDataProvider
*/
public function test_buildPublishReceivedMessage_builds_correct_message(int $messageId, string $expectedResult): void
{
$result = $this->messageProcessor->buildPublishReceivedMessage($messageId);
$this->assertEquals($expectedResult, $result);
}
/**
* Message format:
*
* <fixed header><message id>
*
* @return array[]
* @throws \Exception
*/
public function buildPublishCompleteMessage_testDataProvider(): array
{
return [
// Simple acknowledgement using small message id
[42, hex2bin('70'.'02'.'002a')],
// Simple acknowledgement using large message id
[4242, hex2bin('70'.'02'.'1092')],
];
}
/**
* @dataProvider buildPublishCompleteMessage_testDataProvider
*/
public function test_buildPublishCompleteMessage_builds_correct_message(int $messageId, string $expectedResult): void
{
$result = $this->messageProcessor->buildPublishCompleteMessage($messageId);
$this->assertEquals($expectedResult, $result);
}
public function test_buildPingMessage_builds_correct_message(): void
{
$this->assertEquals(hex2bin('c000'), $this->messageProcessor->buildPingRequestMessage());
}
public function test_buildDisconnectMessage_builds_correct_message(): void
{
$this->assertEquals(hex2bin('e000'), $this->messageProcessor->buildDisconnectMessage());
}
}

View File

@ -0,0 +1,365 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\MessageProcessors;
use PhpMqtt\Client\ConnectionSettings;
use PhpMqtt\Client\Logger;
use PhpMqtt\Client\Subscription;
use PhpMqtt\Client\MessageProcessors\Mqtt31MessageProcessor;
use PHPUnit\Framework\TestCase;
class Mqtt31MessageProcessorTest extends TestCase
{
public const CLIENT_ID = 'test-client';
/** @var Mqtt31MessageProcessor */
protected $messageProcessor;
protected function setUp(): void
{
parent::setUp();
$this->messageProcessor = new Mqtt31MessageProcessor('test-client', new Logger('test.local', 1883, self::CLIENT_ID));
}
public function tryFindMessageInBuffer_testDataProvider(): array
{
return [
// No message and/or no knowledge about the remaining length of the message.
[hex2bin(''), false, null, null],
[hex2bin('20'), false, null, null],
// Incomplete message with knowledge about the remaining length of the message.
[hex2bin('2002'), false, null, 4],
[hex2bin('200200'), false, null, 4],
// Buffer contains only one complete message.
[hex2bin('20020000'), true, hex2bin('20020000'), null],
[hex2bin('800a0a03612f6201632f6402'), true, hex2bin('800a0a03612f6201632f6402'), null],
// Buffer contains more than one complete message.
[hex2bin('2002000044'), true, hex2bin('20020000'), null],
[hex2bin('4002000044'), true, hex2bin('40020000'), null],
[hex2bin('400200004412345678'), true, hex2bin('40020000'), null],
];
}
/**
* @dataProvider tryFindMessageInBuffer_testDataProvider
*
* @param string|null $expectedMessage
* @param int|null $expectedRequiredBytes
*/
public function test_tryFindMessageInBuffer_finds_messages_correctly(
string $buffer,
bool $expectedResult,
?string $expectedMessage,
?int $expectedRequiredBytes
): void
{
$message = null;
$requiredBytes = -1;
$result = $this->messageProcessor->tryFindMessageInBuffer($buffer, strlen($buffer), $message, $requiredBytes);
$this->assertEquals($expectedResult, $result);
$this->assertEquals($expectedMessage, $message);
if ($expectedRequiredBytes !== null) {
$this->assertEquals($expectedRequiredBytes, $requiredBytes);
} else {
$this->assertEquals(-1, $requiredBytes);
}
}
/**
* Message format:
*
* <fixed header><protocol name><protocol version><flags><keep alive><client id><will topic><will message><username><password>
*
* @return array[]
* @throws \Exception
*/
public function buildConnectMessage_testDataProvider(): array
{
return [
// Default parameters
[new ConnectionSettings(), false, hex2bin('101900064d51497364700300000a000b') . self::CLIENT_ID],
// Clean Session
[new ConnectionSettings(), true, hex2bin('101900064d51497364700302000a000b') . self::CLIENT_ID],
// Username, Password and Clean Session
[
(new ConnectionSettings())
->setUsername('foo')
->setPassword('bar'),
true,
hex2bin('102300064d514973647003c2000a000b') . self::CLIENT_ID . hex2bin('0003') . 'foo' . hex2bin('0003') . 'bar',
],
// Last Will Topic, Last Will Message and Clean Session
[
(new ConnectionSettings())
->setLastWillTopic('test/foo')
->setLastWillMessage('bar')
->setLastWillQualityOfService(1),
true,
hex2bin('102800064d5149736470030e000a000b') . self::CLIENT_ID . hex2bin('0008') . 'test/foo' . hex2bin('0003') . 'bar',
],
// Last Will Topic, Last Will Message, Retain Last Will, Username, Password and Clean Session
[
(new ConnectionSettings())
->setLastWillTopic('test/foo')
->setLastWillMessage('bar')
->setLastWillQualityOfService(2)
->setRetainLastWill(true)
->setUsername('blub')
->setPassword('blubber'),
true,
hex2bin('103700064d514973647003f6000a000b') . self::CLIENT_ID . hex2bin('0008') . 'test/foo' . hex2bin('0003') . 'bar'
. hex2bin('0004') . 'blub' . hex2bin('0007') . 'blubber',
],
];
}
/**
* @dataProvider buildConnectMessage_testDataProvider
*/
public function test_buildConnectMessage_builds_correct_message(
ConnectionSettings $connectionSettings,
bool $useCleanSession,
string $expectedResult
): void
{
$result = $this->messageProcessor->buildConnectMessage($connectionSettings, $useCleanSession);
$this->assertEquals($expectedResult, $result);
}
/**
* Message format:
*
* <fixed header><message id><topic><QoS>
*
* @return array[]
* @throws \Exception
*/
public function buildSubscribeMessage_testDataProvider(): array
{
$longTopic = random_bytes(130);
return [
// Simple QoS 0 subscription
[42, [new Subscription('test/foo', 0)], hex2bin('82'.'0d00'.'2a00'.'08') . 'test/foo' . hex2bin('00')],
// Wildcard QoS 2 subscription with high message id
[43764, [new Subscription('test/foo/bar/baz/#', 2)], hex2bin('82'.'17aa'.'f400'.'12') . 'test/foo/bar/baz/#' . hex2bin('02')],
// Long QoS 1 subscription with high message id
[62304, [new Subscription($longTopic, 1)], hex2bin('82'.'8701'.'f360'.'0082') . $longTopic . hex2bin('01')],
];
}
/**
* @dataProvider buildSubscribeMessage_testDataProvider
*
* @param Subscription[] $subscriptions
*/
public function test_buildSubscribeMessage_builds_correct_message(
int $messageId,
array $subscriptions,
string $expectedResult
): void
{
$result = $this->messageProcessor->buildSubscribeMessage($messageId, $subscriptions);
$this->assertEquals($expectedResult, $result);
}
/**
* Message format:
*
* <fixed header><message id><topic>
*
* @return array[]
* @throws \Exception
*/
public function buildUnsubscribeMessage_testDataProvider(): array
{
$longTopic = random_bytes(130);
return [
// Simple unsubscribe without duplicate
[42, ['test/foo'], false, hex2bin('a2'.'0c00'.'2a00'.'08') . 'test/foo'],
// Wildcard unsubscribe with high message id as duplicate
[43764, ['test/foo/bar/baz/#'], true, hex2bin('aa'.'16aa'.'f400'.'12') . 'test/foo/bar/baz/#'],
// Long unsubscribe with high message id as duplicate
[62304, [$longTopic], true, hex2bin('aa'.'8601'.'f360'.'0082') . $longTopic],
];
}
/**
* @dataProvider buildUnsubscribeMessage_testDataProvider
*
* @param string[] $topics
*/
public function test_buildUnsubscribeMessage_builds_correct_message(
int $messageId,
array $topics,
bool $isDuplicate,
string $expectedResult
): void
{
$result = $this->messageProcessor->buildUnsubscribeMessage($messageId, $topics, $isDuplicate);
$this->assertEquals($expectedResult, $result);
}
/**
* Message format:
*
* <fixed header><topic><message id><payload>
*
* @return array[]
* @throws \Exception
*/
public function buildPublishMessage_testDataProvider(): array
{
$longMessage = random_bytes(424242);
return [
// Simple QoS 0 publish
['test/foo', 'hello world', 0, false, 42, false, hex2bin('30'.'17'.'0008') . 'test/foo' . hex2bin('002a') . 'hello world'],
// Retained duplicate QoS 2 publish with long data and high message id
['test/foo', $longMessage, 2, true, 4242, true, hex2bin('3d'.'bef219'.'0008') . 'test/foo' . hex2bin('1092') . $longMessage],
];
}
/**
* @dataProvider buildPublishMessage_testDataProvider
*/
public function test_buildPublishMessage_builds_correct_message(
string $topic,
string $message,
int $qualityOfService,
bool $retain,
int $messageId,
bool $isDuplicate,
string $expectedResult
): void
{
$result = $this->messageProcessor->buildPublishMessage(
$topic,
$message,
$qualityOfService,
$retain,
$messageId,
$isDuplicate
);
$this->assertEquals($expectedResult, $result);
}
/**
* Message format:
*
* <fixed header><message id>
*
* @return array[]
* @throws \Exception
*/
public function buildPublishAcknowledgementMessage_testDataProvider(): array
{
return [
// Simple acknowledgement using small message id
[42, hex2bin('40'.'02'.'002a')],
// Simple acknowledgement using large message id
[4242, hex2bin('40'.'02'.'1092')],
];
}
/**
* @dataProvider buildPublishAcknowledgementMessage_testDataProvider
*/
public function test_buildPublishAcknowledgementMessage_builds_correct_message(int $messageId, string $expectedResult): void
{
$result = $this->messageProcessor->buildPublishAcknowledgementMessage($messageId);
$this->assertEquals($expectedResult, $result);
}
/**
* Message format:
*
* <fixed header><message id>
*
* @return array[]
* @throws \Exception
*/
public function buildPublishReceivedMessage_testDataProvider(): array
{
return [
// Simple acknowledgement using small message id
[42, hex2bin('50'.'02'.'002a')],
// Simple acknowledgement using large message id
[4242, hex2bin('50'.'02'.'1092')],
];
}
/**
* @dataProvider buildPublishReceivedMessage_testDataProvider
*/
public function test_buildPublishReceivedMessage_builds_correct_message(int $messageId, string $expectedResult): void
{
$result = $this->messageProcessor->buildPublishReceivedMessage($messageId);
$this->assertEquals($expectedResult, $result);
}
/**
* Message format:
*
* <fixed header><message id>
*
* @return array[]
* @throws \Exception
*/
public function buildPublishCompleteMessage_testDataProvider(): array
{
return [
// Simple acknowledgement using small message id
[42, hex2bin('70'.'02'.'002a')],
// Simple acknowledgement using large message id
[4242, hex2bin('70'.'02'.'1092')],
];
}
/**
* @dataProvider buildPublishCompleteMessage_testDataProvider
*/
public function test_buildPublishCompleteMessage_builds_correct_message(int $messageId, string $expectedResult): void
{
$result = $this->messageProcessor->buildPublishCompleteMessage($messageId);
$this->assertEquals($expectedResult, $result);
}
public function test_buildPingMessage_builds_correct_message(): void
{
$this->assertEquals(hex2bin('c000'), $this->messageProcessor->buildPingRequestMessage());
}
public function test_buildDisconnectMessage_builds_correct_message(): void
{
$this->assertEquals(hex2bin('e000'), $this->messageProcessor->buildDisconnectMessage());
}
}

2
vendor/services.php vendored
View File

@ -1,5 +1,5 @@
<?php <?php
// This file is automatically generated at:2023-11-23 11:23:43 // This file is automatically generated at:2024-01-21 11:50:25
declare (strict_types = 1); declare (strict_types = 1);
return array ( return array (
0 => 'think\\app\\Service', 0 => 'think\\app\\Service',