Skip to content

Settings

Current version: v0.0.4

telegram-bot-essentials/settings (namespace TelegramBotEssentials\Settings) adds a per-bot, typed key/value settings system to Essence. Every other module in the ecosystem (Billing, User Wallet, Affiliates, the gateways) registers its own configurable options through this package rather than inventing its own settings storage.

Installation

bash
composer require telegram-bot-essentials/settings
php artisan migrate

The package auto-registers a locale setting out of the box, letting each bot pick its own admin/reply language independently (see Locale switching below).

Core Concepts

The Setting DTO

A setting is declared once, in a service provider's boot(), as a TelegramBotEssentials\Settings\DTOs\Setting:

php
use TelegramBotEssentials\Settings\DTOs\Setting;
use TelegramBotEssentials\Settings\Enums\SettingType;

settings()->addSetting(new Setting(
    key: 'billing.user_wallet.status',
    label: fn () => __('my-package::settings.wallet_status.label'),
    type: SettingType::CHECKBOX,
    default: false,
));
ParameterTypeNotes
keystringDot-namespaced identifier, e.g. billing.user_wallet.currency
labelstring|ClosureResolved lazily — use a closure to defer translation lookups
typeSettingTypeControls validation, storage, and the admin UI widget
defaultmixedReturned when no BotSetting row exists yet
optionsarray|Closure|nullRequired for SELECT, ENUM, MULTISELECT
descriptionstring|Closure|nullShown in the admin settings UI

SettingType

src/Enums/SettingType.php defines the supported widget/storage types:

TypeStorageValidation
TEXTraw stringnullable|string
NUMBERraw stringnullable|numeric
CHECKBOXraw string ('1'/'0')nullable|boolean
SENSITIVEencrypted via encrypt()/decrypt()nullable|string
SELECTone of options keysnullable|in:<option keys>
ENUMone of options valuesnullable|in:<option values>
MULTISELECTcomma-joined stringnullable|array
DIRECTORY— (UI grouping only, no dedicated storage branch)

SENSITIVE is the type to reach for with API keys, gateway secrets, or anything else you don't want sitting in plaintext in the bot_settings table.

The settings() helper

Everything is read and written through the global settings() helper (src/helpers.php), which resolves the Settings singleton:

php
// Read (falls back to the Setting's default if no row exists yet)
$locale = settings()->get('locale');
$walletEnabled = settings()->get('billing.user_wallet.status');

// Write (validated against the type's rules before saving)
settings()->set('billing.user_wallet.status', true);

// Introspect a declared setting (for building admin UI)
$setting = settings()->getSetting('billing.user_wallet.status');
$setting->getLabel();
$setting->getTypeEmoji();   // ✅ / ❌ / 🔒 / 🔢 / 💬 / 🧩 / 📋 / 🗃️

// List everything registered, for a settings menu
settings()->getSettings(); // Collection<string, Setting>

get()/set() are always scoped to wHook()->bot()->id — settings are per-bot, not global to the application. This is why other modules read settings inside request-time closures (see How other modules use this) rather than caching the value at boot.

Storage

One row per (bot_id, key) pair in bot_settings (model BotSetting, tenant-scoped via BelongsToTenant). Values are always stored as strings/null and cast back to the appropriate shape by Settings::get() based on the setting's declared SettingType.

Locale switching

The package ships a locale setting (SettingType::SELECT) whose options come from LocaleRegistry::selectOptions() — it scans the host app's translation files (via Essence's TranslationScanner) and shows each supported locale with a live translation-completeness percentage, e.g. فارسی 412/420 (98%).

A listener, SetBotLocale, runs on BotWebhookInitialized and calls App::setLocale(settings()->get('locale') ?? config('app.locale')) — so every bot can run in a different language without any per-request wiring in your own code.

Admin UI

The package registers its own CallbackQuery, StateAnswer, ReplyKey, and Feature (Telegram/CallbackQueries/Admin/BotSettingsQuery.php, etc.) that render a generic settings menu from whatever is currently in the registry — you get a working admin settings screen for free as soon as you addSetting() something, with no bespoke UI code required per setting.

How other modules use this

This is the pattern used throughout the ecosystem for feature flags and per-bot configuration — declare the setting once in your service provider's boot(), then check it lazily wherever it matters:

php
// TbeUserWalletServiceProvider::boot()
settings()->addSetting(new Setting(
    key: 'billing.user_wallet.status',
    label: fn () => __('tbe-user-wallet::settings.status.label'),
    type: SettingType::CHECKBOX,
    default: false,
));

// Elsewhere, e.g. registering a conditional sort — evaluated per request, not at boot:
botUserSorts()->addSort(new BotUserSort(
    // ...
    active: fn () => settings()->get('billing.user_wallet.status'),
));

Evaluate lazily

Because settings are per-bot and boot() runs once per application (not per bot/tenant), never resolve settings()->get(...) directly inside boot() to decide whether to register something — the bot context isn't available yet. Instead, wrap the check in a Closure (an active callback, a conditional inside a handler method, etc.) so it's evaluated at request time, once wHook()->bot() is resolved.