Skip to content

Commit

Permalink
Add connected event (handler) (#152)
Browse files Browse the repository at this point in the history
* Added an connected event (handler list)

* Prevent adding duplicated subscriptions

* Fix phpcs

* Pass info to connected event handlers if is auto-reconnect

* Prevent double subscriptions in MemoryRepository

* Add tests for the new connected event handlers

* Revert "Fix phpcs"

This reverts commit 10a9d42.

* Remove trailing whitespace

---------

Co-authored-by: Marvin Mall <[email protected]>
  • Loading branch information
UtechtDustin and Namoshek authored Feb 10, 2023
1 parent 57b554d commit 99967b4
Show file tree
Hide file tree
Showing 6 changed files with 206 additions and 10 deletions.
77 changes: 77 additions & 0 deletions src/Concerns/OffersHooks.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ trait OffersHooks
/** @var \SplObjectStorage|array<\Closure> */
private $messageReceivedEventHandlers;

/** @var \SplObjectStorage|array<\Closure> */
private $connectedEventHandlers;

/**
* Needs to be called in order to initialize the trait.
*
Expand All @@ -33,6 +36,7 @@ protected function initializeEventHandlers(): void
$this->loopEventHandlers = new \SplObjectStorage();
$this->publishEventHandlers = new \SplObjectStorage();
$this->messageReceivedEventHandlers = new \SplObjectStorage();
$this->connectedEventHandlers = new \SplObjectStorage();
}

/**
Expand Down Expand Up @@ -266,4 +270,77 @@ private function runMessageReceivedEventHandlers(string $topic, string $message,
}
}
}

/**
* 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.
*
* @param \Closure $callback
* @return MqttClient
*/
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.
*
* @param \Closure|null $callback
* @return MqttClient
*/
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.
*
* @param bool $isAutoReconnect
* @return void
*/
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]);
}
}
}
}
4 changes: 2 additions & 2 deletions src/ConnectionSettings.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@

/**
* The settings used during connection to a broker.
*
* This class is immutable and all setters return a clone of the original class because
*
* 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
Expand Down
7 changes: 5 additions & 2 deletions src/MqttClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -148,10 +148,11 @@ public function connect(
* Connect to the MQTT broker using the configured settings.
*
* @param bool $useCleanSession
* @param bool $isAutoReconnect
* @return void
* @throws ConnectingToBrokerFailedException
*/
protected function connectInternal(bool $useCleanSession = false): void
protected function connectInternal(bool $useCleanSession = false, bool $isAutoReconnect = false): void
{
try {
$this->establishSocketConnection();
Expand All @@ -163,6 +164,8 @@ protected function connectInternal(bool $useCleanSession = false): void
}

$this->connected = true;

$this->runConnectedEventHandlers($isAutoReconnect);
}

/**
Expand Down Expand Up @@ -414,7 +417,7 @@ protected function reconnect(): void

for ($i = 1; $i <= $maxReconnectAttempts; $i++) {
try {
$this->connectInternal();
$this->connectInternal(false, true);

return;
} catch (ConnectingToBrokerFailedException $e) {
Expand Down
10 changes: 5 additions & 5 deletions src/Repositories/MemoryRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,9 @@ public function countSubscriptions(): int
*/
public function addSubscription(Subscription $subscription): void
{
// Remove a potentially existing subscription for this topic filter.
$this->removeSubscription($subscription->getTopicFilter());

$this->subscriptions[] = $subscription;
}

Expand All @@ -217,16 +220,13 @@ public function getSubscriptionsMatchingTopic(string $topicName): array
*/
public function removeSubscription(string $topicFilter): bool
{
$result = false;

foreach ($this->subscriptions as $index => $subscription) {
if ($subscription->getTopicFilter() === $topicFilter) {
unset($this->subscriptions[$index]);
$result = true;
break;
return true;
}
}

return $result;
return false;
}
}
116 changes: 116 additions & 0 deletions tests/Feature/ConnectedEventHandlerTest.php
Original file line number Diff line number Diff line change
@@ -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();
}
}
2 changes: 1 addition & 1 deletion tests/Feature/PublishEventHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
use Tests\TestCase;

/**
* Tests that the loop event handler work as intended.
* Tests that the publish event handler work as intended.
*
* @package Tests\Feature
*/
Expand Down

0 comments on commit 99967b4

Please sign in to comment.