govorun/framework 问题修复 & 功能扩展

解决BUG、新增功能、兼容多环境部署,快速响应你的开发需求

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

govorun/framework

Composer 安装命令:

composer require govorun/framework

包简介

Multi-messenger bot framework for PHP

README 文档

README

Мульти-мессенджер бот-фреймворк на PHP 8.3+. Один код — разные мессенджеры (на сегодня поддерживается Telegram; интерфейсы готовы под Viber/WhatsApp). Используется визуальным билдером govorun-factory как целевой рантайм для сгенерированных проектов.

v3.x (breaking, краткая шпаргалка по миграции с v1.x):

  • Keyboard::button() / ->row() удалены. Только Keyboard::make()->buttons([[Button::make('…')->action('…'), …], …]).
  • Button::make(...) + fluent: ->action(), ->url(), ->requestContact(), ->requestLocation().
  • Keyboard::reply()->resize()->oneTime() — fluent-флаги для reply-клавиатуры.
  • Step::ask(string|OutgoingMessage $msg, Closure|Keyboard|null $keyboard) — клавиатуру можно передавать напрямую.
  • В Controller и Flow подмешан трейт MakesHttpCalls$this->http()->connection('slug')->....

Быстрый старт

composer create-project govorun/skeleton my-bot
cd my-bot

Положите токен в .env:

TELEGRAM_BOT_TOKEN=your-token

Установите вебхук:

php govorun webhook:install

Подробный пользовательский гид — govorun-skeleton. Если бот сгенерирован фабрикой — просто разверните ZIP, composer install, заполните .env и php govorun webhook:install.

Архитектура

Жизненный цикл запроса

HTTP POST → public/index.php → Application::handleWebhook()
  1. loadEnvironment()             — загрузка .env
  2. loadConfiguration()           — загрузка config/*.php
  3. registerCoreProviders()       — EventServiceProvider, LogServiceProvider, StateServiceProvider
  4. registerConfiguredProviders() — провайдеры из config('app.providers')
  5. boot()                        — boot всех провайдеров
  6. loadRoutes()                  — routes/messenger.php
  7. resolveDriverName()           — драйвер из URL
  8. resolveDriver()               — экземпляр драйвера
  9. verifyWebhook()               — проверка подписи
 10. parseUpdate()                 — парсинг в IncomingMessage
 11. FlowHandler::handle()         — возобновление активного Flow (если есть)
 12. Router::dispatch()            — диспатч в Controller или Flow-старт

Структура пакета

src/
├── Console/            CLI: make:controller, make:flow, make:api-client,
│                       migrate, state:clear, test, webhook:install/remove,
│                       bot:profile-sync
├── Contracts/          MessengerDriver, StateStorage, StateAccessor
├── Database/Migrations CreateGovorunStatesTable
├── Drivers/Telegram/   TelegramDriver (URL и локальный файл через multipart)
├── Events/             EventServiceProvider
├── Exceptions/         SendFailedException, ApiException
├── Foundation/         Application (Illuminate Container), ServiceProvider
├── Http/               Request, ApiClient (abstract), HttpManager,
│                       ConnectionClient, HttpResponse, MakesHttpCalls trait
├── Log/                LogServiceProvider, Log facade
├── Messaging/          IncomingMessage, OutgoingMessage, Message, Button,
│                       Keyboard, Media + Dto/{User,Media,Location,Contact}
├── Routing/            Route, Router, Controller, Middleware, MiddlewarePipeline
├── State/              Flow, Step, FlowHandler, StateData, PersistentState,
│                       File/Database/CacheStateStorage, StateServiceProvider
├── Support/            helpers.php, Validator
└── Testing/            TestCase, FakeMessenger, FakeDriver, FakeApiClient, traits

Application

Ядро. Наследует Illuminate\Container\Container.

$app = new Application(dirname(__DIR__));
Метод Описание
basePath($path) Базовый путь проекта
configPath($path) Путь к config/
storagePath($path) Путь к storage/
databasePath($path) Путь к database/
register(ServiceProvider) Зарегистрировать провайдер
boot() Загрузить все провайдеры
handleConsole() Обработать CLI-запрос
handleWebhook(Request) Обработать вебхук
loadRoutes() Загрузить routes/messenger.php

Маршрутизация

DSL

use Govorun\Routing\Route;

Route::command('start', StartController::class);
Route::phrase('привет', HelloController::class);
Route::pattern('/^\d+$/', NumberController::class);
Route::action('confirm', ConfirmController::class);
Route::event('member_joined', WelcomeController::class);
Route::media('photo', PhotoController::class);
Route::location(LocationController::class);
Route::contact(ContactController::class);
Route::referral('promo', PromoController::class);
Route::fallback(FallbackController::class);

Route::command('start', ...) нормализуется к /start — слеш можно опускать.

Приоритет

event > command > action > referral > media > location > contact > pattern > phrase > fallback

Middleware

Route::middleware(AuthMiddleware::class, function () {
    Route::command('admin', AdminController::class);
});

Вложенные phrase

Route::phrase('меню', function () {
    Route::phrase('цены', PriceController::class);
    Route::phrase('контакты', ContactInfoController::class);
});

Алиасы

Route::phrase('привет', HelloController::class)
    ->alias(['здравствуйте', 'добрый день']);

Controller

Базовый класс контроллера бота.

use Govorun\Routing\Controller;

class StartController extends Controller
{
    public function handle(): void
    {
        $name = $this->message->user->firstName;
        $this->reply("Привет, {$name}!");
    }
}

handle() вызывается без аргументов; альтернатива — __invoke().

Свойство / метод Тип Описание
$this->message IncomingMessage Входящее сообщение (предпочтительный доступ; $this->incomingMessage — deprecated алиас)
$this->driver MessengerDriver Драйвер мессенджера
$this->state StateAccessor PersistentState (write-through в storage), при отсутствии storage — пустая StateData
reply(string $text) void Отправить текстовый ответ
send(OutgoingMessage $msg) void Отправить сообщение (с клавиатурой/медиа)
user() UserDto Данные отправителя
param(string $key) ?string Параметр callback-действия (для Route::action())
startFlow(string $class) void Запустить Flow-диалог
http() (через MakesHttpCalls) HttpManager Доступ к подключениям ($this->http()->connection('slug')->...)

Auto-finalize inline-клавиатуры

При отправке через send() сообщения с inline-клавиатурой, содержащей action-кнопки, контроллер сохраняет в storage контекст (message_id, original_text, parse_mode, текстовые лейблы кнопок). На следующем Action-сообщении исходное сообщение редактируется — клавиатура убирается, к тексту дописывается (выбрано: <label>). Ошибки driver->edit() не пробрасываются — это «вежливая» финализация.

Reply-клавиатуры, Keyboard::remove() и кнопки без action (только URL/requestContact/requestLocation) контекст не пишут.

Messaging

IncomingMessage

Свойство Тип Описание
id string ID сообщения
chatId string ID чата
driverName string Имя драйвера (telegram, …)
text ?string Текст
user UserDto Отправитель
type ContentType Тип контента
action ?string Callback-action
actionParams ?array Параметры action
event ?string Имя события
media ?MediaDto Медиа
location ?LocationDto Геолокация
contact ?ContactDto Контакт
referral ?string Реферальный код
raw array Сырое тело апдейта

ContentType (enum)

Text | Action | Media | Location | Contact | Event.

OutgoingMessage

use Govorun\Messaging\Message;

$msg = Message::make('Текст')
    ->keyboard($keyboard)
    ->parseMode('HTML');

Keyboard

use Govorun\Messaging\Button;
use Govorun\Messaging\Keyboard;

// Inline
Keyboard::make()->buttons([
    [
        Button::make('Текст')->action('confirm', ['id' => 42]),
        Button::make('Ссылка')->url('https://example.com'),
    ],
    [Button::make('Новый ряд')->action('continue')],
]);

// Reply (с fluent-флагами)
Keyboard::reply()
    ->resize()
    ->oneTime()
    ->buttons([
        [Button::make('Контакт')->requestContact()],
        [Button::make('Локация')->requestLocation()],
    ]);

// Снять клавиатуру
Keyboard::remove();

Media

use Govorun\Messaging\Media;

Media::photo('https://example.com/img.jpg')->caption('Описание');
Media::document('https://example.com/file.pdf');
Media::video('https://example.com/clip.mp4');
Media::audio('https://example.com/track.mp3');
Media::voice('https://example.com/voice.ogg');
Media::animation('https://example.com/anim.gif');

Telegram-драйвер дополнительно умеет отправлять локальные файлы — если в OutgoingMessage::$media['url'] лежит существующий локальный путь (а не URL), используется multipart-upload через Bot API.

Button

Button::make('Текст')
    ->action('callback_action', ['key' => 'value'])
    ->url('https://...')
    ->requestContact()
    ->requestLocation();

DTO

UserDto

Свойство Тип
id string
firstName ?string
lastName ?string
username ?string
phone ?string
locale ?string
raw array

MediaDto / LocationDto / ContactDto

MediaDto: type, url, fileId, mimeType, fileSize, raw. LocationDto: latitude, longitude, raw. ContactDto: phone, firstName, lastName, userId, raw.

Flow (пошаговые диалоги)

Многошаговый диалог. Состояние сохраняется между шагами в StateStorage.

use Govorun\State\Flow;
use Govorun\State\Step;
use Govorun\Messaging\Keyboard;
use Govorun\Messaging\Button;
use Govorun\Messaging\IncomingMessage;

class OrderFlow extends Flow
{
    protected array $steps = ['product', 'quantity', 'confirm'];
    protected array $interruptCommands = ['/start', '/cancel'];
    protected bool $interruptOnEvent = true;

    public function productStep(Step $step): void
    {
        $step->ask('Какой товар вас интересует?');

        $step->receive(function (IncomingMessage $msg) {
            if ($this->validator($msg->text)->required()->fails()) {
                return; // ошибка уже отправлена пользователю
            }

            $this->state->set('product', $msg->text);
            $this->nextStep();
        });
    }

    public function quantityStep(Step $step): void
    {
        $step->ask('Сколько штук?');

        $step->receive(function (IncomingMessage $msg) {
            $this->state->set('quantity', (int) $msg->text);
            $this->nextStep();
        });
    }

    public function confirmStep(Step $step): void
    {
        $product = $this->state->get('product');
        $qty = $this->state->get('quantity');

        $step->ask(
            "Заказ: {$product} x {$qty}. Подтвердить?",
            Keyboard::make()->buttons([[
                Button::make('Да')->action('yes'),
                Button::make('Нет')->action('no'),
            ]]),
        );

        $step->receive(function (IncomingMessage $msg) {
            if ($msg->action === 'yes') {
                $this->reply('Заказ принят!');
            }
            $this->nextStep(); // обязателен — завершает flow и чистит state
        });
    }

    public function onComplete(): void {}
    public function onCancel(): void { $this->reply('Заказ отменён.'); }
}

Свойства

Свойство Тип Описание
$steps array Имена шагов в порядке выполнения
$interruptCommands array Команды, прерывающие flow
$interruptOnEvent bool Прерывать при событии
$state StateData In-memory данные шагов (сохраняются после ask/receive)

Методы

Метод Описание
start() Запуск с первого шага
resume() Возобновление текущего шага (вызывает FlowHandler)
nextStep(?string $name) Переход. Без аргумента — следующий по $steps; с именем — прыжок (goTo). Если текущий последний — completeFlow()
reply(string $text) Текстовый ответ
send(OutgoingMessage $msg) Отправка сложного сообщения
validator(?string $value) Создать Validator с автоматической отправкой ошибки пользователю
http() HttpManager (через трейт MakesHttpCalls)
onComplete() / onCancel() Хуки

Step

$step->ask(
    string|OutgoingMessage $message,
    Closure|Keyboard|null  $keyboard = null,  // прямой Keyboard или Closure-билдер
);

$step->receive(Closure $callback);  // function (IncomingMessage $msg): void

Клавиатура-Closure исполняется в bind'е Flow, поэтому имеет доступ к $this->state.

Прерывание

shouldInterrupt(IncomingMessage) возвращает true когда:

  1. Активна ask_keyboard (есть __ask_keyboard_ctx в state) и пришёл текст — это значит пользователь не нажал кнопку, а написал что-то ещё. События в этом режиме не прерывают.
  2. $interruptOnEvent === true и пришло событие.
  3. Текст совпадает с одной из $interruptCommands (или начинается на <cmd> ).

Auto-finalize ask_keyboard

Если в шаге задана inline-клавиатура с action-кнопками, при отправке ask контекст сохраняется в state.__ask_keyboard_ctx. На nextStep() / onCancel() исходное сообщение редактируется: (выбрано: <label>) или (отменено). При прерывании по команде до выбора — финализация с (отменено).

State Storage

interface StateStorage
{
    public function get(string $chatId, string $driver): ?array;
    public function set(string $chatId, string $driver, array $data): void;
    public function delete(string $chatId, string $driver): void;
}
Класс Описание
FileStateStorage JSON-файлы в storage/state/
DatabaseStateStorage Таблица govorun_states (создаётся миграцией)
CacheStateStorage Кеш Illuminate с TTL

Драйвер выбирается в config/state.php: driver (file/database/cache) + ttl (секунды).

StateAccessor

Общий контракт чтения/записи произвольных ключей состояния. Две реализации:

Реализация Где Семантика записи
StateData $this->state во Flow In-memory, флашится в storage в конце ask/receive
PersistentState $this->state в Controller Write-through — каждый set() сразу пишет в storage
Метод Описание
get(string $key, mixed $default) Получить значение
set(string $key, mixed $value) Сохранить значение
has(string $key) Проверить наличие
all() Получить весь массив

HTTP

ConnectionClient (рекомендуемый путь)

Подключения к внешним API описаны в config/connections.php:

return [
    'payment' => [
        'base_url' => 'https://api.payment.com/v1',
        'default_headers' => ['Accept' => 'application/json'],
        'auth' => ['type' => 'bearer', 'token' => env('PAYMENT_TOKEN')],
    ],
];

Трейт MakesHttpCalls подмешан в Controller и Flow:

$response = $this->http()->connection('payment')->post('/charge', [
    'json' => ['amount' => 100, 'currency' => 'RUB'],
]);

if ($response->successful()) {
    $payment = $response->json();
}
Тип auth Конфиг
none (без auth)
bearer ['type' => 'bearer', 'token' => '…'] → заголовок Authorization: Bearer …
api_key (header) ['type' => 'api_key', 'in' => 'header', 'key' => 'X-Api-Key', 'value' => '…']
api_key (query) ['type' => 'api_key', 'in' => 'query', 'key' => 'api_key', 'value' => '…']
basic ['type' => 'basic', 'login' => '…', 'password' => '…']

Опции Guzzle (json, form_params, query, headers, …) пробрасываются через второй аргумент. Таймаут по умолчанию — 10 секунд, http_errors=false (не бросает исключения на 4xx/5xx).

HttpResponse:

Метод Описание
status(): int HTTP-статус
successful(): bool 2xx
failed(): bool не 2xx
body(): string сырое тело
json(): ?array JSON-декод (null при ошибке)

ApiClient (абстрактный, для собственных клиентов)

Альтернатива для случаев, когда удобнее иметь типизированный клиент-наследник, а не вызывать connection('slug'):

use Govorun\Http\ApiClient;

class PaymentClient extends ApiClient
{
    protected int $timeout = 10;
    protected int $retries = 2;

    public function baseUrl(): string
    {
        return 'https://api.payment.com/v1';
    }

    public function headers(): array
    {
        return ['Authorization' => 'Bearer ' . env('PAYMENT_KEY')];
    }
}
Метод Описание
get(string $uri, array $params) GET
post(string $uri, array $data) POST
put(string $uri, array $data) PUT
delete(string $uri) DELETE

Validator

Govorun\Support\Validator — fluent-валидатор пользовательского ввода с lazy-load дефолтов из resources/validation-messages.json (синхронизирован с govorun-factory/resources/validation-messages.json — править одновременно).

use Govorun\Support\Validator;

$error = Validator::make($msg->text)
    ->required()
    ->numeric()
    ->min(1)
    ->max(999)
    ->validate();   // ?string — null если ок

if ($error !== null) {
    $this->reply($error);
    return;
}

В Flow есть шорткат $this->validator($value) — он сам отправит ошибку пользователю через errorHandler. Терминал — ->fails(): bool (true = ошибка уже отправлена пользователю):

if ($this->validator($msg->text)->required()->email()->fails()) {
    return;
}

Доступные правила: required, email, string, numeric, integer, url, phone, regex, min, max, between, in, date. Полный список — src/Support/Validator.php. Шаблоны сообщений — resources/validation-messages.json.

Middleware

use Govorun\Routing\Middleware;
use Govorun\Messaging\IncomingMessage;

class LogMiddleware implements Middleware
{
    public function handle(IncomingMessage $message, \Closure $next): void
    {
        logger()->info("Message from {$message->user->id}: {$message->text}");
        $next($message);
    }
}

Middleware складываются в pipeline. Если $next не вызван — цепочка прерывается.

Request

Метод Описание
Request::capture() Создать из глобальных переменных
getContent() Сырое тело
json() Декодированный JSON
header(string $name) Значение заголовка
method() HTTP-метод
uri() Полный URI
path() Путь без query
query(string $key, $default) Параметр строки запроса

ServiceProvider

use Govorun\Foundation\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function register(): void { /* привязки контейнера */ }
    public function boot(): void { /* после регистрации всех провайдеров */ }
}

Подключение в config/app.php:

'providers' => [
    App\Providers\AppServiceProvider::class,
],

MessengerDriver (интерфейс)

Метод Описание
verifyWebhook(Request) Проверить подпись запроса
parseUpdate(Request) Распарсить в IncomingMessage
send(OutgoingMessage) Отправить сообщение (вернуть ?string ID отправленного)
edit(string $id, OutgoingMessage) Редактировать сообщение
delete(string $id, string $chatId) Удалить сообщение
installWebhook(string $url) Установить вебхук
removeWebhook() Удалить вебхук
getUser(string $id) Получить данные пользователя

Telegram-драйвер дополнительно реализует методы для bot:profile-sync: setMyName, setMyShortDescription, setMyDescription, setMyCommands, setMyProfilePhoto, removeMyProfilePhoto.

CLI-команды

Команда Описание
php govorun webhook:install Установить вебхуки для активных драйверов
php govorun webhook:remove Удалить вебхуки
php govorun migrate Запустить миграции БД
php govorun make:controller {name} Создать контроллер из stub'а
php govorun make:flow {name} Создать Flow-диалог
php govorun make:api-client {name} Создать API-клиент
php govorun state:clear Очистить состояния Flow
php govorun bot:profile-sync Идемпотентная синхронизация Telegram-профиля (name / short_description / description / commands / photo) из config/bot_profile.php и storage/app/bot-profile.{jpg,mp4}. Флаги --only=<sec>... / --skip=<sec>...
php govorun test Запустить тесты проекта

Тестирование

vendor/bin/phpunit                                    # все тесты
vendor/bin/phpunit --testsuite=Unit                   # только Unit
vendor/bin/phpunit --testsuite=Integration            # только Integration
vendor/bin/phpunit tests/Unit/State/FlowTest.php      # один файл
vendor/bin/phpunit --filter test_specific_thing       # по имени

В src/Testing/ лежат TestCase, FakeDriver, FakeMessenger, FakeApiClient и трейты — используются как в тестах фреймворка, так и из проектов-потребителей.

Лицензия

MIT

govorun/framework 适用场景与选型建议

govorun/framework 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 53 次下载、GitHub Stars 达 0, 最近一次更新时间为 2026 年 03 月 27 日, 在 PHP 生态内属于活跃度较高的组件。

我们在过去多个企业项目中使用过 govorun/framework 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。

围绕 govorun/framework 我们能提供哪些服务?
定制开发 / 二次开发

基于 govorun/framework 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。

BUG 修复 & 性能优化

线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。

项目外包 & 长期维护

承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。

yvsm@zunyunkeji.com QQ:316430983 微信:yvsm316 西安尊云信息科技 · 专注 PHP / Go / 分布式系统研发

统计信息

  • 总下载量: 53
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 29
  • 依赖项目数: 1
  • 推荐数: 0

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-03-27