PHPackages                             stann/google-docs-handlebars - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. stann/google-docs-handlebars

ActiveLibrary

stann/google-docs-handlebars
============================

Render a Handlebars-subset template living in a Google Doc, in place, through the Docs API

v1.1.0(1mo ago)01.0k↑71.2%proprietaryPHPPHP &gt;=8.2

Since Jul 11Pushed 1mo agoCompare

[ Source](https://github.com/stannapp/google-docs-handlebars)[ Packagist](https://packagist.org/packages/stann/google-docs-handlebars)[ RSS](/packages/stann-google-docs-handlebars/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (2)Dependencies (1)Versions (3)Used By (0)

Google Docs Handlebars
======================

[](#google-docs-handlebars)

Renders a Handlebars-subset template **inside a Google Doc, in place**, through the Google Docs API.

Text template engines produce a string. This one produces a plan of `documents.batchUpdate` operations: table rows are duplicated for loops, discarded blocks are deleted, and every `{{tag}}` is replaced — while the document keeps its layout, styles, headers and footers. The document itself (usually a fresh copy of a template document) is the rendering target.

The package has **no dependency** — not even on an HTTP client: you hand it a tiny `DocumentClient` adapter and it never talks to the network itself.

Installation
------------

[](#installation)

```
composer require stann/google-docs-handlebars
```

Quick start
-----------

[](#quick-start)

```
use Stann\GoogleDocsTemplate\Client\DocumentClient;
use Stann\GoogleDocsTemplate\TemplateEngine;

// 1. Adapt your Google API access (PSR-18, SDK, fake in tests...).
final class MyDocumentClient implements DocumentClient
{
    // GET https://docs.googleapis.com/v1/documents/{id}, JSON-decoded.
    public function getDocument(string $documentId): array { /* ... */ }

    // POST https://docs.googleapis.com/v1/documents/{id}:batchUpdate
    public function batchUpdate(string $documentId, array $requests): void { /* ... */ }
}

// 2. Render against display-ready values.
(new TemplateEngine())->render(new MyDocumentClient(), $documentId, [
    'date' => '10/07/2026',
    'customer' => [
        'name' => 'Syndic des Lilas',
        'address' => '3 rue des Lilas, 75020 Paris',
    ],
    'quotation' => [
        'reference' => '20260701',
        'totalAmount' => '1 234,50 €',
    ],
    'items' => [
        ['name' => 'Dératisation', 'quantity' => '2', 'totalAmount' => '590,00 €'],
        ['name' => 'Désinsectisation', 'quantity' => '1', 'totalAmount' => '644,50 €'],
    ],
]);
```

Values must be **display-ready strings**: the engine never formats numbers, dates or currencies — that is the integrator's concern.

Template syntax
---------------

[](#template-syntax)

The grammar is a strict subset of Handlebars. Templates stay portable: what renders here renders identically under handlebars.js.

ConstructExampleNotesVariable`{{customer.name}}`Dotted paths into the context.Comment`{{!-- draft note --}}` / `{{! note }}`Removed from the output.Condition`{{#if quotation.reference}} ... {{/if}}`Keeps or deletes the block.Negated condition`{{#unless customer.address}} ... {{/unless}}`Inverse of `#if`.Loop`{{#each items}} ... {{/each}}`On a **table row** — see below.Loop item`{{item.name}}`, `{{item.totalAmount}}`Current row of the loop.Loop index`{{@index}}`Zero-based position.Helper`{{date "DD/MM/YYYY" quotation.dueAt}}`Registered at construction — see below.Truthiness follows Handlebars: absent, `null`, `false`, `''`, `0` and empty lists are falsy.

A variable whose path does not exist in the context is **left as-is** in the document, so template mistakes stay visible instead of silently disappearing.

### Helpers

[](#helpers)

The engine ships no helper: formatting is the integrator's concern. Register them at construction; arguments reach the callable in template order, string literals as written and paths resolved through the context ('' when absent):

```
$engine = new TemplateEngine(helpers: [
    'date' => fn(string $format, string $value): string => /* ... */,
    'money' => fn(string $amount, string $currency = 'EUR'): string => /* ... */,
]);
// {{date "DD/MM/YYYY" quotation.dueAt}} calls $helpers['date']('DD/MM/YYYY', '2026-07-31T00:00:00+02:00')
```

An unregistered helper raises `UnsupportedFeatureError`; helpers cannot be applied to loop variables (`item.*`, `@index`) yet.

### Everything else is rejected on purpose

[](#everything-else-is-rejected-on-purpose)

`{{else}}`, partials (`{{> x}}`), subexpressions, `{{#with}}`, block parameters (`as |x|`), nested blocks, parent references (`../`), `@first` / `@last`, and triple-stache (`{{{x}}}`) raise an `UnsupportedFeatureError` with a message suitable for template authors. Malformed constructs (unclosed block, mismatched closer, invalid path) raise a `TemplateSyntaxError`.

Placement rules (document structure)
------------------------------------

[](#placement-rules-document-structure)

Because rendering mutates a real document, blocks must map onto units the Docs API can manipulate:

- `{{#each}}`: at most **one per document**, and its opening and closing tags must live **inside the same table row**. That row is the prototype: it is duplicated once per item (and deleted when the list is empty). `{{item.*}}`and `{{@index}}` are only valid inside that row.
- `{{#if}}` / `{{#unless}}`: both tags in the **same table cell**, or both entirely **outside any table**. A discarded block outside a table must not contain a table (partial table deletions are illegal in the Docs API), and conditions cannot live in the loop row.

Violations raise a `TemplateStructureError` explaining what to move.

**Headers, footers and footnotes** are replace-only surfaces: they are parsed like the body, so `{{variables}}` (whatever their spacing) and comments work there, but blocks and loop variables are rejected — the Docs API cannot structurally edit those segments.

How it renders
--------------

[](#how-it-renders)

`TemplateEngine::render()` issues at most three `batchUpdate` calls:

1. **Structure** — delete discarded condition blocks, insert the empty loop rows (bottom-up, so indexes stay valid).
2. **Loop fill** — after re-reading the document (insertions shifted every index), write each copy's cell texts, pre-rendered per item, preserving the prototype row's text style and alignment.
3. **Replacements** — global `replaceAllText` for variables, comments and loop markers; the prototype row becomes item #0.

Nothing is sent when a step has no work to do.

Errors
------

[](#errors)

Every failure extends `Stann\GoogleDocsTemplate\Exception\TemplateError`:

ErrorMeaning`TemplateSyntaxError`Malformed construct (unclosed block, invalid path...). Carries the offending `->tag`.`UnsupportedFeatureError`Valid Handlebars, outside the supported subset. Carries `->tag`.`TemplateStructureError`Tags valid but placed incompatibly with the document structure. Carries `->tag` when identifiable.`InvalidDocumentError`The fetched document resource itself is unusable (no body...).All messages are written for the person editing the template.

The public API is `TemplateEngine`, the `DocumentClient` interface, the exceptions and the `Testing\` namespace — everything else is `@internal`.

Testing
-------

[](#testing)

The `Stann\GoogleDocsTemplate\Testing` namespace ships test doubles so integrators can test without any Google access:

- `Testing\GoogleDocBuilder` builds realistic Docs API document payloads (correct index arithmetic for paragraphs, tables, rows, cells, headers, footers and footnotes);
- `Testing\FakeDocumentClient` records `batchUpdate` requests and replays document snapshots.

The suite itself runs offline:

```
composer install
vendor/bin/phpunit
```

Requirements
------------

[](#requirements)

PHP ≥ 8.2 with `ext-mbstring`. No other dependency.

###  Health Score

44

—

FairBetter than 90% of packages

Maintenance90

Actively maintained with recent releases

Popularity21

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity47

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

2

Last Release

49d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/647a717d67e0f2742e4cb778effba9fb33d1f762dc57e39f3955772655363267?d=identicon)[camillebaronnet](/maintainers/camillebaronnet)

---

Top Contributors

[![camillebaronnet](https://avatars.githubusercontent.com/u/1884268?v=4)](https://github.com/camillebaronnet "camillebaronnet (3 commits)")

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/stann-google-docs-handlebars/health.svg)

```
[![Health](https://phpackages.com/badges/stann-google-docs-handlebars/health.svg)](https://phpackages.com/packages/stann-google-docs-handlebars)
```

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
