Skip to main content

Command reference

CommandWhat it does
legal-consent:publish {key?} {locale?} --editorial|--info|--deemed|--activeFreeze the current source into a new active version under a notice mode (--material is the legacy alias of --active). Flags below.
legal-consent:check-drift {key?} {locale?}Non-zero exit when a source has drifted from its published version (CI or cron). Narrow it to one document or locale with the optional arguments.
legal-consent:dispatch-notices [--dry-run] [--force]Notify subjects of a due legal change, routing by notice mode (hourly, idempotent, auto-scheduled). --dry-run reports the audience of every due version and sends nothing, writes no proof, stamps no watermark. --force sends anyway where the audience exceeds notifications.max_recipients_per_run.
legal-consent:changes {key} {locale} [--clear]Print or discard the pending change description for a document and locale. Authoring goes through the ChangeItems facade.
legal-consent:renotify {key} {locale} {version?} [--tenant=]Clear a version's notice watermark so the next sweep considers it again. For a version whose sweep reached nobody — see below.
legal-consent:close-objection-windowsBind silence to deemed acceptance once a deemed-consent objection window closes (hourly, idempotent, auto-scheduled).
legal-consent:pruneDelete consent and notice records past the retention period (default 3 years); the current standing of an active subject is always kept. Not scheduled by default — see Retention.
legal-consent:cache-flush {key?} {locale?}Flush the cached, rendered documents and the enforceable-version set the gate reads per request. Needed after an out-of-band is_active write.
legal-consent:verify-ledgerVerify the tamper-evidence hash chain (non-zero exit on a break); only when tamper_evidence is on.
legal-consent:verify-documentsVerify every published row against its stored hash, and flag notice-mode divergence across a version's locales or a wording-locale mismatch (non-zero exit on a hard failure). Read-only — a frozen row is never repaired.
legal-consent:doctorCompare your published config file with the package's, report what drifted, name every registered document with no published version, and flag a config value that config:cache cannot serialize. Read-only. See below.

The sweep stamps notified_at once a version has been swept, and never revisits it. That is what keeps a clean re-run from mailing everyone twice — and it is also why a version whose sweep reached nobody can never repair itself.

That happened: until the audience became mode-dependent, an info-only or deemed-consent change published as a minor bump selected no subjects at all, reported success, and stamped the watermark anyway. If you published such a change before upgrading, its audience was never told.

php artisan legal-consent:renotify terms de 1.1.0
php artisan legal-consent:dispatch-notices --dry-run # look at the number first
php artisan legal-consent:dispatch-notices

It clears the watermark and nothing else — the content, the version and every existing proof row are untouched, and a version that was never swept is reported as such rather than silently "repaired". Omit the version to take the currently active one.

Run this after every upgrade. It answers a question no other command asks: does your published config/legal-consent.php still deliver what the package expects?

Publishing the config freezes a copy, and Laravel merges it flatly over the package default. That produces an asymmetry worth knowing:

  • A whole new top-level block reaches you — the merge supplies it.
  • A key added inside a block your file already declares never arrives. Your block wins as a whole, so at runtime the key is not merely undocumented: it is absent, and the package's default for it is replaced by a file that predates the key.

The reverse ages too. A key the package has since removed stays in your file and still reads like valid configuration — including entries naming classes that no longer exist, which fail when something resolves them and point at your config rather than at the upgrade that removed them.

php artisan legal-consent:doctor
These keys exist in the package but NEVER reach your runtime config:

- cache.enforceable_ttl (package default: 60)
- routes.return_to_intended (package default: false)

These keys exist only in your published file — the package no longer defines them:

? sources.database.driver (your value: …\Content\Drivers\DatabaseDriver)

Your file carries fewer entries than the package default in these lists:

? locales (not in your list: de)

These documents are registered but have no published version:

? terms (en)

These config values are closures, so `php artisan config:cache` will fail:

? legal-consent.gate.subject_filter

It changes nothing. Copy the missing keys into the matching block yourself, and delete the stale ones once you have checked them — republishing with --force would discard your own values, which is the reason this reports instead of rewriting.

The closure report exists because the deploy is the first place it shows

Two keys accept a closure — gate.subject_filter and document_url — and a closure in either one makes php artisan config:cache abort the whole cache with a LogicException.

Where that bites is what makes it worth a report:

WhereResult
local, nothing cachedruns
your test suiteruns
deploy, config:cacheaborts

Nothing before the deploy reproduces it. Move the body into an invokable class and configure its class-string instead — both keys resolve one from the container, so the behavior is unchanged:

'subject_filter' => App\Legal\SubjectFilter::class,

doctor reports this and still exits 0: a closure is entirely valid until something caches, and plenty of installations never do.

A list is one value, not a set of keys

A listlocales is the one that matters — is compared as a set, and carrying fewer entries than the package default is reported without failing. Its length is your decision: locales is which legal documents exist in your application, so a single-language app carries one. Adopting the default to satisfy a check would mean publishing a second binding legal text, which is a worse outcome than the finding.

Exit codes

FindingExit
A key never reaches your runtime — your file says one thing, the runtime is another1
A configuration that contradicts itself (deemed consent with durable_medium.proof off)1
A stale key the package no longer defines0
A list you deliberately keep shorter0
A registered document with no published version0

The zero rows are real findings worth reading, and none of them means your configuration is wrong. That split is what lets this gate a deployment: a step that goes red on a state you chose — or on the state every fresh database is in — gets switched off, and the findings that do matter go with it. There is deliberately no flag to suppress the failing class, for the same reason in reverse. Your documents registry is never reported in either direction: curating it is your call.

Exactly one mode flag per publish; the rest are optional metadata.

FlagWhat it means
--editorialSilent activation — a typo or formatting fix that changes no obligation. No notice.
--infoActively announced, no action required, takes effect regardless.
--deemedSilence counts as acceptance. Contract and terms only; requires --objection-at.
--active (--material)The subject must actively accept before it applies.
--regime=The legal regime the change falls under: bgb_agb, psd2_675g, dcd_327r, gdpr, p2b, or eecc. Validated against that set — an unrecognized value is refused. Four of the six carry an advance period of their own: psd2_675g reads psd2_min_days (statutory floor 60 — § 675g's hard two months), p2b reads p2b_standstill_days (floor 15), eecc reads eecc_min_days (floor 30), and gdpr reads privacy_advance_days (no floor — WP260's "well in advance" is guidance, not a number). bgb_agb and dcd_327r declare none, because their period is the mode benchmark. The regime minimum and the mode minimum are combined with max(), never chosen between: a deemed-consent change under P2B owes both. An info-only change has no mode benchmark, so its regime is its only source — under P2B, EECC or GDPR an --info publish with too short an --enforce-at is refused, not waved through. See notice_periods.
--announce-at=ISO date the subjects are notified. Defaults to now.
--enforce-at=ISO date enforcement begins. Omit for an immediate publish.
--objection-at=ISO date the objection window closes (--deemed). Must leave the full statutory period after the announcement.
--offers-terminationThe notice offers a free right to terminate before the effective date (§ 675g / § 327r).
--keeps-unmodifiedThe subject may keep the unmodified version (the DCD / § 327r escape hatch).
--change-class=A free-form classification tag from your legal review (for example agb_minor_peripheral), stored with the version for the audit trail.
--allPublish every registered document in every configured locale.
--only-missingModifies --all: publish only combinations with no active version and leave every existing one untouched, including one whose source has drifted. The flag for a scripted caller — see below.
--dry-runResolve every source and report what a run would do. Writes nothing. Works on a single document and on --all.

--all in a deploy line: reach for --only-missing

--all is idempotent for unchanged sources, and that is what makes it safe to re-run. Once a source has drifted it is no longer a no-op — it is a publication, and it carries whatever mode stands on the line that invoked it. In a deploy script that mode is --editorial, because the first run legitimately is editorial. So a changed legal text would be filed as the one classification that notifies nobody, chosen by a script rather than by a person.

php artisan legal-consent:publish --all --only-missing --editorial # scripts
php artisan legal-consent:publish --all --editorial # a person who looked at the diff

--only-missing never reads the source of a combination that already has an active version, so it cannot classify a change at all. That is the whole of what the flag decides.

How a textless source is treated is a separate question, and it does not move with the flag:

  • A document waiting on an author is named and skipped, and the run stays green — under --only-missing and under the bare --all alike. An unwritten legal text is the normal state of an installation whose editors have not written it yet: there is no file anywhere to freeze, because the text is authored in the admin. It is named, never silent — a count alone would let a document sit unpublished for months behind a green deploy.

  • A document whose source is provisioned — a markdown file that should be in the repository — fails in both. Nobody is going to write that one; a deployment is missing a file, and skipping it produces the empty legal page this command exists to prevent.

    The two raise the same error, so the source is what decides. A source declares Pushery\LegalConsent\Content\AwaitsAuthoring when its empty state means a person has not written yet; the bundled draft source does. Your own source is treated as provisioned unless it declares that interface — the direction that fails loudly rather than skipping quietly. If yours is editorial (a CMS, a review queue), implement the marker; it declares no methods.

  • An unexpected source error is still a failure in both modes. "No text yet" is a state; a broken driver is a defect, and folding the second into the first would turn it into a green skip line.

Drift itself is reported by legal-consent:check-drift, which is the command for that question and stays sharp because the publish does not also try to answer it.

A major version bump forces its mode — the publisher refuses --info and --deemed on a major bump of a contract, and refuses anything but --info on a major privacy bump. See The four notice modes.