Serve webhooks without amphp/http-server
When to use this
You already run nginx + php-fpm, RoadRunner, FrankenPHP, or any single-request PHP runtime — you don't want a second event-loop server next to it. feedWebhookUpdate
accepts an Update
directly, so any HTTP entry point can dispatch to the same handlers.
Solution
// public/webhook.php
require __DIR__ . '/../vendor/autoload.php';
use Gruven\PhpBotGram\Bot;
use Gruven\PhpBotGram\Dispatcher\Dispatcher;
$bot = new Bot(getenv('BOT_TOKEN'));
$dispatcher = require __DIR__ . '/../bootstrap.php'; // returns Dispatcher
$expected = getenv('WEBHOOK_SECRET');
$received = $_SERVER['HTTP_X_TELEGRAM_BOT_API_SECRET_TOKEN'] ?? '';
if ($expected !== '' && !hash_equals($expected, $received)) {
http_response_code(401);
exit;
}
$body = file_get_contents('php://input');
$payload = json_decode($body, true, flags: JSON_THROW_ON_ERROR);
$dispatcher->feedWebhookUpdate($bot, $payload);
http_response_code(200);
Dispatcher::feedWebhookUpdate accepts a raw decoded array or an Update
instance and runs the full dispatch chain.
For the amphp-http path, SimpleRequestHandler already wraps secret-token validation with hash_equals
; replicate the comparison manually in non-amphp deployments.
Pitfalls
hash_equalsis constant-time; a===comparison leaks timing information about the secret. Always usehash_equalsfor header checks.- Single-request runtimes (php-fpm, mod_php) tear down the dispatcher per request, so
MemoryStorageis useless — bind a persistent storage (Redis/Mongo/SQL) or FSM state vanishes between updates. - Telegram retries a 5xx response. If your handler may exceed 60 seconds, return 200 immediately and process in a queue — see Webhook for the fall-through model.