jmd/unoconv-bundle
A Symfony bundle that wraps unoconv to convert office documents such as DOCX, ODT, XLSX and PPT into PDF or other formats using LibreOffice. It exposes a simple service for handling conversions in your PHP applications, keeping document processing out of your main codebase.
Know what you're installing first. jmd/unoconv-bundle is a Symfony 2/3 bundle, not a Laravel package. It has no ServiceProvider, no facade, no config publish tag. It was last released in 2016 and is archived, so treat it as a reference implementation for wrapping unoconv — not as a dependency you composer require into a Laravel app and expect to autowire.
Practical first steps:
unoconv) plus a small DependencyInjection/Configuration.php for defaults. Clone it, read Resources/config/services.yml, then reimplement 40 lines in app/Services/.apt-get install -y unoconv libreoffice-writer libreoffice-calc libreoffice-impress \
fonts-liberation fonts-dejavu
unoconv --version && soffice --version
unoconv needs a reachable LibreOffice UNO listener. Either let it spawn one per call (slow, ~2–5s cold start) or run a persistent one:
unoconv --listener --server 127.0.0.1 --port 2002 &
.docx/.xlsx/.pptx → PDF stored in storage/app/converted/{uuid}.pdf, triggered from a controller or (better) a queued job.If you genuinely are on Symfony legacy, register JMD\UnoconvBundle\JMDUnoconvBundle in AppKernel.php and configure the jmd_unoconv node. On Laravel, skip that entirely and use symfony/process (already a transitive dep of many packages, and Laravel 10+ ships Illuminate\Process).
Minimal Laravel-shaped equivalent:
// app/Services/UnoconvConverter.php
use Illuminate\Support\Facades\Process;
class UnoconvConverter
{
public function toPdf(string $source, string $outDir): string
{
$outDir = rtrim($outDir, '/');
$result = Process::timeout(120)->run([
'unoconv',
'--server', '127.0.0.1', '--port', '2002',
'-f', 'pdf',
'-o', $outDir.'/',
$source,
]);
abort_unless($result->successful(), 500, $result->errorOutput());
$pdf = $outDir.'/'.pathinfo($source, PATHINFO_FILENAME).'.pdf';
abort_unless(is_file($pdf) && filesize($pdf) > 0, 500, 'unoconv produced no output');
return $pdf;
}
}
Wrap it behind an interface. This is the single highest-value habit here, because you will outgrow unoconv:
interface DocumentConverter {
public function convert(string $path, string $format): string;
}
// AppServiceProvider
$this->app->bind(DocumentConverter::class, UnoconvConverter::class);
When unoconv inevitably gives you trouble you swap in a Gotenberg HTTP client or a soffice --headless implementation without touching call sites.
Always convert inside a queued job. Conversions are 1–30s of CPU and spawn child processes; they do not belong in a request cycle.
class ConvertDocument implements ShouldQueue
{
public int $timeout = 180;
public int $tries = 3;
public array $backoff = [10, 60];
public function __construct(public int $documentId) {}
public function handle(DocumentConverter $converter)
{
$doc = Document::findOrFail($this->documentId);
$out = storage_path("app/converted/{$doc->uuid}.pdf");
$converter->convert(Storage::path($doc->path), 'pdf');
$doc->update(['converted_path' => $out, 'status' => 'converted']);
}
public function failed(\Throwable $e) { /* mark doc failed, log stderr */ }
}
Persistent listener + per-job profile for concurrency. The listener avoids cold starts, but LibreOffice cannot share a single user profile across simultaneous conversions. Give each worker its own:
'-env:UserInstallation=file:///tmp/lo-profile-'.getmypid()
…or serialize with a dedicated single-worker queue (queue:work --queue=conversions) plus WithoutOverlapping middleware.
Format mapping. -f takes a unoconv filter name, not always an extension: pdf, docx, xlsx, pptx, html, txt, csv. Derive it from the requested target extension and whitelist it — never interpolate user input into the filter or path.
Model the state machine. Add conversion_status (pending|processing|converted|failed) and conversion_error to the documents table. unoconv failures are opaque; persisting stderr is the only sane debugging path.
Validate before you shell out. mimes:doc,docx,xls,xlsx,ppt,pptx,odt,rtf,txt + a max: size. Reject anything not on the whitelist before LibreOffice parses it — the parser is the attack surface.
Cleanup pattern. Convert into a per-job temp dir, then move the artifact into permanent storage. unoconv writes next to the source by default, so always pass explicit -o.
composer require jmd/unoconv-bundle on Laravel 10/11 gets you an unused bundle and a stale lock entry. Copy the idea, not the code.unoconv itself is effectively unmaintained. Python 2 heritage, patchy on Python 3, and behavior shifted across LibreOffice 5→7. Pin your distro's versions in the Docker image and don't apt upgrade blindly.is_file() && filesize() > 0, and validate the magic bytes (%PDF) instead of trusting the extension.soffice.bin processes are the #1 operational failure. When a job times out, the child survives, holds the profile lock, and every subsequent conversion hangs. Add a reaper: kill processes older than N minutes matching soffice for your app user, or run conversions in a short-lived container.$HOME must exist and be writable by www-data. LibreOffice writes to ~/.config/libreoffice; a user with /usr/sbin/nologin and no home dir fails with a cryptic "Failed to connect to socket".exec("unoconv {$file}"). Filenames contain spaces, quotes, unicode. Use Process::run([...]) with array args — argv, not a shell string.-env:UserInstallation, or a single-worker queue. Pick one deliberately.fonts-liberation, fonts-dejavu, plus any brand fonts, and visually diff one output.--network=none), drop the macro execution path, and cap CPU/memory with cgroups. This is a strong argument for a Gotenberg sidecar over in-process shelling.sudo -u www-data unoconv -f pdf -o /tmp/out /tmp/in.docx — and log stdout + stderr + exit code from the Process result. unoconv --listener logs to the foreground only; capture it under supervisor.POST /forms/libreoffice/convert, no shared profiles, no zombies) is a drop-in later.soffice --headless --convert-to pdf under a queue worker. Reach for this bundle's pattern only as a shape reference for the thin DocumentConverter abstraction.How can I help you explore Laravel packages today?