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

Text Chunker Laravel Package

ecourty/text-chunker

ecourty/text-chunker is a PHP library for splitting long text into smaller, configurable chunks. It helps prepare content for LLM prompts, embeddings, pagination, SMS, and APIs while keeping chunks readable and within limits.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a framework-agnostic Composer library — there is no service provider, facade, or config file shipped. Treat it as a pure PHP dependency you wire into Laravel yourself.

composer require ecourty/text-chunker

Where to look first (in this order):

  1. src/TextChunker.php — the entire orchestration lives in one small class. Read it once and you'll know the whole API: setFile() / setText(), withReader(), withMetadata(), withPostProcessor(), and chunk(ChunkingStrategyInterface): Generator.
  2. src/ValueObject/Chunk.php — the only thing you'll handle downstream. Immutable; withMetadata() returns a copy.
  3. src/Strategy/ — pick the strategy that matches the shape of your content, not the file extension. MarkdownChunkingStrategy for docs, HtmlChunkingStrategy for scraped pages, DialogueChunkingStrategy for transcripts, RecursiveChunkingStrategy as your safe default.
  4. src/PostProcessor/ — this is where the real value is for RAG work. Most people stop at strategies and miss that overlap, token limits, and dedupe are already built.

Minimal first use case — chunk a file already in storage/app/private:

use Ecourty\TextChunker\TextChunker;
use Ecourty\TextChunker\Strategy\MarkdownChunkingStrategy;

$chunks = (new TextChunker())
    ->setFile(storage_path('app/private/handbook.md'))
    ->withMetadata(['doc_id' => 42, 'tenant' => 'acme'])
    ->chunk(new MarkdownChunkingStrategy());

foreach ($chunks as $chunk) {
    // getters vs. public readonly props vary by version — inspect Chunk.php once
    dump($chunk->getText(), $chunk->getPosition(), $chunk->getMetadata());
}

The first genuinely useful thing to build is an Artisan preview command (php artisan chunk:preview {path} {--strategy=markdown}) that dumps the first five chunks with character counts. Chunk quality is a tuning problem, and you cannot tune what you cannot eyeball.

Implementation Patterns

Bind a factory, not a singleton. TextChunker is mutable and fluent — setText(), withMetadata(), withPostProcessor() all change instance state. Under Octane or in a queue worker processing many documents, a singleton will leak one document's metadata into the next. Bind a closure instead:

// AppServiceProvider::register()
$this->app->bind(TextChunker::class, fn () => new TextChunker());

Strategy selection belongs in one factory, not scattered through jobs. Route on MIME type first, extension second:

final class ChunkingStrategyFactory
{
    public function forDocument(string $mime, ?string $ext = null): ChunkingStrategyInterface
    {
        return match (true) {
            str_contains($mime, 'html')      => new HtmlChunkingStrategy(/* verify ctor args in README */),
            $ext === 'md'                   => new MarkdownChunkingStrategy(),
            str_contains($mime, 'text/csv') => new LineChunkingStrategy(50),
            default                         => new RecursiveChunkingStrategy(/* max size */),
        };
    }
}

Assemble the post-processor pipeline from config, in a deliberate order. The pipeline is applied in registration order, so this is a place to be explicit rather than clever:

// config/chunking.php
'pipeline' => [
    TextNormalizationPostProcessor::class,  // clean before measuring anything
    ChunkFilterPostProcessor::class,        // drop empties before they cost an embedding
    DeduplicationPostProcessor::class,      // md5-based; must run before overlap inflates text
    ChunkMergerPostProcessor::class,        // merge small fragments
    OverlappingChunkPostProcessor::class,   // add context tails
    TokenLimitPostProcessor::class,         // split whatever is still too big
    MetadataEnricherPostProcessor::class,   // word/char counts, index, total
];

Resolve with app($class) in a loop so any processor that later grows dependencies still works.

Queue job for ingestion. Keep the chunker inside the job, stream straight into batched embeddings, and never materialise the full set:

public function handle(EmbeddingService $embeddings): void
{
    $path = Storage::disk('local')->path($this->document->path);

    $chunker = app(TextChunker::class)
        ->setFile($path)
        ->withMetadata(['doc_id' => $this->document->id]);

    foreach (config('chunking.pipeline') as $processor) {
        $chunker->withPostProcessor(app($processor));
    }

    LazyCollection::make($chunker->chunk($this->strategy))
        ->map(fn ($c) => $c->getText())
        ->chunk(64)
        ->each(fn ($batch) => $this->document->embeddings()->createMany(
            $embeddings->embed($batch->values()->all())
        ));
}

Wrap it behind your own interface. ChunkingService::chunk(string $path, array $meta): Generator with this package as the implementation. Costs one file, and it's the difference between a one-line swap and a refactor if the package stalls (9 stars, 0 dependents — it is one maintainer's good idea, not an ecosystem).

Custom post-processor for domain cleanup (PII redaction, redacting customer names, stripping template boilerplate) is the cheapest extension point: implement process(\Generator $chunks, string $source = ''): \Generator, yield each chunk, and do not buffer — return a new Chunk via withMetadata() rather than mutating.

Gotchas and Tips

  • chunk() returns a Generator — nothing happens until you iterate. Exceptions from a bad path, unreadable file, or malformed HTML surface inside the foreach, not at the setFile() call. Wrap the loop, not the construction.
  • Generators are single-pass. iterator_to_array() on a 200-page PDF is exactly the memory blow-up this library exists to avoid. Use LazyCollection::make() and consume once.
  • The chunker holds mutable state. setText() / withMetadata() persist between calls on the same instance. In Octane, queue workers, and tests using RefreshDatabase with a resolved singleton, you will see metadata from the previous document. Bind fresh or call ->setText() on a new instance.
  • Strategies are stateful (reset() exists for a reason). Reusing one strategy object across unrelated chunk() calls is only safe if the chunker resets it — verify in TextChunker.php before relying on it; when in doubt, new the strategy per document.
  • Order matters more than the choice of processors. Dedupe after overlap will not dedupe (overlap makes every chunk unique); normalize after MetadataEnricher gives you stale char counts; token-limit before overlap means the overlap pushes you back over budget. Overlap → token limit → enrich is the sane tail.
  • Overlap inflates your token bill silently. OverlappingChunkPostProcessor prepends the previous tail to every chunk, so effective tokens ≈ n × (size + overlap). If you're near an API limit, budget with overlap already applied, and leave 10–15% headroom.
  • Buffer size vs. boundary-spanning splits. LocalFileReader streams in bufferSize blocks; paragraph, sentence, and regex strategies receive data with $isEnd to know when to flush. A regex or paragraph break that straddles a buffer seam is the classic bug — reproduce it with a file several times larger than your buffer before you trust RegexChunkingStrategy in production.
  • Fixed-size splitting is byte/character based, not grapheme based. Emoji and CJK text may split mid-character. Test FixedSizeChunkingStrategy with real international content; if it breaks, prefer SentenceChunkingStrategy or RecursiveChunkingStrategy for those locales.
  • TokenLimitPostProcessor is a safety net, not a tokenizer. These libraries typically approximate (~4 chars/token). Do not treat it as a hard guarantee against a provider's context_length_exceeded — validate against the real tokenizer for your model, or set the limit conservatively.
  • HtmlChunkingStrategy leans on DOM/XPath. Malformed markup produces libxml warnings and possibly empty yields. If you're chunking scraped pages, normalize/sanitize first, and check whether tag-stripping and comment-keeping options are enabled for your use case — otherwise your embeddings carry <nav> boilerplate.
  • Storage::path() only works on the local driver. For S3 you must either download to a temp file or implement ReaderInterface::readChunks(string $path, int $bufferSize): \Generator against Storage::readStream() and throw \RuntimeException on read failure. The custom reader is ~20 lines and unlocks the streaming benefit for remote files.
  • Persist metadata as a JSON column alongside the chunk text and a position offset — the offsets are what let you later deep-link a citation back into the source document, which is the thing reviewers ask for first.
  • Pin the version in composer.json (exact or ~). With a 9-star package, a minor release is an unreviewed diff; the changelog is the commit history.
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.
unabomber/timezone
delatbabel/contacts
astritzeqiri/g-recaptcha
kevinongko/laravel-genode
developeruz/laravel-analytics
robbiep/afterthedeadline
frankkessler/incontact-laravel-oauth2-rest
jlxh/laravel-userlog
davidmgilo/timetables
qylinfly/role-permission
reflect-digital/conduit
takashato/laravel-vietqr
aitor24/laralang
kraenzle-ritter/entity-relations
dindigital/din-image
pomirleanu/image-colors
sawmainek/laravel-api-generator
delatbabel/jsonresponses
mouhamedfd/permissions-generator
eduplus/qr