Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Telegram Entities Laravel Package

danog/telegram-entities

Convert Telegram message entities to and from Markdown, HTML and plain text with this lightweight PHP library. Parse formatted messages, preserve styling, and generate entity arrays for sending rich text via the Telegram Bot API or MadelineProto.

Deep Wiki
Context7

Getting Started

composer require danog/telegram-entities

The package solves exactly one problem, but it is a problem you will hit: Telegram expresses offset/length for every MessageEntity in UTF-16 code units, while PHP gives you UTF-8 bytes and mb_* gives you characters or graphemes. None of those agree once a message contains an emoji, a flag, a CJK string, or a Zalgo-stacked name. Every "bold text is off by one" and "my link points into the middle of an emoji" bug traces back to this.

Where to look first, in this order:

  1. src/Entity.php — the DTO. Note the fields: type, offset, length, plus the optional payload fields (url for text_link, user for text_mention, language for pre, custom emoji id). This is the shape you will persist.
  2. src/Entities.php — the collection plus the converters. Grep for public static function here; the factory/converter names (fromHtml / parseHTML / toMarkdown / toTelegram) are the whole API surface, and they carry across versions more reliably than the names themselves.
  3. The tests, if present — they double as a spec for surrogate-pair handling.

First use case — read a message, render it on the web. This is the 20-minute win: you have a bot or MadelineProto client ingesting updates, and you want a web panel that shows messages with formatting intact.

use danog\TelegramEntities\Entities;

// $update['message'] contains: text + entities[] in UTF-16 units
$entities = Entities::fromTelegram($message['text'], $message['entities'] ?? []);

echo $entities->toHTML();   // safe for a Blade view

Persist text as-is plus json_encode($entities) as a TEXT/JSON column. Do not persist the HTML — that is a rendering decision, not a data decision.

Implementation Patterns

1. Entities are the canonical form; HTML/Markdown are view-layer only. Treat (text, entities[]) as your storage format and convert at the edges. Anything else (HTML in the DB, Markdown in the DB) guarantees a lossy round-trip the first time someone sends a nested text_link inside bold.

// Outbound controller / notification
$entities = Entities::fromHtml('<b>Deploy</b> finished: <a href="https://ci.example.com/42">#42</a>');
$this->telegram->messages->sendMessage([
    'peer'      => $peer,
    'message'   => $entities->getText(),     // plain text, no tags
    'entities'  => $entities->toTelegram(),  // MessageEntity[] in UTF-16
]);

2. Build a thin application-level value object around it. You will want RichText::fromTelegram(), ->toHtml(), ->toPlain(), ->truncate(). Wrapping the library means the day its constructor signature shifts, you patch one class instead of forty call sites.

final class RichText implements JsonSerializable
{
    private function __construct(
        public readonly string $text,
        public readonly Entities $entities,
    ) {}

    public static function fromTelegram(string $text, array $raw): self
    {
        return new self($text, Entities::fromTelegram($text, $raw));
    }

    public function toPlain(): string { return $this->text; }

    public function jsonSerialize(): array
    {
        return ['text' => $this->text, 'entities' => $this->entities];
    }
}

3. Normalise heterogeneous input at the boundary. Updates arrive as TL objects from MadelineProto, as plain arrays from webhooks, and as JSON from your queue. Hydrate all three into one Entities instance on ingest. If you skip this, you end up with three subtly different offset conventions and a text_mention bug that only reproduces for one transport.

4. Chunking long messages. Telegram caps at 4096 chars (1024 for captions) and 100 entities per message. When you split, you must slice entities as well as text, and re-base offsets — the library gives you the UTF-16 arithmetic, not the chunking policy. Slice on entity boundaries where possible, drop entities that would be orphaned, and never substr() the text.

5. Use it as a test oracle, not just a converter. In your own entity-handling code (highlighting, search snippets, truncation), assert against this library:

$this->assertSame($expectedLengthInUtf16, $entities->getEntity(0)->length);

That single assertion catches the class of bug that otherwise only shows up in production on a user with an emoji in their display name.

6. Pair with whatever already escapes. If you use MadelineProto, it has its own htmlEscape/markdownEscape helpers. Pick one escaping strategy per code path; mixing two is how you get &amp;amp; in sent messages.

Gotchas and Tips

  • UTF-16 ≠ characters. A 😀 is 2 code units, a family emoji with ZWJ can be 11, and a combining-accent e + ◌́ is 2 units but 1 visible glyph. Quick sanity check in tinker: strlen(mb_convert_encoding($s, 'UTF-16LE', 'UTF-8')) / 2. If your computed length disagrees with that, your offsets are wrong.
  • Entities must be sorted by offset and properly nested. Telegram rejects or silently mangles overlapping, non-nested ranges. bold from 0–10 and italic from 5–15 is illegal; 0–10 and 2–8 is fine. Sort and validate before sending, especially when merging entities from two sources (e.g. an auto-linker plus user formatting).
  • text_link without url, text_mention without user, pre without language all fail validation on send. The DTO will happily carry a half-populated entity; validation is on you.
  • Truncation is the #1 source of corruption. Preview snippets, substr($text, 0, 100) . '…' — this slices bytes and will split a surrogate pair, producing invalid UTF-8 and dangling entities. Truncate through the library, or truncate plain text and drop all entities.
  • toHtml() output goes into a browser. Confirm the library escapes &<>" in text nodes; if you are on an older release, escape the plain text yourself and only trust the library for offsets. Never {!! !!} raw user entities in Blade without verifying.
  • MarkdownV2 escaping is a minefield: _*[]()~\>#+-=|{}.!all need escaping outside code spans. If the library emits Markdown rather than MarkdownV2, do not pass it toparse_mode=MarkdownV2— use the entities array instead. Honestly, always prefer sendingentitiesoverparse_mode`; it removes the escaping problem entirely.
  • Small package, low bus factor (27 stars). Apache-2.0 and a prolific maintainer, so the risk is "abandoned," not "license." Pin the exact version in composer.json, keep the wrapper from Pattern 2, and skim the changelog before composer update — this is a library where a patch release can change a converter's whitespace behaviour.
  • Debugging technique that actually works: send yourself a message with the failing text, then compare Telegram's returned entities against what you computed. Log text, each entity's offset/length, and mb_substr($text, 0, $offset) alongside it — the mismatch jumps out immediately.
  • Extension point: the entity collection is iterable/countable and JsonSerializable, so you can post-process (strip all text_links before storing, promote url entities to real links in your HTML renderer, redact mentions in logs) without forking. Do that in your wrapper rather than patching vendor code.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle