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

Extractor Laravel Package

helgesverre/extractor

Extractor is a Laravel package that uses OpenAI to pull structured data out of images, PDFs, emails, Word docs, and web pages. Define fields, get arrays or Spatie data objects back—no custom parsing logic needed.

View on GitHub
Deep Wiki
Context7

Getting Started

Three commands get you to a working extraction call:

composer require helgesverre/extractor
php artisan vendor:publish --tag="extractor-config"
php artisan vendor:publish --provider="OpenAI\Laravel\ServiceProvider"

Then add OPENAI_API_KEY to .env. Note the second publish step — this package is a thin layer over openai-php/laravel, so the OpenAI config file and key are not optional extras, they are the actual transport.

Where to look first

  1. config/extractor.php after publishing — this holds the default model, Textract/AWS settings, and any global defaults.
  2. config/openai.php (from the published OpenAI config) — the API key, base URI, organization, and request timeout live here, not in the Extractor config.
  3. The README's two tables: the Text::* extraction methods and the built-in extractor list. Those two tables are effectively the API surface.

First use case: pull fields out of an invoice or CV.

use HelgeSverre\Extractor\Engine;
use HelgeSverre\Extractor\Facades\Extractor;
use HelgeSverre\Extractor\Facades\Text;
use Illuminate\Support\Facades\Storage;

$text = Text::pdf(Storage::get('invoices/2024-001.pdf'));

$data = Extractor::fields($text, fields: [
    'vendor' => 'the company issuing the invoice',
    'invoiceNumber',
    'total' => 'the grand total as a number',
    'lineItems' => [
        'description',
        'quantity' => 'integer',
        'unitPrice' => 'number',
    ],
], model: Engine::GPT_4O_MINI);

This is the right first test because it exercises the whole path — binary document → text → structured array — and will surface your first gotcha (model output types) immediately.

Implementation Patterns

Treat it as an ETL stage, not a controller concern. The idiomatic shape is: a Text::* call to normalise input into a string, then Extractor::fields() or a custom extractor, then a persistence step. Keep the raw text if you can afford the column — when an extraction goes wrong, having the source text lets you re-run with a new prompt without re-fetching or re-OCRing the document.

Choosing the text path. The Text methods all take raw content (file_get_contents() / Storage::get()), not file paths, and they differ meaningfully in cost and reliability:

Input Method Notes
Digital PDF Text::pdf Free, local, smalot/pdfparser. Fastest and cheapest — try this first.
Scanned PDF / photo Text::textract AWS OCR, base64 sync call, size/page limited
Large or multi-page Text::textractUsingS3Upload Upload + poll; needed for anything Textract sync rejects
.docx Text::word Local unzip + XML parse
Web page Text::web($url) Fetches, strips tags, collapses whitespace
Image ImageContent::file() + Vision Skips OCR entirely — see below

Skip OCR when you can. For images, ImageContent + a vision model removes an entire dependency (AWS credentials, S3 bucket, polling loop) and is usually more accurate than OCR-then-extract, because the model sees layout. It's marked WIP in the README, so wrap it and keep it behind a feature flag, but for one-off image extraction it is the shortest path:

$image = ImageContent::file($path); // or ::raw($bytes), ::url($url)
$data = Extractor::fields($image, fields: ['productName', 'price'], model: Engine::GPT_4O);

Custom extractors for anything reused. If the same field set appears in more than one place, promote it to a class. The contract is two methods: prompt() builds the instruction string, expectedOutputKey() names the JSON root key. Register once in a service provider and call it by name:

Extractor::extend('job-posting', fn () => new JobPostingExtractor());
// ...
Extractor::extract('job-posting', $content);

Built-in Contacts and Receipt extractors work the same way via Extractor::extract(Receipt::class, $text), which is useful when you want a typed result without maintaining a prompt yourself.

Prefer fields() for one-off shapes and a class when you need validation or a Spatie data object. The fields() array is expressive — descriptions, nesting, type hints — but it has no validation layer. If the shape must be guaranteed, use a custom extractor that returns a spatie/laravel-data object and let the DTO do the coercion and rejection.

Queue it. Every extraction is a network round-trip to an LLM with a variable 2–30 second latency. A ProcessDocumentExtraction job with retries and a per-document token/usage log is the pattern that survives production. Do the Text::* step inside the job too — Textract polling on a multi-page PDF can take minutes.

Provider flexibility is the real integration win. Because it rides on openai-php/laravel, OPENAI_BASE_URI alone switches you to Ollama, Groq, or Together:

OPENAI_BASE_URI="http://localhost:11434/v1"

That makes local development and CI free, and gives you a credible escape hatch if OpenAI pricing or availability becomes a problem. Azure needs explicit wiring — override the ClientContract singleton in AppServiceProvider with the custom base URI, api-key header, and api-version query param, as shown in the README.

Gotchas and Tips

Two classes named Extractor. HelgeSverre\Extractor\Extractor is the static registry (extend()), and HelgeSverre\Extractor\Facades\Extractor is the facade you call fields()/extract() on. The README imports one for registration and the other for usage; mixing them up in a single use block is the most likely first-hour stumble.

JSON mode nests everything under a root key. This is why expectedOutputKey() exists. If you write a custom extractor and skip both the "nest under $outputKey" instruction in the prompt and the method override, your payload comes back wrapped in an extra object and every field read returns null. Symptom: extraction "succeeds" but the result is empty or one level deeper than expected.

Model output is not typed. 'quantity' => 'integer' is a hint to the model, not a cast. Prices arrive as "1 299,00 kr", "$1,299.00", or 1299 depending on the day and the document. Always validate/cast on your side; never bind the raw array straight to a database column. Dates and currencies are the two worst offenders — give explicit format instructions in the field description and still normalise afterwards.

Timeouts default to 30s and that is not generous. OPENAI_REQUEST_TIMEOUT=60 is called out in the README for a reason: a long PDF plus a large maxTokens will blow through the default and throw a connection exception. Set it to 60–120 and use queue retries with backoff rather than a synchronous user-facing request.

maxTokens must cover the output, not the input. Set it too low and the JSON arrives truncated, which fails to parse and looks like a prompt problem. For nested lists (line items, work history), the output is much larger than you expect — 4000 is a reasonable starting point, and a truncation failure usually means "raise this", not "the AI is bad".

Text::web() performs an outbound HTTP request on your behalf. If the URL can come from a user, you have an SSRF vector — validate against allowed hosts, block private IP ranges, and cap response size. The same applies to ImageContent::url().

OCR vs. parser is a silent accuracy cliff. Feeding a scanned PDF to Text::pdf returns near-empty text without erroring, and the LLM then "extracts" plausible fields from nothing. Guard with a cheap sanity check: if the extracted text length is under ~50 characters, route to Textract instead of calling the model.

Textract has real infrastructure requirements. Text::textractUsingS3Upload needs an S3 bucket, credentials, and IAM permissions; the sync variant has documented size/page limits. If you're not already on AWS, Vision or Text::pdf is a far cheaper path — don't adopt Textract for a single document type.

Watch memory on large files. Every method takes the full document as a string in memory. A 50 MB scanned PDF read in a request worker is a bad time; do it in a job with a raised memory_limit.

Vendor risk is worth mitigating. One maintainer, ~226 stars, zero dependents, MIT. The library is a genuinely useful prompt-and-plumbing layer, but that layer is thin — if it goes stale, the OpenAI SDK underneath still works. Isolate it: your own DocumentExtractor interface, the package's facade calls in one or two adapter classes, and a recorded-fixture test suite. Then migrating to a hand-rolled OpenAI::chat() call is an afternoon, not a rewrite.

Testing. Bind a fake ClientContract in the container (the OpenAI Laravel package supports this) and assert your code handles a well-formed response, a malformed JSON body, and a timeout. The LLM itself is not the thing to test — your parsing, casting, and failure handling are.

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.
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
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource