itul/pdf-tools
itul/pdf-tools is a Laravel/PHP package offering practical PDF utilities for common document workflows, giving developers a simple, framework-friendly way to generate, manipulate, and manage PDF files with reusable helpers and clean integration.
itul/pdf-tools is a Bitbucket-hosted PDF utility package with no public documentation and no Packagist presence, so treat onboarding as source-reading rather than doc-reading.
Confirm repository access first. Because it lives on Bitbucket, Composer needs credentials before require will resolve. Add the VCS entry and authenticate:
{
"repositories": [
{ "type": "vcs", "url": "https://bitbucket.org/bmooreitul/pdf-tools" }
],
"require": { "itul/pdf-tools": "dev-master" }
}
Use a Bitbucket app password / OAuth consumer in auth.json (or CI secrets) so both local dev and pipelines can clone it.
Discover the API from the package itself. Open these in order:
composer.json — reveals whether it wraps a binary (Ghostscript/qpdf/pdftk), a PHP library (FPDI, mPDF, Dompdf), or is bespoke.src/ — the service provider and the main entry class tell you the public surface.config/ — anything publishable (php artisan vendor:publish).tests/ and README — the only usage examples you are likely to get.Publish and inspect config (if a config file ships):
php artisan vendor:publish --provider="Itul\\PdfTools\\PdfToolsServiceProvider"
Pay attention to binary paths (gs, qpdf, pdftk) and temp directories — these are the usual first-run failures.
First use case: a low-risk round-trip. Pick an operation you can verify byte-for-byte, e.g. splitting a small known PDF into pages and asserting page count, before touching HTML-to-PDF conversion or merging. This isolates install/config problems from logic problems.
Given the package has zero dependents and zero stars, assume no semver discipline and pin to a commit hash (dev-master#<sha>) rather than a floating branch.
Wrap it, don't sprinkle it. Introduce a single application-level service that owns the package, so a future replacement (or a fork of this abandoned-looking repo) is one file's worth of work:
final class PdfService
{
public function __construct(private PdfTools $pdf) {}
public function merge(array $paths, string $out): string
{
// package-specific calls isolated here
}
}
Bind this in a service provider and inject it — never resolve the package directly in controllers or jobs.
Push heavy operations onto the queue. Merging, compression, OCR, and HTML-to-PDF are CPU- and IO-bound and will blow through web request timeouts. Model each PDF operation as a job that writes to a disk and emits an event when done:
final class MergePdfs implements ShouldQueue
{
public function handle(PdfService $pdf): void
{
$out = "pdfs/{$this->uuid}.pdf";
$pdf->merge($this->inputs, Storage::path($out));
PdfMerged::dispatch($this->uuid);
}
}
Use a pipeline for multi-step workflows. A typical chain is upload → validate → normalize → transform → store → stream download. Keep each step a small method on your wrapper so you can test them independently and skip steps conditionally.
Separate storage from streaming. Write outputs to a dedicated disk (pdfs/), then return a response()->streamDownload() or a signed temporary URL. Avoid echoing PDF bytes from inside the package call — you lose the ability to retry or cache.
Test against fixture PDFs. Commit a couple of small PDFs under tests/Fixtures/. Assert on page count, file existence, and file size rather than binary equality. If the package exposes page-count or metadata helpers, use them to verify results instead of pulling in a second parser.
Validate and sanitize paths. Any filename or path coming from a user should be resolved against a known base directory and checked with realpath() before being handed to the package, which likely passes it to a shell binary.
composer install works locally but not in your pipeline, it is almost always a missing Bitbucket credential in auth.json or the CI secret store. Bake this into your Docker image build, not just a local script.dev-master can shift under you. Lock the hash and update deliberately.which gs qpdf) so a missing binary surfaces at deploy time, not on a user's first merge.0 with empty output, or write a truncated file on error. After every call, assert that the output file exists and is non-zero. Wrap calls in try/finally so temp files are unlinked even on exception — otherwise your storage/app/pdf/tmp directory grows forever.memory_limit. Set memory_limit and set_time_limit per job, or pass the work to a binary that runs out-of-process.env() values, remember php artisan config:cache in production will freeze those at build time — set binary paths and temp directories via config, and drive only the config values from env.Content-Disposition header.src/Contracts you can bind your own implementation to, and any events the package dispatches. If none exist, that is a signal to keep your integration thin — this package gives you little to hook into and may not be maintained.pdf:doctor) that reports resolved binary paths, versions, and a successful round-trip on a bundled fixture. It will save hours the first time a fresh environment misbehaves.How can I help you explore Laravel packages today?