diff --git a/.github/workflows/analysis.yaml b/.github/workflows/analysis.yaml index 7486ca9..e35cf74 100644 --- a/.github/workflows/analysis.yaml +++ b/.github/workflows/analysis.yaml @@ -13,7 +13,7 @@ jobs: os: - ubuntu-latest php: - - "8.1" + - "8.5" steps: - name: Checkout diff --git a/.github/workflows/cs.yaml b/.github/workflows/cs.yaml index 0c4584d..e7ab8e5 100644 --- a/.github/workflows/cs.yaml +++ b/.github/workflows/cs.yaml @@ -13,7 +13,7 @@ jobs: os: - ubuntu-latest php: - - "8.0" + - "8.5" steps: - name: Checkout diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 30f8e5f..425249b 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -18,6 +18,7 @@ jobs: - "8.2" - "8.3" - "8.4" + - "8.5" deps: - latest - lowest @@ -49,6 +50,10 @@ jobs: name: Update composer run: composer self-update + - + name: Remote tools + run: composer remove psalm/plugin-symfony vimeo/psalm friendsofphp/php-cs-fixer --dev + - name: Install dependencies with composer if: matrix.deps == 'latest' diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 074f5ea..0a91bf5 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -54,7 +54,9 @@ 'yoda_style' => false, 'declare_strict_types' => false, 'void_return' => false, - 'phpdoc_align' => [], + 'phpdoc_align' => [ + 'align' => 'left', + ], 'phpdoc_to_comment' => false, 'single_line_comment_spacing' => false, 'nullable_type_declaration_for_default_null_value' => true, diff --git a/CHANGELOG.md b/CHANGELOG.md index a5ca7c2..357adf7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,17 @@ CHANGELOG ========= +6.0.2 (2025-12-15) +------------------ + +* Add Symfony 8 support +* Add php 8.5 support + +6.0.1 (2025-07-10) +------------------ + +* Add php 8.4 support + 6.0.0 (2024-09-27) ------------------ diff --git a/composer.json b/composer.json index 3e91fd2..ccce56d 100644 --- a/composer.json +++ b/composer.json @@ -27,20 +27,20 @@ "php": "^8.0", "ext-curl": "*", "ext-json": "*", - "symfony/console": "^5.4 || ^6.0 || ^7.0", - "symfony/event-dispatcher": "^5.4 || ^6.0 || ^7.0", - "symfony/framework-bundle": "^5.4 || ^6.0 || ^7.0", + "symfony/console": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/event-dispatcher": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/framework-bundle": "^5.4 || ^6.0 || ^7.0 || ^8.0", "telegram-bot/api": "^2.3.14" }, "require-dev": { - "friendsofphp/php-cs-fixer": "~3.23.0", + "friendsofphp/php-cs-fixer": "~3.92.0", "symfony/phpunit-bridge": "^7.0.1", - "symfony/security-http": "^5.4 || ^6.0 || ^7.0", - "symfony/http-client": "^5.4 || ^6.0 || ^7.0", - "symfony/messenger": "^5.4 || ^6.0 || ^7.0", - "symfony/yaml": "^5.4 || ^6.0 || ^7.0", - "vimeo/psalm": "~4.30.0", - "psalm/plugin-symfony": "^4.0" + "symfony/security-http": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/http-client": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/messenger": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/yaml": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "vimeo/psalm": "~6.14.2", + "psalm/plugin-symfony": "^5.2.7" }, "suggest": { "symfony/security-guard": "Required to implement user authentication through Telegram", diff --git a/psalm.xml b/psalm.xml index 881c5e4..03ee220 100644 --- a/psalm.xml +++ b/psalm.xml @@ -13,14 +13,10 @@ - - - - - - - + + + diff --git a/src/Authenticator/TelegramAuthenticator.php b/src/Authenticator/TelegramAuthenticator.php index 225748c..f0c62c9 100644 --- a/src/Authenticator/TelegramAuthenticator.php +++ b/src/Authenticator/TelegramAuthenticator.php @@ -39,6 +39,7 @@ public function __construct( ) { } + #[\Override] public function supports(Request $request): ?bool { $route = $request->attributes->get('_route'); @@ -46,6 +47,7 @@ public function supports(Request $request): ?bool return $route === $this->guardRoute; } + #[\Override] public function authenticate(Request $request): Passport { $credentials = $request->query->all(); @@ -66,6 +68,7 @@ public function authenticate(Request $request): Passport })); } + #[\Override] public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response { if ($targetPath = $this->getTargetPath($request->getSession(), $firewallName)) { @@ -75,6 +78,7 @@ public function onAuthenticationSuccess(Request $request, TokenInterface $token, return new RedirectResponse($this->urlGenerator->generate($this->defaultTargetRoute)); } + #[\Override] public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response { if ($this->loginRoute) { diff --git a/src/BoShurikTelegramBotBundle.php b/src/BoShurikTelegramBotBundle.php index 0bc9136..2e3af96 100644 --- a/src/BoShurikTelegramBotBundle.php +++ b/src/BoShurikTelegramBotBundle.php @@ -19,6 +19,7 @@ final class BoShurikTelegramBotBundle extends Bundle { + #[\Override] public function build(ContainerBuilder $container): void { parent::build($container); @@ -26,12 +27,13 @@ public function build(ContainerBuilder $container): void $container->addCompilerPass(new CommandCompilerPass()); } + #[\Override] public function getContainerExtension(): ?ExtensionInterface { if (null === $this->extension) { $this->extension = new BoShurikTelegramBotExtension(); } - return $this->extension; + return $this->extension ?: null; } } diff --git a/src/Command/UpdatesCommand.php b/src/Command/UpdatesCommand.php index 17ec800..7e70088 100644 --- a/src/Command/UpdatesCommand.php +++ b/src/Command/UpdatesCommand.php @@ -24,6 +24,7 @@ public function __construct(private Telegram $telegram) parent::__construct(); } + #[\Override] protected function configure(): void { $this @@ -33,9 +34,9 @@ protected function configure(): void ; } + #[\Override] protected function execute(InputInterface $input, OutputInterface $output): int { - /** @var string|null $bot */ $bot = $input->getArgument('bot'); if ($bot) { $this->telegram->processUpdates($bot); diff --git a/src/Command/Webhook/InfoCommand.php b/src/Command/Webhook/InfoCommand.php index 9bf461e..7c153e5 100644 --- a/src/Command/Webhook/InfoCommand.php +++ b/src/Command/Webhook/InfoCommand.php @@ -26,6 +26,7 @@ public function __construct(private BotLocator $botLocator) parent::__construct(); } + #[\Override] protected function configure(): void { $this @@ -35,11 +36,11 @@ protected function configure(): void ; } + #[\Override] protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); - /** @var string|null $bot */ $bot = $input->getArgument('bot'); if ($bot) { $api = $this->botLocator->get($bot); @@ -56,7 +57,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int private function printWebhookInfo(SymfonyStyle $io, string $name, BotApi $api): void { - $io->block(sprintf('Bot "%s"', $name)); + $io->block(\sprintf('Bot "%s"', $name)); $info = $api->getWebhookInfo(); diff --git a/src/Command/Webhook/SetCommand.php b/src/Command/Webhook/SetCommand.php index e567647..c8b068f 100644 --- a/src/Command/Webhook/SetCommand.php +++ b/src/Command/Webhook/SetCommand.php @@ -34,6 +34,7 @@ public function __construct(private BotLocator $botLocator, private UrlGenerator parent::__construct(); } + #[\Override] protected function configure(): void { $this @@ -51,6 +52,7 @@ protected function configure(): void ; } + #[\Override] protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); @@ -58,15 +60,13 @@ protected function execute(InputInterface $input, OutputInterface $output): int $certificateFile = null; if ($certificate = $input->getArgument('certificate')) { if (!is_file($certificate) || !is_readable($certificate)) { - throw new \RuntimeException(sprintf('Can\'t read certificate file "%s"', $certificate)); + throw new \RuntimeException(\sprintf('Can\'t read certificate file "%s"', $certificate)); } $certificateFile = new \CURLFile($certificate); } - /** @var string|null $urlOrHostname */ $urlOrHostname = $input->getArgument('urlOrHostname'); - /** @var string|null $bot */ $bot = $input->getOption('bot'); $allowedUpdates = $input->getOption('allowedUpdateType'); @@ -108,7 +108,7 @@ private function setWebhook( ?\CURLFile $certificateFile = null, ?array $allowedUpdates = null ): bool { - $io->block(sprintf('Bot "%s"', $name)); + $io->block(\sprintf('Bot "%s"', $name)); if (!$urlOrHostname) { $url = $this->urlGenerator->generate('_telegram_bot_webhook', [ @@ -127,7 +127,7 @@ private function setWebhook( } catch (RouteNotFoundException $e) { $helpUrl = 'https://github.com/BoShurik/TelegramBotBundle#add-routing-for-webhook'; $message = "We could not find the webhook route. Read on\n%s\nhow to add the route or use symfony/flex."; - $io->block(messages: sprintf($message, $helpUrl), escape: false); + $io->block(messages: \sprintf($message, $helpUrl), escape: false); return false; } @@ -135,9 +135,9 @@ private function setWebhook( $url = $urlOrHostname; } - $api->setWebhook($url, $certificateFile, null, self::MAX_CONNECTIONS, json_encode($allowedUpdates)); + $api->setWebhook($url, $certificateFile, null, self::MAX_CONNECTIONS, (string) json_encode($allowedUpdates)); - $message = sprintf('Webhook URL has been set to %s', $url); + $message = \sprintf('Webhook URL has been set to %s', $url); $io->block($message, 'OK', 'fg=black;bg=green', ' ', true, false); return true; diff --git a/src/Command/Webhook/UnsetCommand.php b/src/Command/Webhook/UnsetCommand.php index c5b674d..b8b71b4 100644 --- a/src/Command/Webhook/UnsetCommand.php +++ b/src/Command/Webhook/UnsetCommand.php @@ -26,6 +26,7 @@ public function __construct(private BotLocator $botLocator) parent::__construct(); } + #[\Override] protected function configure(): void { $this @@ -35,11 +36,11 @@ protected function configure(): void ; } + #[\Override] protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); - /** @var string|null $bot */ $bot = $input->getArgument('bot'); if ($bot) { $api = $this->botLocator->get($bot); @@ -55,7 +56,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int private function deleteWebhook(SymfonyStyle $io, string $name, BotApi $api): void { - $io->block(sprintf('Bot "%s"', $name)); + $io->block(\sprintf('Bot "%s"', $name)); $api->deleteWebhook(); diff --git a/src/DependencyInjection/BoShurikTelegramBotExtension.php b/src/DependencyInjection/BoShurikTelegramBotExtension.php index 16ea721..83f435f 100644 --- a/src/DependencyInjection/BoShurikTelegramBotExtension.php +++ b/src/DependencyInjection/BoShurikTelegramBotExtension.php @@ -17,20 +17,24 @@ use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Extension\Extension; use Symfony\Component\DependencyInjection\Loader; use Symfony\Component\DependencyInjection\Parameter; use Symfony\Component\DependencyInjection\Reference; -use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\Contracts\HttpClient\HttpClientInterface as SymfonyHttpClientInterface; use TelegramBot\Api\BotApi; use TelegramBot\Api\Http\CurlHttpClient; use TelegramBot\Api\Http\HttpClientInterface; use TelegramBot\Api\Http\SymfonyHttpClient; +/** + * @psalm-suppress InternalClass + */ final class BoShurikTelegramBotExtension extends Extension { private const BOT_API_ID_TEMPLATE = 'boshurik_telegram_bot.api.bot.%s'; + #[\Override] public function load(array $configs, ContainerBuilder $container): void { $configuration = new Configuration(); @@ -45,25 +49,30 @@ public function load(array $configs, ContainerBuilder $container): void $defaultBot = $config['api']['default_bot']; + /** @psalm-suppress UndefinedClass */ if (interface_exists(HttpClientInterface::class)) { + /** @psalm-suppress UndefinedClass */ if (interface_exists(SymfonyHttpClientInterface::class)) { + /** @psalm-suppress UndefinedClass */ $httpClient = new Definition(SymfonyHttpClient::class, [ new Reference(SymfonyHttpClientInterface::class), ]); } else { + /** @psalm-suppress UndefinedClass */ $httpClient = new Definition(CurlHttpClient::class); $httpClient->addMethodCall('setProxy', [new Parameter('boshurik_telegram_bot.api.proxy')]); $httpClient->addMethodCall('setOption', [\CURLOPT_TIMEOUT, new Parameter('boshurik_telegram_bot.api.timeout')]); } + /** @psalm-suppress UndefinedClass */ $container->setDefinition(HttpClientInterface::class, $httpClient); } $bots = []; $registries = []; foreach ($config['api']['bots'] as $name => $bot) { - $botId = sprintf(self::BOT_API_ID_TEMPLATE, $name); - $registryId = sprintf(CommandCompilerPass::REGISTRY_ID_TEMPLATE, $name); + $botId = \sprintf(self::BOT_API_ID_TEMPLATE, $name); + $registryId = \sprintf(CommandCompilerPass::REGISTRY_ID_TEMPLATE, $name); $container ->setDefinition( @@ -123,6 +132,7 @@ public function load(array $configs, ContainerBuilder $container): void ; } + #[\Override] public function getAlias(): string { return 'boshurik_telegram_bot'; diff --git a/src/DependencyInjection/Compiler/CommandCompilerPass.php b/src/DependencyInjection/Compiler/CommandCompilerPass.php index 57df5c4..7e85cf2 100644 --- a/src/DependencyInjection/Compiler/CommandCompilerPass.php +++ b/src/DependencyInjection/Compiler/CommandCompilerPass.php @@ -26,6 +26,7 @@ final class CommandCompilerPass implements CompilerPassInterface public const REGISTRY_ID_TEMPLATE = 'boshurik_telegram_bot.command.registry.%s'; + #[\Override] public function process(ContainerBuilder $container): void { $commands = []; @@ -33,11 +34,11 @@ public function process(ContainerBuilder $container): void foreach ($this->findAndSortTaggedServices(new TaggedIteratorArgument(self::COMMAND_TAG), $container) as $command) { $definition = $container->getDefinition((string) $command); if (!$class = $definition->getClass()) { - throw new LogicException(sprintf('Unknown class for service "%s"', (string) $command)); + throw new LogicException(\sprintf('Unknown class for service "%s"', (string) $command)); } $interfaces = class_implements($class); if (!isset($interfaces[CommandInterface::class])) { - throw new LogicException(sprintf('Can\'t apply tag "%s" to %s class. It must implement %s interface', self::COMMAND_TAG, $class, CommandInterface::class)); + throw new LogicException(\sprintf('Can\'t apply tag "%s" to %s class. It must implement %s interface', self::COMMAND_TAG, $class, CommandInterface::class)); } $tags = $definition->getTag(self::COMMAND_TAG); diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 9f173af..0308d3e 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -17,13 +17,14 @@ final class Configuration implements ConfigurationInterface { + #[\Override] public function getConfigTreeBuilder(): TreeBuilder { $treeBuilder = new TreeBuilder('boshurik_telegram_bot'); /** @var ArrayNodeDefinition $rootNode */ $rootNode = $treeBuilder->getRootNode(); - /** @psalm-suppress all */ + /** @psalm-suppress PossiblyNullReference,UnusedForeachValue,PossiblyNullArrayOffset,UndefinedInterfaceMethod */ $rootNode ->children() ->arrayNode('api')->isRequired() diff --git a/src/EventListener/CommandListener.php b/src/EventListener/CommandListener.php index 3bd4f32..207e155 100644 --- a/src/EventListener/CommandListener.php +++ b/src/EventListener/CommandListener.php @@ -18,6 +18,7 @@ final class CommandListener implements EventSubscriberInterface { + #[\Override] public static function getSubscribedEvents(): array { return [ diff --git a/src/Telegram/BotLocator.php b/src/Telegram/BotLocator.php index 6561e97..f6b95e9 100644 --- a/src/Telegram/BotLocator.php +++ b/src/Telegram/BotLocator.php @@ -24,13 +24,15 @@ public function get(string $bot): BotApi { $api = $this->locator->get($bot); if (!$api instanceof BotApi) { - throw new \RuntimeException(sprintf('Expect "%s", instance of "%s" given', BotApi::class, $api::class)); + throw new \RuntimeException(\sprintf('Expect "%s", instance of "%s" given', BotApi::class, $api::class)); } return $api; } /** + * @psalm-suppress UnusedForeachValue + * * @return \Generator */ public function all(): \Generator diff --git a/src/Telegram/Command/AbstractCommand.php b/src/Telegram/Command/AbstractCommand.php index 3df5672..273c203 100644 --- a/src/Telegram/Command/AbstractCommand.php +++ b/src/Telegram/Command/AbstractCommand.php @@ -31,6 +31,7 @@ public function getAliases(): array return []; } + #[\Override] public function isApplicable(Update $update): bool { $callbackQuery = $update->getCallbackQuery(); diff --git a/src/Telegram/Command/HelpCommand.php b/src/Telegram/Command/HelpCommand.php index 49eea34..912c5ba 100644 --- a/src/Telegram/Command/HelpCommand.php +++ b/src/Telegram/Command/HelpCommand.php @@ -25,6 +25,7 @@ public function __construct( ) { } + #[\Override] public function execute(BotApi $api, Update $update): void { $commands = $this->commandRegistry->getCommands(); @@ -37,22 +38,25 @@ public function execute(BotApi $api, Update $update): void continue; } - $reply .= sprintf("%s - %s\n", $command->getName(), $command->getDescription()); + $reply .= \sprintf("%s - %s\n", $command->getName(), $command->getDescription()); } $api->sendMessage($message->getChat()->getId(), $reply); } + #[\Override] public function getName(): string { return '/help'; } + #[\Override] public function getAliases(): array { return $this->aliases; } + #[\Override] public function getDescription(): string { return $this->description; diff --git a/src/Telegram/Command/Registry/CommandRegistryLocator.php b/src/Telegram/Command/Registry/CommandRegistryLocator.php index d04652e..a5c6ae4 100644 --- a/src/Telegram/Command/Registry/CommandRegistryLocator.php +++ b/src/Telegram/Command/Registry/CommandRegistryLocator.php @@ -23,7 +23,7 @@ public function get(string $bot): CommandRegistry { $registry = $this->locator->get($bot); if (!$registry instanceof CommandRegistry) { - throw new \RuntimeException(sprintf('Expect "%s", instance of "%s" given', CommandRegistry::class, $registry::class)); + throw new \RuntimeException(\sprintf('Expect "%s", instance of "%s" given', CommandRegistry::class, $registry::class)); } return $registry; diff --git a/src/Telegram/Telegram.php b/src/Telegram/Telegram.php index b6ef212..e62bf2a 100644 --- a/src/Telegram/Telegram.php +++ b/src/Telegram/Telegram.php @@ -26,6 +26,9 @@ public function __construct( ) { } + /** + * @psalm-suppress UnusedForeachValue + */ public function processAllUpdates(): void { foreach ($this->botLocator->all() as $name => $id) { diff --git a/tests/EventListener/CommandListenerTest.php b/tests/EventListener/CommandListenerTest.php index afe2d24..346e613 100644 --- a/tests/EventListener/CommandListenerTest.php +++ b/tests/EventListener/CommandListenerTest.php @@ -45,7 +45,7 @@ public function testNotProcessedWhenCommandsIsNotApplicable(): void $event = new UpdateEvent('default', $update); $listener = $this->createListener([ - new class() implements CommandInterface { + new class implements CommandInterface { public function execute(BotApi $api, Update $update): void { } @@ -68,7 +68,7 @@ public function testProcessed(): void $event = new UpdateEvent('default', $update); $listener = $this->createListener([ - new class() implements CommandInterface { + new class implements CommandInterface { public function execute(BotApi $api, Update $update): void { } diff --git a/tests/Kernel/Multiple/MultipleTestKernel.php b/tests/Kernel/Multiple/MultipleTestKernel.php index 63d57a9..9899f57 100644 --- a/tests/Kernel/Multiple/MultipleTestKernel.php +++ b/tests/Kernel/Multiple/MultipleTestKernel.php @@ -35,7 +35,7 @@ public function registerContainerConfiguration(LoaderInterface $loader): void protected function build(ContainerBuilder $container): void { - $container->addCompilerPass(new class() implements CompilerPassInterface { + $container->addCompilerPass(new class implements CompilerPassInterface { public function process(ContainerBuilder $container): void { $router = $container->getDefinition('router.default'); diff --git a/tests/Kernel/Single/SingleTestKernel.php b/tests/Kernel/Single/SingleTestKernel.php index 8741a02..cca0aee 100644 --- a/tests/Kernel/Single/SingleTestKernel.php +++ b/tests/Kernel/Single/SingleTestKernel.php @@ -35,7 +35,7 @@ public function registerContainerConfiguration(LoaderInterface $loader): void protected function build(ContainerBuilder $container): void { - $container->addCompilerPass(new class() implements CompilerPassInterface { + $container->addCompilerPass(new class implements CompilerPassInterface { public function process(ContainerBuilder $container): void { $router = $container->getDefinition('router.default');