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

Pdf Parser Bundle Laravel Package

kreatys/pdf-parser-bundle

A PHP bundle for extracting text and metadata from PDF documents. Wraps a PDF parser behind a simple service interface so you can pull content, page counts, and document info from uploaded or stored PDFs in your PHP application.

View on GitHub
Deep Wiki
Context7

Getting Started

The first thing to internalise: this is a Symfony bundle, not a Laravel package. There is no service provider, no auto-discovery, no facade. KreatysPdfParserBundle::load() — the class that registers all the bundle's DI extensions, config tree, and service definitions — never runs inside a Laravel app. What you get for the composer require is a single useful class: Kreatys\PdfParserBundle\Service\PdfParser, a convenience wrapper over smalot/pdfparser's Parser/Document/Page objects (parseFile, parseContent, getText, getPages, etc.).

Read the source before trusting the README — it's thin:

vendor/kreatys/pdf-parser-bundle/
├── KreatysPdfParserBundle.php
├── DependencyInjection/
│   ├── KreatysPdfParserExtension.php   # extension + Configuration
│   └── Configuration.php
├── Resources/config/services.yml       # service id + constructor args
└── Service/PdfParser.php               # the real API surface

That services.yml is the ground truth for wiring — copy its constructor arguments into your own binding rather than guessing at them.

Minimum steps:

composer require kreatys/pdf-parser-bundle
php artisan vendor:publish --provider="..."  # nothing to publish — bundle has no providers

Then wire it yourself:

// app/Providers/PdfParserServiceProvider.php
namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Kreatys\PdfParserBundle\Service\PdfParser;

class PdfParserServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        // Bind, not singleton — see Gotchas (the wrapper is stateful).
        $this->app->bind(PdfParser::class, fn () => new PdfParser());
    }
}

Register it in config/app.php under providers (or bootstrap/providers.php on L11+).

First use case — extract text from an uploaded invoice so you can index it, search it, or hand it to a downstream step:

Route::post('/invoices', function (Request $request) {
    $request->validate(['pdf' => ['required', 'file', 'mimes:pdf', 'max:20480']]);

    /** @var \Kreatys\PdfParserBundle\Service\PdfParser $parser */
    $parser = app(\Kreatys\PdfParserBundle\Service\PdfParser::class);
    $parser->parseFile($request->file('pdf')->getRealPath());

    return response()->json(['text' => $parser->getText()]);
});

If getText() returns a Document in your version, prefer getPages() and iterate — per-page text keeps peak memory flatter and gives you page numbers for citations or RAG chunking.

Honest framing before you build on it: this package is a ~50-line convenience wrapper over smalot/pdfparser with zero stars, zero dependents, and no maintenance signal. For most Laravel apps the correct move is to composer require smalot/pdfparser directly and skip the wrapper entirely. The rest of this section assumes you're using the wrapper anyway (maybe it's already vendored, maybe the wrapper's API shape suits an existing codebase) — but treat it as a convenience you could replace with a one-file adapter in an afternoon.

Implementation Patterns

Hide extraction behind a contract. This is the single pattern that pays off regardless of which library wins. Callers should never see Kreatys\PdfParserBundle\Service\PdfParser in their typehints:

namespace App\Contracts;

interface PdfTextExtractor
{
    /** @return array{text: string, pages: array<int, string>, metadata: array, empty: bool} */
    public function extract(string $absolutePath): array;
}
namespace App\Services;

use App\Contracts\PdfTextExtractor;
use Kreatys\PdfParserBundle\Service\PdfParser;

class KreatysPdfTextExtractor implements PdfTextExtractor
{
    public function extract(string $absolutePath): array
    {
        $parser = new PdfParser();
        $parser->parseFile($absolutePath);

        $pages = [];
        foreach ($parser->getPages() as $i => $page) {
            $pages[$i + 1] = (string) $page->getText();
        }
        $text = implode("\n\n", $pages);

        return [
            'text'    => $text,
            'pages'   => $pages,
            'metadata' => method_exists($parser, 'getMetadata') ? $parser->getMetadata() : [],
            // Empty text on a successful parse almost always means scanned/image-only PDF.
            'empty'   => trim($text) === '',
        ];
    }
}

Bind it once: $this->app->bind(PdfTextExtractor::class, KreatysPdfTextExtractor::class);. When the wrapper dies or you outgrow it, you swap the implementation and no caller changes.

Never call the wrapper from a request cycle in production. PDF parsing is CPU- and memory-bound; a 40 MB document with a complex page tree can take seconds and hundreds of MB. Put extraction on a queue:

class ExtractPdfText implements ShouldQueue
{
    public int $timeout = 120;
    public int $tries = 3;
    public int $backoff = 30;

    public function __construct(public int $documentId) {}

    public function handle(PdfTextExtractor $extractor): void
    {
        $doc = Document::findOrFail($this->documentId);
        $path = Storage::disk($doc->disk)->path($doc->path);

        try {
            $result = $extractor->extract($path);
        } catch (\Throwable $e) {
            $doc->update(['status' => 'failed', 'error' => $e->getMessage()]);
            return; // don't retry malformed PDFs three times for nothing
        }

        $doc->update([
            'status'  => $result['empty'] ? 'needs_ocr' : 'parsed',
            'text'    => $result['text'],
            'pages'   => $result['pages'],
            'metadata' => $result['metadata'],
        ]);
    }

    public function failed(\Throwable $e): void
    {
        Document::whereKey($this->documentId)->update(['status' => 'failed']);
    }
}

Set worker --timeout above the job $timeout, and retry_after on the queue connection above the worker timeout — otherwise a slow extraction re-queues itself and duplicates work.

Batch / bulk ingestion. For a folder of PDFs, process one job per file. Don't loop parseFile() over hundreds of files in a single job — one bad PDF kills the batch and there is no partial progress.

Storage::disk('inbox')->files()
    ->filter(fn ($f) => str_ends_with($f, '.pdf'))
    ->each(fn ($f) => ExtractPdfText::dispatch(Document::createFromPath($f)->id));

Cap concurrency per worker pool (e.g. --max-jobs=...) or a dedicated low-memory queue) so a decompression bomb can't take every Horizon supervisor offline at once.

Metadata-first triage. Parsing metadata is much cheaper than parsing the full text layer. If your app needs page counts, titles, or creation dates to decide whether to extract, get metadata first and defer full text:

$parser->parseFile($path);
$meta = $parser->getMetadata(); // title, author, pages, creator, etc.
if (($meta['pages'] ?? 0) > 500) {
    // route to a slower, higher-memory path
}

Storage integration. parseFile() wants an absolute local path. For S3 or other remote disks, stream to a temp file first — the wrapper has no streaming API:

$tmp = tempnam(sys_get_temp_dir(), 'pdf_');
file_put_contents($tmp, Storage::disk('s3')->get($doc->path));
try {
    $result = $extractor->extract($tmp);
} finally {
    @unlink($tmp);
}

Chunk for RAG. Extracted text is unformatted prose — no column structure, no table delimiters, no reading-order guarantees across multi-column layouts. Before feeding it to an embedding model, split on page boundaries (from getPages()) so retrieval can cite a page, and treat obvious artifacts (runs of whitespace, orphaned headers) as expected noise rather than a bug to debug.

Gotchas and Tips

Auto-discovery does not work. The bundle's KreatysPdfParserExtension and Configuration never execute in Laravel. Any config keys you'd expect to set in a config/pdf_parser.php have no effect — the constructor arguments in Resources/config/services.yml are just defaults you must replicate in your own binding. Don't waste an afternoon wondering why your config change did nothing.

The wrapper is stateful. PdfParser holds an internal Document/Parser between calls. A singleton binding means the second request reads the first request's document when you call getText(). Always bind(), not singleton(), or use new PdfParser() per call. This also matters under Octane — long-lived workers and static state on the parser are a data-leak waiting to happen. If you must use Octane, instantiate fresh inside the job/handler every time.

Empty text is the dangerous failure, not the exception. A password-protected, truncated, or scanned PDF commonly does not throw — getText() returns ''. That looks like success downstream. Always distinguish empty from extracted:

$text = trim($parser->getText());
if ($text === '') {
    // Almost certainly image-only or encrypted — flag for OCR / manual review.
    // Do NOT store '' as a successful extraction.
}

Malformed, encrypted, and adversarial PDFs throw or hang. Wrap parseFile() in try/catch (\Throwable), but also set limits before the call: ini_set('memory_limit', '512M') inside the job, a job $timeout, and a hard upload size cap (max:20480 in the validator, plus a server-side post_max_size). Decompression bombs are cheap to construct and expensive to survive — cap the worker's memory and let the queue restart it.

Large files and multi-page PDFs will hit memory limits. The parser loads the whole document. A 5,000-page or 200 MB PDF may exceed a 128M PHP limit; you'll see Allowed memory size exhausted and sometimes a segfault rather than an exception. Mitigations: raise the job's memory limit temporarily, reject documents over a threshold up front, and never do this synchronously in a web request.

The smplot vs smalot fork is a due-diligence item. The description mentions smlot/pdfparser, but the well-known maintained library is smalot/pdfparser. Verify which one actually lands in composer.lock. If it's the fork, check commit recency and PHP version support before relying on anything beyond basic text extraction. The safest configuration is to depend on smalot/pdfparser directly and skip this wrapper — you get the maintained library, an actively tagged release history, and one less unmaintained dependency.

Constructor arguments live in YAML, not config. If new PdfParser() throws a TypeError about missing arguments, open Resources/config/services.yml and mirror the arguments in your service provider. The extension's default config values are visible in DependencyInjection/Configuration.php.

Method names may drift across versions. The assessment and the README are not authoritative — Service/PdfParser.php is. Grep for public function in that file and trust what you find rather than the docs. Methods like getPages(), getText(), parseContent() are the common ones, but per-page text access varies.

Static caches and Octane. If you deploy under Laravel Octane, any static property on the parser (or on smalot's Parser internals) survives between requests. Re-bind the wrapper per request and never cache a Document on the container.

Extension point: swap the implementation behind the contract. The wrapper's only real value is its API shape. Once PdfTextExtractor is in place, the migration path — smalot/pdfparser direct, pdftotext via symfony/process, or an HTTP client to Textract/Document AI — is a single adapter class. Don't let callers typehint the vendor class; the day you outgrow PHP-only parsing, that's a refactor instead of a one-file swap.

Debugging output. If extraction "works" but the text is garbage (missing sections, reversed order, split words), the cause is almost always the PDF's internal structure (image-only, rotated text, custom encoding), not the wrapper. Open the file in a viewer, try selecting text with the cursor — if you can't, no PHP parser will help and you need OCR (Tesseract, Textract) regardless of package choice.

No support channel. There's no issue tracker activity, no advisory feed, and no maintainer triage to lean on. Pin the exact version in composer.json, document an owner and a review date in your internal runbook, and budget a migration to the upstream library — the wrapper is a temporary convenience, not a foundation.

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.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console