vaites/php-apache-tika
PHP bindings for Apache Tika that extract text, HTML, and metadata from documents, images, and other formats. Run the JAR via CLI or use the faster Tika server mode over HTTP. Supports local and remote resources, OCR, and Apache Tika 1.15+.
The package is a thin, dependency-free PHP client — there is no Laravel service provider, facade, or config file to publish. You wire it yourself, which takes about five minutes.
1. Install the package
composer require vaites/php-apache-tika
2. Decide on a mode before writing any code. This is the single most important early decision:
apache/tika:latest-full on port 9998.tika-app.jar and it shells out to java -jar. Zero infrastructure, but each call pays JVM startup (~2–5s) and you must have a JRE on every host.For a Laravel app that will process uploads on a queue, use server mode. Add Tika to docker-compose.yml alongside MySQL and Redis:
tika:
image: apache/tika:latest-full # "full" includes OCR + more parsers
ports: ["9998:9998"]
3. Bind a configured client in a service provider. Because the underlying Client is a mutable singleton-ish object, register it as a singleton and configure it once in AppServiceProvider::register():
$this->app->singleton(\Vaites\ApacheTika\Client::class, function () {
$client = \Vaites\ApacheTika\Client::make(
config('tika.host', 'tika'),
config('tika.port', 9998)
);
$client->setTimeout(config('tika.timeout', 60));
return $client;
});
4. First use case: extract text from an uploaded PDF.
$text = app(\Vaites\ApacheTika\Client::class)
->getText(Storage::path($request->file('resume')->store('resumes')));
From there, the obvious next steps are getMetadata() for author/page-count/MIME data, and getJSON() when you want a single call returning both.
Where to look first: the README's mode comparison and the client source (src/Client.php, src/Client/AppClient.php, src/Client/ServerClient.php). There is no Laravel glue to read, so the client classes are the documentation.
Wrap the client in a domain service. Don't let Client leak into controllers — it has a wide surface (getText, getHTML, getJSON, getMetadata, getLanguage, detectLanguage, getMimeType, getRecursiveText) and you'll want to swap or mock it later.
final class DocumentExtractor
{
public function __construct(private \Vaites\ApacheTika\Client $tika) {}
public function extract(string $path): ExtractedDocument
{
$json = $this->tika->getJSON($path); // text + metadata in one round trip
return new ExtractedDocument(
text: $json->getText(),
metadata: (array) $json->getMetadata(),
);
}
}
Always extract on a queue for anything user-supplied. A 40-page scanned PDF with OCR can take tens of seconds; doing it synchronously in a POST handler will hit PHP's max_execution_time and your users' patience.
class ExtractDocumentJob implements ShouldQueue
{
public int $timeout = 300;
public int $tries = 3;
public function __construct(public Document $document) {}
public function handle(DocumentExtractor $extractor): void
{
$result = $extractor->extract(
Storage::disk('documents')->path($this->document->path)
);
$this->document->update([
'text' => $result->text,
'meta' => $result->metadata,
'indexed_at' => now(),
]);
}
}
Use getRecursiveText() for archives and email containers (.zip, .eml, .msg). Plain getText() on a zip returns the container's own metadata, not the contents — a common source of "why is my extraction empty?" bugs.
Pattern for search/RAG pipelines: use getText() for the chunked body you embed, and a separate cheap getMimeType() call as a gate before you spend time on parsing. Store the MIME Tika detects rather than trusting the client-supplied Content-Type or the file extension.
Testing. Bind a fake extractor in the container and assert jobs dispatch; keep one integration test tagged @group tika that hits a real server in CI. Do not attempt to unit-test the client itself — it is a transport.
Multi-tenant / per-tenant resources. If you run multiple Tika servers, key the singleton by tenant and resolve per-request rather than reconfiguring a shared instance.
setTimeout, setOCRLanguage, setMaxFileSize, setDownloadRemote, setUserAgent, setRetries mutate the instance and persist for later calls in the same request/worker. Configure once at resolution time; never toggle options mid-request.JobProcessing or accept occasional retries — set $tries > 1 and treat connection exceptions as retryable.setDownloadRemote(true) is a remote-file-inclusion footgun. If enabled, a user-supplied string passed as the "path" can make Tika fetch a URL (SSRF). Keep it off; always pass a real local filesystem path.depends_on. Tika takes ~10–20 s to boot. Add a healthcheck on GET /tika (or just /) before starting the app/worker container, otherwise the first job in CI fails.java on PATH and the JAR path to be absolute. A missing JRE throws a fairly opaque process-execution error. Pin the JAR version and store it outside vendor/ so a composer install can't wipe it.-Xmx512m default won't parse a 200 MB scanned PDF. Set JAVA_OPTS on the container and enforce a max upload size in Laravel that is smaller than what Tika can handle.setTimeout covers the HTTP read; PHP's max_execution_time and the queue's $timeout cover the job. Set them in descending order (queue > Tika timeout > 2× expected parse time).setOCRLanguage('eng'). The latest-full image includes it; the slim image does not..doc/.txt files can come back mangled. Run the output through mb_convert_encoding($text, 'UTF-8', 'auto') before storing, and make the DB column utf8mb4.mb_strlen($text) === 0 and mark the document as needing OCR rather than treating it as success.getText/getJSON calls and map to your own domain exception so retries treat them as transient, and so callers don't need to catch (\Throwable).TIKA_CONFIG on the server, or (c) post-process the returned text. There is no hook to intercept the request.How can I help you explore Laravel packages today?