Content sources
Each document declares a source in config/legal-consent.php:
markdown(default) — git-diffable, PR-reviewable.mdfiles underresources/legal.drafts— the admin-maintained draft store. Texts are authored and reviewed per locale in your own screens, and only a reviewed draft set can be released. Opt a document in with'source' => 'drafts'. See Managing legal texts.
Any other value is your own class implementing Pushery\LegalConsent\Content\LegalDocumentSource,
named as the source's driver and resolved from the container. The registry it is named in is
sources in config/legal-consent.php, and a document points at an entry by its key:
// config/legal-consent.php
'sources' => [
'markdown' => ['driver' => MarkdownFilesDriver::class, 'path' => null],
'drafts' => ['driver' => DraftDocumentSource::class],
// Yours. The key is what a document's `source` names.
'cms' => ['driver' => \App\Legal\CmsDocumentSource::class],
],
'documents' => [
'terms' => ['source' => 'cms', /* … */],
],
Anything else in the entry is handed to the driver, so a source that needs its own settings takes
them there — markdown does exactly that with path.
A custom source is also the operator-identity seam
A custom source is the seam for interpolating operator identity (Impressum, Art. 13 contact,
VAT id, jurisdiction) from a single config source at render time: render your own Blade or HTML
with the values filled in and return it as an Html-format body — the pipeline hashes exactly
what you rendered, so the acceptance proof still covers precisely what the subject saw. To fill
{{ name }} placeholders with every value checked, use Placeholders, below.
Two rules keep it safe:
- Pin the
$localeyou are handed. A source that calls__()on the ambient locale hashes the same text differently per viewer and makeslegal-consent:check-driftflap. - Give every document a real
MAJOR.MINOR.PATCHversion. A null version defaults to0.0.0, which can never gate (the gate compares major versions, andheld >= 0is always true), so always set one on a gating document. The exception is a source that counts its own versions, below.
Placeholders, filled and checked
Pushery\LegalConsent\Content\Placeholders fills {{ name }} placeholders in a text and refuses
what it cannot fill. Give it every name a text may use, with its value, and fill the text in your
source before you return it:
use Pushery\LegalConsent\Content\ContentFormat;
use Pushery\LegalConsent\Content\Placeholders;
$body = (new Placeholders([
'operator.name' => config('company.name'),
'operator.vat_id' => config('company.vat_id'),
]))->fill($template, ContentFormat::Markdown);
A name the text uses that you did not give, and a name whose value is null or blank, throw
UnresolvedPlaceholders. Its message names every one of them, and it lists them in unknown and
missing. A value is set on one line and escaped for the format, with backslashes in Markdown and
entities in HTML, so it reads as written and never becomes markup.
Rendering a text of your own
RenderPipeline::render() renders, sanitizes and hashes a text exactly as a published document is
rendered and hashed, and publishes nothing: it needs no document key, no version and no acceptance
sentence. It is for a text you publish on your own terms, the terms of each shop on a storefront
for example, that you still want rendered and hashed the way this package proves a document:
use Pushery\LegalConsent\Content\ContentFormat;
use Pushery\LegalConsent\Content\Placeholders;
use Pushery\LegalConsent\Content\RenderPipeline;
$rendered = app(RenderPipeline::class)->render(
$shop->terms_markdown,
ContentFormat::Markdown,
new Placeholders(['shop.name' => $shop->name, 'shop.domain' => $shop->domain]),
);
$rendered->html; // the sanitized HTML
$rendered->contentHash; // the hash a published document of the same text carries
$rendered->sourceHash; // the hash over the text, its placeholders filled
The placeholders are filled first, so both hashes cover the text as it is shown. A text over the
pipeline's byte limit throws LegalDocumentTooLarge, before its placeholders are filled and after.
A source that counts its own versions
Some texts come from a system that numbers their revisions itself, often with a whole number, and
keeps the archive of which revision a customer read. Return that number as sourceVersion on the
RawDocument:
return new RawDocument(
type: $type,
locale: $locale,
title: $text->title,
body: $text->html,
format: ContentFormat::Html,
uiWording: $text->wording,
sourceVersion: (string) $text->revision, // what the other system calls this text
);
It is recorded word for word, beside the SemVer version, on the published version
(source_version, PublishedDocument::$sourceVersion, PublishedVersion::$sourceVersion) and on
every consent given to it (document_source_version, also in each entry of history()). It
decides nothing: the gate reads the SemVer version alone.
- Without a SemVer version, each new source version is the next minor of the active version,
and the first one is
1.0.0. A minor raises nobody's consent, so publish these with a mode that does not gate, such asSilentEditorial. A change that has to ask everybody again is a major you state asversion, and it goes through the same guards as any major. - The same source version publishes as the version it was. Publishing an unchanged text again changes nothing, and a changed text under a source version that was published already is refused: a source version names one text.
- One to 64 printable ASCII characters, with no space at either end. Anything else is refused
with
InvalidSourceVersionrather than trimmed, because the source version is folded into the record hash of every consent given to the text.