blaspsoft/doxswap
DoxSwap is a Laravel package for working with DOCX templates: fill placeholders with data, generate Word documents, and convert them to PDF. It offers a simple, fluent API for building dynamic documents in PHP applications.
DoxSwap is best treated as a document rendering layer, not a full document management system. You keep .docx files as versioned templates in your app, pass a data array into the package, and receive a filled document that you can store, download, or attach to mail.
Minimal setup:
composer require blaspsoft/doxswap
php artisan vendor:publish --tag=doxswap-config
resources/templates/offers/offer-v1.docx
storage/app/templates/contracts/contract-v2.docx
{{ customer_name }}, {{ offer_total }}, {{ valid_until }}. Keep the first template deliberately plain: no images, loops, tables, or conditional blocks.A good first use case is an offer letter or invoice-style document: one template, one record, half a dozen scalar placeholders. That validates installation, placeholder syntax, storage paths, and download flow before you attempt tables or repeating sections.
A useful day-to-day structure is to wrap the package in your own small service:
namespace App\Services;
use App\Models\Offer;
use Illuminate\Support\Facades\Storage;
class OfferDocumentService
{
public function generate(Offer $offer): string
{
$template = Storage::disk('templates')->path('offers/offer-v1.docx');
$output = "offers/{$offer->id}/offer-{$offer->id}.docx";
// Replace with the package's actual facade/service calls.
app('doxswap')
->template($template)
->with($this->dataFor($offer))
->save(Storage::disk('documents')->path($output));
return $output;
}
private function dataFor(Offer $offer): array
{
return [
'customer_name' => $offer->customer->name,
'offer_number' => $offer->reference,
'offer_total' => number_format($offer->total, 2),
'valid_until' => $offer->valid_until->format('d M Y'),
];
}
}
This adapter gives you one place to change if the package API or placeholder syntax changes, and it keeps controllers free of document logic.
Use a template registry per document type. Avoid scattering template paths through controllers. Map a document type to a versioned template and keep that mapping in config or a small enum:
enum DocumentTemplate: string
{
case Offer = 'offers/offer-v1.docx';
case Contract = 'contracts/contract-v2.docx';
case CompletionCertificate = 'certificates/completion-v1.docx';
}
When wording changes, add offer-v2.docx and update the mapping. Never overwrite a live template in place; you lose the ability to reproduce old documents.
Normalize data before rendering. The package should receive scalar, display-ready values. Do formatting in PHP, not in the DOCX template:
private function dataFor(Contract $contract): array
{
return [
'client_name' => $contract->client->name,
'client_address' => $contract->client->address,
'start_date' => $contract->starts_at->format('d/m/Y'),
'end_date' => $contract->ends_at->format('d/m/Y'),
'fee' => '€' . number_format($contract->fee_cents / 100, 2),
'payment_terms' => $contract->payment_terms,
];
}
This avoids locale surprises, keeps templates editable by non-developers, and makes output testable.
Use queued jobs for anything nontrivial. DOCX processing is CPU and memory bound. For bulk generation, queued generation, or large templates, dispatch a job:
GenerateOfferDocument::dispatch($offer)->onQueue('documents');
Store the generated file on a private disk, record the path and template version on the model, then notify the user when ready. The request cycle should not wait for document rendering.
Return downloads through a controller with authorization. Never expose storage paths directly. Use a signed URL or an authenticated download route with a policy check:
public function download(Offer $offer)
{
$this->authorize('downloadDocument', $offer);
return Storage::disk('documents')->download($offer->document_path);
}
Keep generated files immutable and name them deterministically. Include the record ID, template version, and timestamp:
offers/2025/offer-1842-v1-20251005-143201.docx
This simplifies auditing, cleanup, and support requests.
Test with fixture templates and assertions on extracted text. Keep a tiny tests/Fixtures/templates/simple.docx and assert that the rendered document contains expected values and does not contain unresolved placeholders. If the package does not provide text extraction, unzip the generated .docx and inspect word/document.xml in the test.
Use events and observers for lifecycle integration. Fire a DocumentGenerated event after rendering, then attach listeners for mail delivery, activity logging, or pushing the file to S3. This keeps generation separate from delivery.
Add a preview path for internal users. Generate a watermarked or clearly labeled draft by using a draft template variant (e.g. offer-draft-v1.docx) and the same data mapping. This is useful for operations teams who need to review wording before sending.
{{ customer_name }} into multiple <w:r> elements when formatting, spell-check, or editing occurs. The result is a placeholder the package cannot find. Fix by typing placeholders in one pass, ideally pasting them as plain text, and avoid applying character formatting inside the braces. If replacement silently fails, unzip the file and inspect word/document.xml first.&, <, or >, verify that the package escapes values correctly. A value containing & that causes a corrupt document is a common integration failure..docx, rename it to .zip, extract it, and inspect word/document.xml. This is faster than guessing why a replacement did not appear.Storage abstraction and absolute paths from Storage::path() rather than string concatenation.composer.json. With an evolving document package, a minor release can change placeholder parsing or output behavior. Run your fixture-based tests in CI after every update.Day-to-day, the winning pattern is simple: versioned templates, a small data-mapping service per document type, queued rendering, private storage, authorized downloads, and fixture tests that catch placeholder breakage before users do.
How can I help you explore Laravel packages today?