Інтеграція сторонніх HTTP-служб безпосередньо в дії контролера або обробники черг без рівня абстракції створює крихкі, некеровані кодові бази. При взаємодії з API Telegram Bot у додатку Yii2 необроблені виклики file_get_contents або розрізнені фрагменти cURL створюють критичні ризики безпеки, невідстежувані винятки API, необроблені обмеження швидкості (HTTP 429) та жорстко закодовані облікові дані.
У цьому уроці ми побудуємо готовий до використання у виробництві компонент TelegramClient для Yii2. Ця архітектура інкапсулює стандартні мережеві запити cURL, ізолює конфіденційні токени всередині локальних параметрів, безпосередньо інтегрується з контейнером Yii2 Dependency Injection (DI), автоматично обробляє затримки (back-offs) при обмеженні швидкості та реєструє збої API через базову інфраструктуру логування Yii2.
У цьому посібнику основна увага приділяється побудові вихідного транспортного рівня HTTP для кінцевих точок API Telegram. Він не охоплює міграцію бази даних, моделі Active Record або налаштування маршруту вебхука.
\n---
\nЗбереження облікових даних у params-local.php
\nЖорстке кодування облікових даних бота всередині компонентів додатка або історії комітів ставить під загрозу безпеку та порушує принципи ізоляції середовища. Yii2 надає модель розділеної конфігурації: базові значення знаходяться в config/params.php, тоді як перевизначення, залежні від середовища, живуть у config/params-local.php, які мають бути виключені з контролю версій через .gitignore.
Спочатку визначте ключ-заповнювач у config/params.php:
<?php
return [
'adminEmail' => 'admin@example.com',
'telegramBotToken' => '',
];\nПотім вставте свій фактичний токен бота, отриманий від @BotFather, у config/params-local.php:
<?php
return [
'telegramBotToken' => '1234567890:ABCdefGHIjklMNOpqrsTUVwxyZ123456789',
];\nЦе гарантує, що середовища розробки, тестування та виробництва підтримують окремі екземпляри ботів без зміни основної кодової бази.
\n---
\nПроектування компонента TelegramClient
\nКастомний компонент наслідує yii\base\Component для підключення до логіки ініціалізації об'єкта Yii2. Він керує ресурсами cURL, встановлює таймаути виконання, перевіряє корисне навантаження відповіді та обробляє тимчасові збої API.
Коли Telegram повертає код HTTP-статусу Too Many Requests (Забагато запитів), відповідь API містить об'єкт JSON із parameters.retry_after, який вказує, скільки секунд потрібно почекати перед повторною спробою. Компонент зчитує цю властивість і виконує обмежений цикл повторних спроб перед поверненням помилки.
Створіть components/TelegramClient.php:
<?php
namespace app\components;
use Yii;
use yii\base\Component;
use yii\base\InvalidConfigException;
use yii\base\Exception;
class TelegramClient extends Component
{
public string $botToken = '';
public int $timeout = 10;
public int $connectTimeout = 5;
public int $maxRetries = 3;
public function init(): void
{
parent::init();
if (empty($this->botToken)) {
throw new InvalidConfigException('The "botToken" property must be configured in TelegramClient.');
}
}
/**
* Executes a POST request to the Telegram Bot API.
*
* @param string $method API method (e.g., 'sendMessage', 'answerCallbackQuery')
* @param array $payload Key-value payload parameters
* @return array Decoded JSON response from Telegram
* @throws Exception If cURL encounters a network error or retries are exhausted
*/
public function sendRequest(string $method, array $payload = []): array
{
$url = sprintf('https://api.telegram.org/bot%s/%s', $this->botToken, $method);
$attempts = 0;
while ($attempts < $this->maxRetries) {
$attempts++;
$result = $this->executeCurl($url, $payload);
if ($result['status'] === 200 && isset($result['response']['ok']) && $result['response']['ok'] === true) {
return $result['response'];
}
// Check for Rate Limit (HTTP 429)
if ($result['status'] === 429 || (isset($result['response']['error_code']) && $result['response']['error_code'] === 429)) {
$retryAfter = $result['response']['parameters']['retry_after'] ?? 1;
Yii::warning(
sprintf('Telegram Rate Limit reached on %s. Attempt %d/%d. Waiting %d seconds.', $method, $attempts, $this->maxRetries, $retryAfter),
__METHOD__
);
if ($attempts < $this->maxRetries) {
sleep((int) $retryAfter);
continue;
}
}
// Log fatal error details if request failed completely
Yii::error(
sprintf(
"Telegram API Request Failed.
Method: %s
HTTP Status: %d
Payload: %s
Response: %s",
$method,
$result['status'],
json_encode($payload, JSON_UNESCAPED_UNICODE),
json_encode($result['response'], JSON_UNESCAPED_UNICODE)
),
__METHOD__
);
return $result['response'] ?? ['ok' => false, 'error_code' => $result['status'], 'description' => 'Unknown HTTP Error'];
}
throw new Exception(sprintf('Telegram API call "%s" failed after %d attempts.', $method, $this->maxRetries));
}
private function executeCurl(string $url, array $payload): array
{
$ch = curl_init();
$jsonPayload = json_encode($payload);
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $jsonPayload,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Content-Length: ' . strlen($jsonPayload),
],
CURLOPT_TIMEOUT => $this->timeout,
CURLOPT_CONNECTTIMEOUT => $this->connectTimeout,
CURLOPT_SSL_VERIFYPEER => true,
]);
$rawResponse = curl_exec($ch);
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlErrno = curl_errno($ch);
$curlError = curl_error($ch);
curl_close($ch);
if ($curlErrno !== 0) {
Yii::error(sprintf('cURL execution error (%d): %s', $curlErrno, $curlError), __METHOD__);
return [
'status' => 0,
'response' => ['ok' => false, 'description' => 'cURL Error: ' . $curlError],
];
}
$decoded = json_decode((string) $rawResponse, true);
if (json_last_error() !== JSON_ERROR_NONE) {
Yii::error(sprintf('Failed to parse Telegram JSON response: %s', json_last_error_msg()), __METHOD__);
return [
'status' => $httpCode,
'response' => ['ok' => false, 'description' => 'Malformed JSON response'],
];
}
return [
'status' => $httpCode,
'response' => $decoded,
];
}
}\n---
\nРеєстрація сервісу за допомогою Yii2 Dependency Injection
\nЩоб уникнути створення екземпляра класу вручну за допомогою new TelegramClient(), налаштуйте його всередині визначень контейнера Yii2. Це розділяє класи, дозволяє використовувати моки під час модульних тестів і централізує конфігурацію компонентів.
Відкрийте config/web.php (та config/console.php, якщо консольні воркери взаємодіють із Telegram) і зареєструйте клас під ключем container:
$params = require __DIR__ . '/params.php';
if (file_exists(__DIR__ . '/params-local.php')) {
$params = array_merge($params, require __DIR__ . '/params-local.php');
}
$config = [
'id' => 'basic',
'basePath' => dirname(__DIR__),
'components' => [
// Standard components...
],
'container' => [
'singletons' => [
\app\components\TelegramClient::class => [
'class' => \app\components\TelegramClient::class,
'botToken' => $params['telegramBotToken'],
'timeout' => 12,
'maxRetries' => 3,
],
],
],
'params' => $params,
];
return $config;\nВизначаючи TelegramClient всередині singletons, Yii2 створює екземпляр об'єкта один раз за життєвий цикл запиту на вимогу через Constructor Injection або Yii::$container->get().
---
\nВикористання TelegramClient у контролерах та сервісах
\nПісля налаштування впровадження залежностей ви можете впроваджувати TelegramClient безпосередньо в контролери, консольні команди або обробники завдань у черзі.
При рендерингу введення користувача всередині тіла повідомлення, відформатованого за допомогою HTML (parse_mode = 'HTML'), завжди пропускайте значення через htmlspecialchars для запобігання синтаксичним помилкам, викликаним неекранованими символами <, > або &.
Нижче наведено приклад дії контролера, що обробляє вихідні сповіщення:
\n<?php
namespace app\controllers;
use Yii;
use yii\web\Controller;
use yii\web\Response;
use app\components\TelegramClient;
class NotificationController extends Controller
{
private TelegramClient $telegram;
// Inject TelegramClient through Constructor Injection
public function __construct($id, $module, TelegramClient $telegram, $config = [])
{
$this->telegram = $telegram;
parent::__construct($id, $module, $config);
}
public function actionSendSystemAlert(string $chatId, string $userInputName): Response
{
// Sanitize untrusted input to avoid breaking Telegram HTML parser
$safeName = htmlspecialchars($userInputName, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
$messageText = sprintf(
"<b>System Notification</b>
User <i>%s</i> triggered a critical alert.",
$safeName
);
$payload = [
'chat_id' => $chatId,
'text' => $messageText,
'parse_mode' => 'HTML',
'disable_web_page_preview' => true,
];
$response = $this->telegram->sendRequest('sendMessage', $payload);
if (isset($response['ok']) && $response['ok'] === true) {
return $this->asJson([
'success' => true,
'message_id' => $response['result']['message_id'],
]);
}
return $this->asJson([
'success' => false,
'error' => $response['description'] ?? 'Failed to deliver Telegram message.',
]);
}
}\n---
\nТехнічні граничні випадки та експлуатаційні примітки
\n1. Запити зворотного виклику (Callback Requests): При роботі з інтерактивними вбудованими клавіатурами за допомогою вебхуків викликайте answerCallbackQuery негайно, щоб очистити стан завантаження на екрані користувача. Якщо процес вимагає фонових завдань, викличте answerCallbackQuery перед відправкою тривалих завдань. 2. Обмеження розміру корисного навантаження: Атрибут callback_data всередині об'єктів вбудованих кнопок не може перевищувати 64 байти. Не поміщайте всередину callback_data цілі об'єкти бази даних або складний JSON. Використовуйте короткі унікальні ідентифікатори стану, такі як шістнадцятково закодовані випадкові рядки або первинні ключі бази даних, і відновлюйте стан на своєму бекенді. 3. Розвантаження черги для великих обсягів: Хоча внутрішній цикл обробляє тимчасові коди стану Too Many Requests, синхронні веб-запити блокуватимуть роботу вашого веб-сервера під час тривалих затримок. Для масових сповіщень обробляйте повідомлення асинхронно через yii2tech/queue або yiisoft/yii2-queue за підтримки Redis або RabbitMQ. 4. Мережеві таймаути: Таймаути з'єднання (CURLOPT_CONNECTTIMEOUT) мають залишатися жорсткими (наприклад, 3–5 секунд), тоді як таймаути виконання (CURLOPT_TIMEOUT) мають бути встановлені трохи вище за очікувану затримку, щоб уникнути зависання PHP-процесів під час збоїв у Telegram.
---
\nПотрібна кастомна архітектура для складних вебхуків, черг обміну транзакційними повідомленнями або синхронізації станів міні-додатків? BotCreator створює готові до використання у виробництві інтеграції Telegram, виділені клієнти ботів та бекенди для сучасних веб-додатків.
" }