ResumeJSON

Resume parser PHP: what exists, the code, and when to call an API

If you are looking for a resume parser PHP developers can install today, the honest answer is that PHP has excellent document readers and no maintained resume parser. smalot/pdfparser will hand you the text of a CV, PHPWord will hand you the text of a .docx, and both are well kept — but neither knows what a job title is. The one PHP package that promised resume fields is archived. So the real decision is not which library to composer require; it is whether you write the field extraction yourself or call something that returns typed JSON. This article gives you the code for both routes, and the point where the second one wins.

Every version, date and price below was read from Packagist, GitHub or the vendor's own pricing page as of September 2026. We build ResumeJSON, a per-parse resume parsing API, so read the last section as written by an interested party — the library facts are checkable with one composer show, and you should check them.

The PHP packages, compared

PackageLatest stablePHP it declaresWhat it returnsMaintained?
smalot/pdfparserv2.12.5, 17 April 2026>= 7.1Text, pages and metadata from a PDFYes
phpoffice/phpword1.4.0, 5 June 2025^7.1 | ^8.0Text and structure from DOCX, ODF, RTF, HTMLYes
praem90/Resume-Parsernot statedName, email, phone, date of birthNo — repository archived 11 February 2025
sharpapi/laravel-resume-parser>= 8.1, Laravel >= 10.48.29Structured resume JSON, via SharpAPI's hosted APIYes, as an API client

Two columns decide this for you. "What it returns" is the gap you will be filling yourself: the two maintained libraries are document readers, so everything between "here is 4,000 characters of text" and "here is this candidate's third-most-recent employer" is your code. "Maintained?" is the risk you are taking on, and it is a sharper risk here than in most library choices, because a resume parser is a pile of heuristics about how humans format documents, and those formats keep moving.

Reading the file: the part PHP does well

Whatever route you choose, you need the document's text, and this is a solved problem in PHP. smalot/pdfparser describes itself as a library that "can read and extract information from pdf file", has been installed over 48 million times, and is pure PHP with no system binary to deploy:

composer require smalot/pdfparser
use Smalot\PdfParser\Parser;

$text = (new Parser())->parseFile('/path/to/cv.pdf')->getText();

For .docx, PHPWord is the equivalent — "a pure PHP library for reading and writing word processing documents", 44 million installs, covering OOXML, ODF, RTF and HTML:

$phpWord = \PhpOffice\PhpWord\IOFactory::load('/path/to/cv.docx');
$text = '';
foreach ($phpWord->getSections() as $section) {
    foreach ($section->getElements() as $element) {
        if (method_exists($element, 'getText')) {
            $text .= $element->getText() . "\n";
        }
    }
}

Two caveats worth knowing before you build on this. smalot/pdfparser's own documentation says secured documents and form data are not supported. And neither library touches a scanned CV: a PDF that is a photograph of a page contains no text layer, so getText() correctly returns almost nothing. If your upload form is open to the public, a meaningful share of what arrives is scanned, and text extraction alone will silently return empty candidates for those files. That "silently" is the dangerous part — an empty result looks exactly like a CV with nothing in it.

The archived PHP resume parser, and why it is still worth reading

Search this topic and you land on praem90/Resume-Parser, a PHP project whose README says it parses resumes for "fields like name, email, ph, date of birth" across doc, docx, xlsx, pptx and pdf. It is real and it is small — 17 stars, 9 commits — and its GitHub page states it was archived on 11 February 2025. Archived means read-only: no fixes, no new formats, no security patches.

Read it anyway, because it shows you the shape of the work honestly. Names, emails, phone numbers and dates of birth are the fields regular expressions can reach. Everything a hiring workflow actually sorts on — employer, title, the start and end dates of each role, which role is current, how many years of experience that adds up to — needs structure this approach does not have. That is why the archived package stops where it does, and it is the same wall you hit the afternoon you decide to write your own.

Writing your own: what it really costs

If you extract with smalot/pdfparser and then match fields yourself, budget for these, in roughly this order of pain:

None of that is impossible. It is simply a body of work that does not get finished, because the input is human formatting, and it is worth being clear-eyed that the maintenance never ends rather than discovering it in month four.

Calling an API from PHP instead

The other route is to keep PHP doing what PHP is good at — receiving the upload, running the workflow — and let something else return the fields.

If you are on Laravel, sharpapi/laravel-resume-parser is a maintained client for SharpAPI's hosted parser: MIT licensed, PHP >= 8.1, Laravel >= 10.48.29, and it accepts eleven formats including flattened, image-based PDFs. Note its flow before you design around it: parseResume() dispatches a job and returns a status URL, and you then poll with fetchResults($statusUrl) until the status is success. That is a good fit for a queued import and a poor one for a web request a person is waiting on. Its pricing is per plan rather than per parse — as of September 2026 the published tiers are Build at $50, Launch at $200 and Scale at $500 a month, quoted in words per month with an annual discount, plus a custom Enterprise tier.

ResumeJSON is the per-parse, synchronous shape of the same idea, which is the difference that matters in a Controller: one HTTP call, one response, no job table. It is sold on RapidAPI, so the call is a plain POST with your RapidAPI key:

$response = Http::withHeaders([
    'X-RapidAPI-Key' => config('services.resumejson.key'),
    'X-RapidAPI-Host' => config('services.resumejson.host'),
])->post('https://<your-rapidapi-host>/v1/parse', [
    'text' => $text,   // from smalot/pdfparser, or send the file instead
]);

$resume = $response->json('resume');
$currentRole = collect($resume['work'])->firstWhere('is_current', true);

You can skip the extraction step entirely and forward the upload as multipart/form-data with a file part — the type is detected from the file's own bytes, so an upload your framework labelled application/octet-stream still works. What comes back is a typed document: basics, work, education, skills, certifications, languages and total_years_experience, with a field the CV does not state returned as null rather than guessed. Try it with no signup on the free parser, and the comparison page covers how it lines up against the enterprise vendors.

So which one should you pick?

The PHP question turns out not to be a PHP question at all. The language has the readers it needs; what nobody in this ecosystem maintains is the résumé-shaped part. Decide whether that part is your product, and the rest follows.

For the same map in another language, see Python resume parser: the libraries that work and Open source resume parser: what works, and when to buy instead.

All articles