Integrating third-party HTTP services directly into controller actions or queue handlers without an abstraction layer creates fragile, unmaintainable codebases. When interacting with the Telegram Bot API in a Yii2 application, raw file_get_contents calls or scattered cURL snippets introduce critical security risks, untracked API exceptions, unhandled rate limits (HTTP 429), and hardcoded credentials.
In this tutorial, we will construct a production-ready TelegramClient component for Yii2. This architecture encapsulates standard cURL network requests, isolates sensitive tokens inside local parameters, integrates directly with the Yii2 Dependency Injection (DI) container, automatically handles rate-limiting backoffs, and logs API failures via Yii2's core logging infrastructure.
This guide focuses strictly on building the outbound HTTP transport layer for Telegram API endpoints. It does not cover database migrations, active record models, or webhook route setup.
Storing Credentials in params-local.php
Hardcoding bot credentials inside application components or commit history compromises security and violates environment isolation principles. Yii2 provides a split configuration model: base values reside in config/params.php, while environment-specific overrides live in config/params-local.php, which must be excluded from version control via .gitignore.
First, define a placeholder key in config/params.php:
<?php
return [
'adminEmail' => 'admin@example.com',
'telegramBotToken' => '',
];
Next, insert your actual bot token obtained from @BotFather into config/params-local.php:
<?php
return [
'telegramBotToken' => '1234567890:ABCdefGHIjklMNOpqrsTUVwxyZ123456789',
];
This ensures that development, staging, and production environments maintain distinct bot instances without modifying the main codebase.
Designing the TelegramClient Component
The custom component extends yii\base\Component to hook into Yii2 object initialization logic. It manages cURL resources, sets execution timeouts, inspects response payloads, and handles transient API failures.
When Telegram returns HTTP status code 429 Too Many Requests, the API response includes a JSON object with parameters.retry_after indicating how many seconds to wait before retrying. The component reads this property and executes a bounded retry loop before returning an error.
Create 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,
];
}
}
Registering the Service with Yii2 Dependency Injection
To avoid instantiating the class manually with new TelegramClient(), configure it inside the Yii2 container definitions. This decouples classes, allows mocking during unit tests, and centralizes component configuration.
Open config/web.php (and config/console.php if console workers interact with Telegram) and register the class under the container key:
$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;
By defining TelegramClient inside singletons, Yii2 instantiates the object once per request life-cycle when requested through Constructor Injection or Yii::$container->get().
Utilizing TelegramClient in Controllers and Services
With Dependency Injection configured, you can inject TelegramClient directly into controllers, console commands, or queued job handlers.
When rendering user input inside message bodies formatted with HTML (parse_mode = 'HTML'), always run values through htmlspecialchars() to prevent syntax errors caused by unescaped <, >, or & characters.
Here is an example controller action handling outbound notifications:
<?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.',
]);
}
}
Technical Edge Cases and Operational Notes
-
Callback Queries: When handling interactive inline keyboards via webhooks, call
answerCallbackQueryimmediately to clear the loading state on the user's screen. If the process requires background tasks, invokeanswerCallbackQuerybefore dispatching long-running jobs. -
Payload Size Limits: The
callback_dataattribute inside inline button objects cannot exceed 64 bytes. Do not place entire database entities or complex JSON insidecallback_data. Use short, unique state identifiers—such as hex-encoded random strings or database primary keys—and resolve state on your backend. -
Queue Offloading for High Volume: While the internal loop handles transient
429 Too Many Requestsstatus codes, synchronous web requests will block your web server workers during long backoffs. For bulk notifications, process messages asynchronously viayii2tech/queueoryiisoft/yii2-queuebacked by Redis or RabbitMQ. -
Network Timeouts: Connection timeouts (
CURLOPT_CONNECTTIMEOUT) should remain tight (e.g., 3–5 seconds), while execution timeouts (CURLOPT_TIMEOUT) should be set slightly higher than your expected latency, avoiding hung PHP processes during upstream Telegram outages.
Need custom architecture for complex webhooks, transactional messaging queues, or Mini App state synchronization? BotCreator builds production-ready Telegram integrations, dedicated bot clients, and backends for modern web applications.
Top comments (0)