Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Document Templates Laravel Package

42coders/document-templates

42coders/document-templates is a Laravel package for creating reusable document templates with dynamic placeholders. Map model or array data into template variables and render personalized invoices, contracts, and reports, simplifying data-driven document generation and export workflows in your appl...

View on GitHub
Deep Wiki
Context7

Getting Started

Install and scaffold. The package follows the standard Spatie package-tools layout, so the three commands you'll run first are:

composer require 42coders/document-templates

php artisan vendor:publish --tag="document-templates-migrations"
php artisan migrate

php artisan vendor:publish --tag="document-templates-config"

If you want the admin UI (list/upload/edit templates) rather than wiring your own controllers, also install the companion Filament plugin (42coders/filament-document-templates) and register it as a panel plugin. Most of the package's 200 stars come from that pairing — the core package is effectively a headless renderer.

Where to look first.

  1. config/document-templates.php — storage disk, route prefix, placeholder delimiters, and any PDF conversion driver. Read this before anything else; every surprising default lives here.
  2. The DocumentTemplate model (src/Models/DocumentTemplate.php) — the public surface is small: the template file, its parsed placeholders, and a render/generate method.
  3. src/DataSources/ — one abstract base plus a couple of concrete resolvers (typically a model/Eloquent source and a static/array source). Copy one to start.
  4. routes/*.php in the package — note the middleware. The editor endpoints are usually behind auth by default but rarely behind a specific policy; that's your job to add.

First use case in 10 minutes. Create a .docx in Word containing:

Dear ${customer_name}, your invoice ${invoice_number} for ${total} is due ${due_date}.

Upload it as a DocumentTemplate, bind the placeholders to a Customer / Invoice data source, and render:

$template = DocumentTemplate::findOrFail($id);

$path = $template->generate([
    'customer_name'  => $invoice->customer->name,
    'invoice_number' => $invoice->number,
    'total'          => number_format($invoice->total, 2),
    'due_date'       => $invoice->due_at->format('d/m/Y'),
]);

return response()->download($path);

Treat the method name as provisional — check the model for the actual generate / render / download names, since the README and source have drifted across versions.

Implementation Patterns

Render-and-stream in a controller. The most common integration is a controller action that resolves data and returns a download without persisting the output:

public function download(Invoice $invoice)
{
    $template = DocumentTemplate::findOrFail($invoice->template_id);

    return response()->streamDownload(function () use ($template, $invoice) {
        echo $template->generate($invoice->toPlaceholderArray());
    }, "invoice-{$invoice->number}.docx", [
        'Content-Type' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
    ]);
}

Custom DataSource per domain object. Rather than shoving an array into every call site, give each model a source class that owns the mapping:

class InvoiceDataSource extends DataSource
{
    public function __construct(private Invoice $invoice) {}

    public function getData(): array
    {
        return [
            'customer_name'  => $this->invoice->customer->name,
            'invoice_number' => $this->invoice->number,
            'total'          => number_format($this->invoice->total, 2),
            'due_date'       => $this->invoice->due_at->format('d/m/Y'),
        ];
    }
}

This keeps placeholder names stable across templates and gives you one place to add money_format/date-format changes when finance asks for a new locale.

Version templates instead of mutating them. Treat the uploaded .docx as immutable once referenced. Add your own document_template_versions table (or use a parent_id column on the templates table) and always render from a pinned version. When a user edits a template, insert a new row rather than overwriting the file — otherwise every historical document silently becomes un-reproducible.

Queue generation for anything user-triggered. Docx rendering holds the whole file in memory as a PHPWord object. For batches (monthly statements, bulk contracts), push a job:

GenerateInvoices::dispatch($batch)->onQueue('documents');

Store generated files on a private disk and hand out signed temporary URLs, not public paths.

Wire it into mail. The generated path can be passed straight into ->attach():

Mail::to($invoice->customer)->send(
    (new InvoiceMail($invoice))->attach($path, ['as' => 'invoice.docx'])
);

Filament panel as the editing surface. Register the plugin and point it at your template model. Editors then upload docx files, the plugin parses placeholders from the file, and you bind each placeholder to a data-source column. It's the fastest way to hand the template editor to non-developers — just make sure the panel resource is gated by an admin role and not exposed to end customers.

Extension points to design around. Subclass DataSource for new sources; override the published config for disk and delimiters; listen to model events on DocumentTemplate to bust any cached parsed-placeholder map; and if you need PDF, expect to call out to LibreOffice headless yourself — the package's PDF story is thin and usually delegated to a separate driver.

Gotchas and Tips

The run-splitting trap. If you type ${customer_name} in Word, Word often stores it as three separate XML runs (${, customer, _name}). Naive string replacement then silently leaves the placeholder in the output. If you author templates by hand, use Word's "Insert > Field" or paste the placeholder from Notepad to keep it in one run; if you let end users author them, expect to write a run-merging pass over the docx XML before rendering. This is the single most common "why is my template not filling in" bug with any PHPWord-based package.

.docx only. Old .doc binaries and .odt files won't be touched. Validate the extension and MIME type on upload and reject anything else with a clear error.

Headers, footers, and images are a separate code path. PHPWord's template processor replaces placeholders in the main body by default. Placeholders in headers/footers, text boxes, or images require explicit iteration over sections. Test a template with a footer before promising it works.

Keep uploaded templates out of public/. These files routinely contain salary data, contract clauses, or customer PII in the sample content. Store on a private disk (storage/app/private/document-templates), and never serve the source to the browser — only the rendered output, and only through signed URLs.

Lock down the editor routes. The published routes give an authenticated user the ability to upload a template. Placeholders bound to sensitive data sources (e.g. a User model password column) will happily resolve if a user types ${password} as a placeholder. Maintain an allow-list of permitted placeholder-to-source bindings and reject the rest at save time.

Normalize placeholder names. Decide early whether ${Customer Name} and ${customer_name} are the same placeholder. The package doesn't enforce it; pick a convention (snake_case, no spaces) and add a regex validator on save.

Cache parsed placeholders by file hash, not by template ID. If you cache placeholder lists, keying by ID means a re-upload keeps serving stale placeholder bindings. Hash the file and regenerate the cache entry when the hash changes.

Memory and time limits on large docs. Table-heavy templates with thousands of rows can blow past 128MB / 30s. Raise memory_limit per-request in the controller (or in the queued job) and stream the response instead of building the whole string.

Testing. Use Storage::fake() in tests and assert against the file contents rather than the filesystem. A useful assertion helper:

Storage::disk('local')->assertExists($path);
$this->assertStringNotContainsString('${', Storage::disk('local')->get($path));

The second assertion is a cheap sanity check that every placeholder was resolved.

Version-pin the package. It's still in the 0.x line — a minor bump can rename generate(). Pin with ~0.3 or whatever the current tag is, and read the changelog before upgrading.

Don't fight the UI. If you find yourself heavily customizing the Filament resource, you're probably better off building your own minimal upload/list screens against the package's model and keeping only the renderer. The package is a renderer first and a UI second; treat the UI as optional and disposable.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator