Skip to content

Commit fe65623

Browse files
author
darkdarin
committed
feat: base feature for execute commands
1 parent 60448b4 commit fe65623

12 files changed

+389
-4
lines changed

src/Commands/Attributes/Command.php

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace DarkDarin\TelegramBotSdk\Commands\Attributes;
4+
5+
#[\Attribute(\Attribute::TARGET_CLASS)]
6+
readonly class Command
7+
{
8+
public function __construct(
9+
public string $name,
10+
public ?string $description,
11+
) {
12+
}
13+
}
+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
namespace DarkDarin\TelegramBotSdk\Commands\Attributes;
4+
5+
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
6+
readonly class CommandAlias
7+
{
8+
public function __construct(
9+
public string $alias,
10+
) {
11+
}
12+
}
+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
namespace DarkDarin\TelegramBotSdk\Commands\Attributes;
4+
5+
#[\Attribute(\Attribute::TARGET_CLASS)]
6+
readonly class CommandPattern
7+
{
8+
public function __construct(
9+
public string $pattern
10+
) {
11+
}
12+
}

src/Commands/CommandHandler.php

+168
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
<?php
2+
3+
namespace DarkDarin\TelegramBotSdk\Commands;
4+
5+
use DarkDarin\TelegramBotSdk\DTO\Message;
6+
use DarkDarin\TelegramBotSdk\DTO\MessageEntityTypeEnum;
7+
use DarkDarin\TelegramBotSdk\Exceptions\TelegramException;
8+
use DarkDarin\TelegramBotSdk\Telegram;
9+
use DarkDarin\TelegramBotSdk\TelegramClient;
10+
use Psr\Container\ContainerExceptionInterface;
11+
use Psr\Container\ContainerInterface;
12+
use Psr\Container\NotFoundExceptionInterface;
13+
14+
class CommandHandler implements CommandHandlerInterface
15+
{
16+
private array $commands = [];
17+
18+
public function __construct(
19+
private readonly Telegram $telegram,
20+
private readonly ContainerInterface $container,
21+
) {
22+
}
23+
24+
public function registerCommand(string $botName, object $command): void
25+
{
26+
if (!method_exists($command, 'handle')) {
27+
throw new \InvalidArgumentException('Command must have method [handle]');
28+
}
29+
30+
$commandWrapper = new CommandWrapper($command);
31+
$this->commands[$botName][$commandWrapper->getName()][] = $commandWrapper;
32+
foreach ($commandWrapper->getAliases() as $alias) {
33+
$this->commands[$botName][$alias][] = $commandWrapper;
34+
}
35+
}
36+
37+
/**
38+
* @throws ContainerExceptionInterface
39+
* @throws NotFoundExceptionInterface
40+
* @throws \ReflectionException
41+
*/
42+
public function handle(string $botName, Message $message): void
43+
{
44+
$command = $this->findCommand($message);
45+
46+
if ($command === null || empty($this->commands[$botName][$command->command])) {
47+
return;
48+
}
49+
foreach ($this->commands[$botName][$command->command] as $commandHandler) {
50+
$arguments = $this->parseArguments($commandHandler, $command);
51+
$this->handleCommand($botName, $message, $commandHandler, $arguments);
52+
}
53+
}
54+
55+
private function findCommand(Message $message): ?MessageCommand
56+
{
57+
if ($message->entities === null) {
58+
return null;
59+
}
60+
61+
foreach ($message->entities as $entity) {
62+
if ($entity->type === MessageEntityTypeEnum::BotCommand) {
63+
$command = substr($message->text, $entity->offset + 1, $entity->length - 1);
64+
$arguments = substr($message->text, $entity->offset + $entity->length + 1);
65+
return new MessageCommand($command, $arguments);
66+
}
67+
}
68+
69+
return null;
70+
}
71+
72+
public function parseArguments(CommandWrapper $commandWrapper, MessageCommand $messageCommand): array
73+
{
74+
$pattern = $commandWrapper->getPattern();
75+
[$regex, $parameters] = $this->makeRegexPattern($pattern);
76+
preg_match("%{$regex}%ixmu", $messageCommand->arguments, $matches, PREG_UNMATCHED_AS_NULL);
77+
78+
$result = [];
79+
foreach ($matches as $key => $value) {
80+
if (in_array($key, $parameters)) {
81+
$result[$key] = $value;
82+
}
83+
}
84+
85+
return array_filter($result);
86+
}
87+
88+
private function makeRegexPattern(string $pattern): array
89+
{
90+
preg_match_all(
91+
pattern: '#\{\s*(?<name>\w+)\s*(?::\s*(?<pattern>\S+)\s*)?}#ixmu',
92+
subject: $pattern,
93+
matches: $matches,
94+
flags: PREG_SET_ORDER
95+
);
96+
97+
$patterns = collect($matches)
98+
->mapWithKeys(function ($match): array {
99+
$pattern = $match['pattern'] ?? '[^ ]++';
100+
101+
return [
102+
$match['name'] => "(?<{$match['name']}>{$pattern})?",
103+
];
104+
})
105+
->filter();
106+
107+
return [
108+
$patterns->implode('\s*'),
109+
$patterns->keys()->all(),
110+
];
111+
}
112+
113+
/**
114+
* @throws ContainerExceptionInterface
115+
* @throws NotFoundExceptionInterface
116+
* @throws \ReflectionException
117+
*/
118+
private function handleCommand(string $botName, Message $message, CommandWrapper $commandWrapper, array $arguments): void
119+
{
120+
$command = $commandWrapper->getCommand();
121+
$parameters = $this->getMethodParameters($botName, $message, $command, $arguments);
122+
123+
$command->handle(...$parameters);
124+
}
125+
126+
/**
127+
* @throws ContainerExceptionInterface
128+
* @throws NotFoundExceptionInterface
129+
* @throws \ReflectionException
130+
*/
131+
private function getMethodParameters(string $botName, Message $message, object $command, array $arguments): array
132+
{
133+
$parameters = [];
134+
$methodReflection = new \ReflectionMethod($command, 'handle');
135+
foreach ($methodReflection->getParameters() as $parameter) {
136+
if (array_key_exists($parameter->getName(), $arguments)) {
137+
$parameters[$parameter->getName()] = $arguments[$parameter->getName()];
138+
continue;
139+
}
140+
141+
if ($parameter->getType() instanceof \ReflectionNamedType) {
142+
$typeName = $parameter->getType()->getName();
143+
if ($typeName === TelegramClient::class) {
144+
$parameters[$parameter->getName()] = $this->telegram->bot($botName);
145+
continue;
146+
} elseif($typeName === Message::class) {
147+
$parameters[$parameter->getName()] = $message;
148+
continue;
149+
} elseif ($this->container->has($typeName)) {
150+
$parameters[$parameter->getName()] = $this->container->get($typeName);
151+
continue;
152+
}
153+
}
154+
155+
if (!$parameter->isDefaultValueAvailable()) {
156+
throw new TelegramException(
157+
sprintf(
158+
'Command [%s] expects parameter [%s], but it not declared',
159+
get_class($command),
160+
$parameter->getName()
161+
)
162+
);
163+
}
164+
}
165+
166+
return $parameters;
167+
}
168+
}
+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
namespace DarkDarin\TelegramBotSdk\Commands;
4+
5+
use DarkDarin\TelegramBotSdk\DTO\Message;
6+
7+
interface CommandHandlerInterface
8+
{
9+
public function registerCommand(string $botName, object $command): void;
10+
11+
public function handle(string $botName, Message $message): void;
12+
}

src/Commands/CommandWrapper.php

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
3+
namespace DarkDarin\TelegramBotSdk\Commands;
4+
5+
use DarkDarin\TelegramBotSdk\Commands\Attributes\Command;
6+
use DarkDarin\TelegramBotSdk\Commands\Attributes\CommandAlias;
7+
use DarkDarin\TelegramBotSdk\Commands\Attributes\CommandPattern;
8+
9+
class CommandWrapper
10+
{
11+
private string $name;
12+
private array $aliases = [];
13+
private ?string $pattern = null;
14+
private ?string $description = null;
15+
16+
public function __construct(
17+
private readonly object $command
18+
) {
19+
$this->parseCommand();
20+
}
21+
22+
public function getName(): string
23+
{
24+
return $this->name;
25+
}
26+
27+
public function getAliases(): array
28+
{
29+
return $this->aliases;
30+
}
31+
32+
public function getPattern(): ?string
33+
{
34+
return $this->pattern;
35+
}
36+
37+
public function getDescription(): ?string
38+
{
39+
return $this->description;
40+
}
41+
42+
public function getCommand(): object
43+
{
44+
return $this->command;
45+
}
46+
47+
private function parseCommand(): void
48+
{
49+
$reflection = new \ReflectionClass($this->command);
50+
51+
$this->name = get_class($this->command);
52+
53+
$attributes = $reflection->getAttributes();
54+
foreach ($attributes as $attribute) {
55+
$attributeInstance = $attribute->newInstance();
56+
if ($attributeInstance instanceof Command) {
57+
$this->name = $attributeInstance->name;
58+
$this->description = $attributeInstance->description;
59+
}
60+
if ($attributeInstance instanceof CommandAlias) {
61+
$this->aliases[] = $attributeInstance->alias;
62+
}
63+
if ($attributeInstance instanceof CommandPattern) {
64+
$this->pattern = $attributeInstance->pattern;
65+
}
66+
}
67+
}
68+
}

src/Commands/MessageCommand.php

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
namespace DarkDarin\TelegramBotSdk\Commands;
4+
5+
readonly class MessageCommand
6+
{
7+
public function __construct(
8+
public string $command,
9+
public string $arguments,
10+
) {
11+
}
12+
}

src/ConfigProvider.php

+6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace DarkDarin\TelegramBotSdk;
44

5+
use DarkDarin\TelegramBotSdk\Commands\CommandHandlerInterface;
6+
use DarkDarin\TelegramBotSdk\Factories\CommandHandlerFactory;
57
use DarkDarin\TelegramBotSdk\Factories\PsrClientFactory;
68
use DarkDarin\TelegramBotSdk\Factories\PsrRequestFactoryFactory;
79
use DarkDarin\TelegramBotSdk\Factories\PsrResponseFactoryFactory;
@@ -14,6 +16,9 @@
1416
use Psr\Http\Message\ResponseFactoryInterface;
1517
use Psr\Http\Message\StreamFactoryInterface;
1618

19+
/**
20+
* @psalm-api
21+
*/
1722
class ConfigProvider
1823
{
1924
public function __invoke(): array
@@ -26,6 +31,7 @@ public function __invoke(): array
2631
StreamFactoryInterface::class => PsrStreamFactoryFactory::class,
2732
TransportClientInterface::class => TransportClient::class,
2833
Telegram::class => TelegramFactory::class,
34+
CommandHandlerInterface::class => CommandHandlerFactory::class,
2935
],
3036
'publish' => [
3137
[
+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
namespace DarkDarin\TelegramBotSdk\Factories;
4+
5+
use DarkDarin\TelegramBotSdk\Commands\CommandHandler;
6+
use DarkDarin\TelegramBotSdk\Commands\CommandHandlerInterface;
7+
use Hyperf\Contract\ConfigInterface;
8+
use Hyperf\Contract\ContainerInterface;
9+
use Psr\Container\ContainerExceptionInterface;
10+
use Psr\Container\NotFoundExceptionInterface;
11+
12+
class CommandHandlerFactory
13+
{
14+
/**
15+
* @throws ContainerExceptionInterface
16+
* @throws NotFoundExceptionInterface
17+
*/
18+
public function __invoke(ContainerInterface $container): CommandHandlerInterface
19+
{
20+
$config = $container->get(ConfigInterface::class);
21+
22+
$bots = $config->get('telegram.bots', []);
23+
/** @var CommandHandler $commandHandler */
24+
$commandHandler = $container->make(CommandHandler::class);
25+
26+
foreach ($bots as $botName => $config) {
27+
if (!empty($config['commands'])) {
28+
foreach ($config['commands'] as $command) {
29+
$commandHandler->registerCommand($botName, $container->get($command));
30+
}
31+
}
32+
}
33+
34+
return $commandHandler;
35+
}
36+
}

0 commit comments

Comments
 (0)