Skip to content

Commit 59a8436

Browse files
authored
Merge pull request #3 from BushlanovDev/dev
Refactoring update dispatchers Added onCommand method Added LongPollingHandler
2 parents 1b00bae + 9d56be2 commit 59a8436

9 files changed

+888
-751
lines changed

src/Api.php

Lines changed: 31 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
use BushlanovDev\MaxMessengerBot\Enums\UploadType;
1111
use BushlanovDev\MaxMessengerBot\Exceptions\ClientApiException;
1212
use BushlanovDev\MaxMessengerBot\Exceptions\NetworkException;
13-
use BushlanovDev\MaxMessengerBot\Exceptions\SecurityException;
1413
use BushlanovDev\MaxMessengerBot\Exceptions\SerializationException;
1514
use BushlanovDev\MaxMessengerBot\Models\AbstractModel;
1615
use BushlanovDev\MaxMessengerBot\Models\Attachments\Requests\AbstractAttachmentRequest;
@@ -31,12 +30,10 @@
3130
use BushlanovDev\MaxMessengerBot\Models\Result;
3231
use BushlanovDev\MaxMessengerBot\Models\Subscription;
3332
use BushlanovDev\MaxMessengerBot\Models\UpdateList;
34-
use BushlanovDev\MaxMessengerBot\Models\Updates\AbstractUpdate;
3533
use BushlanovDev\MaxMessengerBot\Models\UploadEndpoint;
3634
use BushlanovDev\MaxMessengerBot\Models\VideoAttachmentDetails;
3735
use InvalidArgumentException;
3836
use LogicException;
39-
use Psr\Http\Message\ServerRequestInterface;
4037
use Psr\Log\LoggerInterface;
4138
use Psr\Log\NullLogger;
4239
use ReflectionException;
@@ -83,13 +80,16 @@ class Api
8380

8481
private readonly LoggerInterface $logger;
8582

83+
private readonly UpdateDispatcher $updateDispatcher;
84+
8685
/**
8786
* Api constructor.
8887
*
8988
* @param string $accessToken Your bot's access token from @MasterBot.
9089
* @param ClientApiInterface|null $client Http api client.
91-
* @param ModelFactory|null $modelFactory
92-
* @param LoggerInterface|null $logger
90+
* @param ModelFactory|null $modelFactory The model factory.
91+
* @param LoggerInterface|null $logger PSR LoggerInterface.
92+
* @param UpdateDispatcher|null $updateDispatcher The update dispatcher.
9393
*
9494
* @throws InvalidArgumentException
9595
*/
@@ -98,6 +98,7 @@ public function __construct(
9898
?ClientApiInterface $client = null,
9999
?ModelFactory $modelFactory = null,
100100
?LoggerInterface $logger = null,
101+
?UpdateDispatcher $updateDispatcher = null,
101102
) {
102103
$this->logger = $logger ?? new NullLogger();
103104

@@ -129,6 +130,7 @@ public function __construct(
129130

130131
$this->client = $client;
131132
$this->modelFactory = $modelFactory ?? new ModelFactory();
133+
$this->updateDispatcher = $updateDispatcher ?? new UpdateDispatcher($this);
132134
}
133135

134136
/**
@@ -151,67 +153,45 @@ public function request(string $method, string $uri, array $queryParams = [], ar
151153
}
152154

153155
/**
154-
* Creates a WebhookHandler instance, pre-configured with the necessary dependencies.
155-
*
156-
* @param string|null $secret The secret key for request verification.
157-
* Should be the same one you used when calling the subscribe() method.
156+
* Gets the central update dispatcher instance. Use this to register your event and command handlers.
158157
*
159-
* @return WebhookHandler
158+
* @return UpdateDispatcher
159+
* @codeCoverageIgnore
160160
*/
161-
public function createWebhookHandler(?string $secret = null): WebhookHandler
161+
public function getUpdateDispatcher(): UpdateDispatcher
162162
{
163-
return new WebhookHandler($this, $this->modelFactory, $secret, $this->logger);
163+
return $this->updateDispatcher;
164164
}
165165

166166
/**
167-
* Parses an incoming webhook request and returns a single Update object.
168-
* This is an alternative to the event-driven WebhookHandler::handle() method,
169-
* allowing for manual processing of updates.
167+
* Creates a WebhookHandler instance, pre-configured with the necessary dependencies.
170168
*
171-
* @param string|null $secret The secret key to verify the request signature.
172-
* @param ServerRequestInterface|null $request The PSR-7 request object. If null, it's created from globals.
169+
* @param string|null $secret The secret key for request verification.
173170
*
174-
* @return AbstractUpdate The parsed update object (e.g., MessageCreatedUpdate).
175-
* @throws \ReflectionException
176-
* @throws SecurityException
177-
* @throws SerializationException
178-
* @throws \LogicException
171+
* @return WebhookHandler
179172
*/
180-
public function getWebhookUpdate(?string $secret = null, ?ServerRequestInterface $request = null): AbstractUpdate
173+
public function createWebhookHandler(?string $secret = null): WebhookHandler
181174
{
182-
return $this->createWebhookHandler($secret)->getUpdate($request);
175+
return new WebhookHandler(
176+
$this->updateDispatcher,
177+
$this->modelFactory,
178+
$this->logger,
179+
$secret,
180+
);
183181
}
184182

185183
/**
186-
* A simple way to process a single incoming webhook request using callbacks.
187-
* This method creates a WebhookHandler, registers the provided callbacks, and processes the request.
184+
* Creates a LongPollingHandler instance, pre-configured for running a long-polling loop.
188185
*
189-
* @param array<string, callable> $handlers An associative array where keys are UpdateType string values
190-
* (e.g., UpdateType::MessageCreated->value) and values are handlers.
191-
* @param string|null $secret The secret key for request verification.
192-
* @param ServerRequestInterface|null $request The PSR-7 request object.
193-
*
194-
* @throws SecurityException
195-
* @throws SerializationException
196-
* @throws ReflectionException
197-
* @throws LogicException
186+
* @return LongPollingHandler
198187
*/
199-
public function handleWebhooks(
200-
array $handlers,
201-
?string $secret = null,
202-
?ServerRequestInterface $request = null,
203-
): void {
204-
$webhookHandler = $this->createWebhookHandler($secret);
205-
206-
foreach ($handlers as $updateType => $callback) {
207-
$updateType = UpdateType::tryFrom($updateType);
208-
// @phpstan-ignore-next-line
209-
if ($updateType && is_callable($callback)) {
210-
$webhookHandler->addHandler($updateType, $callback);
211-
}
212-
}
213-
214-
$webhookHandler->handle($request);
188+
public function createLongPollingHandler(): LongPollingHandler
189+
{
190+
return new LongPollingHandler(
191+
$this,
192+
$this->updateDispatcher,
193+
$this->logger,
194+
);
215195
}
216196

217197
/**
@@ -251,64 +231,6 @@ public function getUpdates(
251231
);
252232
}
253233

254-
/**
255-
* Starts a long-polling loop to process updates using callbacks.
256-
* This method will run indefinitely until the script is terminated.
257-
*
258-
* @param array<string, callable> $handlers An associative array where keys are UpdateType enums
259-
* and values are the corresponding handler functions.
260-
* @param int|null $timeout Timeout in seconds for long polling (0-90). Defaults to 90.
261-
* @param int|null $marker Pass `null` to get updates you didn't get yet.
262-
*/
263-
public function handleUpdates(array $handlers, ?int $timeout = null, ?int $marker = null): void
264-
{
265-
// @phpstan-ignore-next-line
266-
while (true) {
267-
try {
268-
$this->processUpdatesBatch($handlers, $timeout, $marker);
269-
} catch (NetworkException $e) {
270-
$this->logger->error(
271-
'Long-polling network error: {message}',
272-
['message' => $e->getMessage(), 'exception' => $e],
273-
);
274-
sleep(5);
275-
} catch (\Exception $e) {
276-
$this->logger->error(
277-
'An error occurred during long-polling: {message}',
278-
['message' => $e->getMessage(), 'exception' => $e],
279-
);
280-
sleep(1);
281-
}
282-
}
283-
}
284-
285-
/**
286-
* Processes a single batch of updates. This is the core logic used by handleUpdates().
287-
* Useful for custom loop implementations or for testing.
288-
*
289-
* @param array<string, callable> $handlers An associative array of update handlers.
290-
* @param int|null $timeout Timeout for the getUpdates call.
291-
* @param int|null $marker The marker for which updates to fetch.
292-
*
293-
* @throws ClientApiException
294-
* @throws NetworkException
295-
* @throws ReflectionException
296-
* @throws SerializationException
297-
*/
298-
public function processUpdatesBatch(array $handlers, ?int $timeout, ?int &$marker = null): void
299-
{
300-
$updateList = $this->getUpdates(timeout: $timeout, marker: $marker);
301-
302-
foreach ($updateList->updates as $update) {
303-
$handler = $handlers[$update->updateType->value] ?? null;
304-
if ($handler) {
305-
$handler($update, $this);
306-
}
307-
}
308-
309-
$marker = $updateList->marker;
310-
}
311-
312234
/**
313235
* Information about the current bot, identified by an access token.
314236
*

src/LongPollingHandler.php

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace BushlanovDev\MaxMessengerBot;
6+
7+
use BushlanovDev\MaxMessengerBot\Exceptions\NetworkException;
8+
use Psr\Log\LoggerInterface;
9+
10+
/**
11+
* Handles receiving updates via long polling.
12+
*/
13+
final readonly class LongPollingHandler
14+
{
15+
/**
16+
* @param Api $api
17+
* @param UpdateDispatcher $dispatcher The update dispatcher.
18+
* @param LoggerInterface $logger PSR LoggerInterface.
19+
* @codeCoverageIgnore
20+
*/
21+
public function __construct(
22+
private Api $api,
23+
private UpdateDispatcher $dispatcher,
24+
private LoggerInterface $logger,
25+
) {
26+
if (!(\PHP_SAPI === 'cli')) {
27+
throw new \RuntimeException('LongPollingHandler can only be used in CLI mode.');
28+
}
29+
}
30+
31+
/**
32+
* Processes a single batch of updates. Useful for custom loop implementations or for testing.
33+
*
34+
* @param int $timeout Timeout for the getUpdates call.
35+
* @param int|null $marker The marker for which updates to fetch.
36+
* @return int|null The new marker to be used for the next iteration.
37+
* @throws \Exception Re-throws exceptions from the API or dispatcher.
38+
*/
39+
public function processUpdates(int $timeout, ?int $marker): ?int
40+
{
41+
$updateList = $this->api->getUpdates(timeout: $timeout, marker: $marker);
42+
43+
foreach ($updateList->updates as $update) {
44+
try {
45+
$this->dispatcher->dispatch($update);
46+
} catch (\Throwable $e) {
47+
$this->logger->error('Error dispatching update', [
48+
'message' => $e->getMessage(),
49+
'exception' => $e,
50+
]);
51+
}
52+
}
53+
54+
return $updateList->marker;
55+
}
56+
57+
/**
58+
* Starts a long-polling loop to process updates.
59+
* This method will run indefinitely until the script is terminated.
60+
*
61+
* @param int $timeout Timeout in seconds for long polling (0-90).
62+
* @param int|null $marker Initial marker. Pass `null` to get updates you didn't get yet.
63+
*/
64+
public function handle(int $timeout = 90, ?int $marker = null): void
65+
{
66+
$this->listenSignals();
67+
// @phpstan-ignore-next-line
68+
while (true) {
69+
try {
70+
$marker = $this->processUpdates($timeout, $marker);
71+
} catch (NetworkException $e) {
72+
$this->logger->error(
73+
'Long-polling network error: {message}',
74+
['message' => $e->getMessage(), 'exception' => $e],
75+
);
76+
sleep(5);
77+
} catch (\Exception $e) {
78+
$this->logger->error(
79+
'An error occurred during long-polling: {message}',
80+
['message' => $e->getMessage(), 'exception' => $e],
81+
);
82+
sleep(1);
83+
}
84+
}
85+
}
86+
87+
/**
88+
* @codeCoverageIgnore
89+
*/
90+
protected function listenSignals(): void
91+
{
92+
if (extension_loaded('pcntl')) {
93+
pcntl_async_signals(true);
94+
95+
$kill = static function () {
96+
exit(0);
97+
};
98+
99+
pcntl_signal(SIGINT, $kill);
100+
pcntl_signal(SIGQUIT, $kill);
101+
pcntl_signal(SIGTERM, $kill);
102+
}
103+
}
104+
}

0 commit comments

Comments
 (0)