ginkelsoft / laravel-data-consent
A Laravel package that implements GDPR art. 6(1)(a) and art. 7 consent recording as a tamper-evident append-only event log, with grant/withdraw actions and a status helper for the current state.
Package info
github.com/ginkelsoft-development/laravel-data-consent
pkg:composer/ginkelsoft/laravel-data-consent
Requires
- php: ^8.2
- ginkelsoft/laravel-compliance-core: dev-development
- illuminate/console: ^10.0 || ^11.0 || ^12.0 || ^13.0
- illuminate/database: ^10.0 || ^11.0 || ^12.0 || ^13.0
- illuminate/support: ^10.0 || ^11.0 || ^12.0 || ^13.0
- nesbot/carbon: ^2.62 || ^3.0
Requires (Dev)
- laravel/pint: ^1.18
- orchestra/testbench: ^8.0 || ^9.0 || ^10.0 || ^11.0
- pestphp/pest: ^2.30 || ^3.0
- phpstan/phpstan: ^1.10 || ^2.0
- phpunit/phpunit: ^10.5 || ^11.0 || ^12.0
This package is auto-updated.
Last update: 2026-05-28 17:15:44 UTC
README
Overview
Implements GDPR art. 6(1)(a) (consent as lawful basis) and art. 7
(conditions for consent — including the demonstrability requirement) for
a Laravel application. Records every grant and every withdrawal as an
append-only, hash-chained event in consent_log. The current state for a
given (subject, purpose, version) is derived from the latest event.
This is the consent member of the GinkelSoft compliance family. The
chain is built on the shared HashChain from
ginkelsoft/laravel-compliance-core and the signing secret is shared
with every other audit log in the family.
The family
| Package | GDPR Article(s) | Role |
|---|---|---|
laravel-compliance-core |
art. 5(2) | Shared primitives |
laravel-data-retention |
art. 5(1)(e) | Storage limitation |
laravel-data-right-to-be-forgotten |
art. 17 | Subject-driven erasure |
laravel-data-subject-access |
art. 15 + 20 | Subject access |
laravel-data-consent |
art. 6(1)(a) + 7 | Consent registry — this package |
laravel-data-breach-registry |
art. 33 + 34 | Breach registry |
laravel-compliance-hub |
art. 5(2) | Umbrella |
How it works
Record a grant or withdrawal
use Ginkelsoft\DataConsent\Actions\RecordConsent; $consent = new RecordConsent; $consent->grant( subjectId: '01HXYZ', purpose: 'newsletter', version: '2026-05', source: 'web', metadata: ['ip' => '203.0.113.5', 'form' => 'signup-v3'], ); $consent->withdraw( subjectId: '01HXYZ', purpose: 'newsletter', version: '2026-05', source: 'email', );
version lets you tie consent to a specific consent text or processing
context. When you change your terms, prior consent does not automatically
cover the new version — record a fresh grant against the new version
string.
Query consent
use Ginkelsoft\DataConsent\Support\ConsentStatus; $status = new ConsentStatus; $status->isGranted('01HXYZ', 'newsletter'); $status->isGranted('01HXYZ', 'newsletter', version: '2026-05'); $status->latest('01HXYZ', 'newsletter'); $status->activeFor('01HXYZ'); $status->history('01HXYZ', purpose: 'newsletter');
activeFor returns only purposes whose latest event is granted —
perfect for an account dashboard that lists "what you currently consent
to".
CLI
For ops, backfills, and tests:
php artisan retention:consent:grant 01HXYZ newsletter --consent-version=2026-05 --source=web php artisan retention:consent:withdraw 01HXYZ newsletter --consent-version=2026-05 --source=email php artisan retention:consent:status 01HXYZ
The command names keep the retention:consent: prefix for BC with the
v1.x monolithic package. --consent-version rather than --version
because Symfony already uses --version as a reserved option.
Verify the chain
use Ginkelsoft\ComplianceCore\Config\LogSecret; use Ginkelsoft\ComplianceCore\Support\HashChain; use Illuminate\Support\Facades\DB; $entries = DB::table('consent_log')->orderBy('id')->get() ->map(fn ($row) => (array) $row)->all(); $intact = HashChain::verify($entries, LogSecret::value());
Or run php artisan compliance:verify from the
hub to
verify every chain in the family in one shot.
What the log stores
consent_log is the only audit log in the family that stores the
subject identifier directly. Consent inherently requires identification —
you cannot prove "this person consented" without knowing who they are.
Document that in your DPIA and apply your own retention policy to this
table.
Per row: subject identifier, purpose, version, action (granted /
withdrawn), source, optional metadata, occurred-at timestamp, plus the
chain bookkeeping.
Compliance notes
- GDPR art. 6(1)(a) — Consent as a lawful basis for processing.
- GDPR art. 7 — Conditions for consent: demonstrability ("the controller shall be able to demonstrate that the data subject has consented"). The hash-chained log IS the demonstration.
- GDPR art. 5(2) — Accountability. Idem.
This package is not legal advice. Whether consent is freely given, specific, informed and unambiguous is a question for your DPO.
Installation
composer require ginkelsoft/laravel-data-consent php artisan vendor:publish --tag=compliance-config php artisan vendor:publish --tag=consent-config php artisan vendor:publish --tag=consent-migrations php artisan migrate
Then add a secret to .env (shared with the rest of the family):
COMPLIANCE_LOG_SECRET="$(openssl rand -base64 32)"
Gotchas
- The log stores subject identifiers directly. That is necessary for art. 7 accountability — proof of consent has to be linkable to a real person. Mention this table in your DPIA.
- No automatic deduplication. Two
grantcalls in a row produce twograntedrows. Sometimes that is exactly what you want (re-affirming consent). When you want "only when not currently granted" semantics, checkConsentStatus::isGranted()first. - Withdrawal does not delete the prior grant. It records a new event that supersedes it. The grant stays in the log forever — that is what makes the chain a usable audit trail.
- Forget does not automatically cascade to consent_log. A subject
exercising their right to be forgotten (via
laravel-data-right-to-be-forgotten) will not have their consent records removed unless you explicitly registerConsentEntryas Forgettable. The legal argument for keeping consent + withdrawal records even after forget is real (you may need them to defend the lawfulness of past processing), so this is opt-in rather than default.
Testing
composer install vendor/bin/pest vendor/bin/phpstan analyse --memory-limit=1G vendor/bin/pint --test
Reporting bugs
Found a bug or unexpected behaviour? We want to hear about it.
Preferred — open a GitHub issue: https://github.com/ginkelsoft-development/laravel-data-consent/issues/new
When opening an issue, please include:
- Versions — PHP, Laravel, and the package version
(
composer show ginkelsoft/laravel-data-consent). - What you did — the artisan command, code snippet, or steps that triggered the bug.
- What you expected vs what actually happened — include full error output or a stack trace if there is one.
- A minimal reproduction if you can — a failing test or a small code sample beats a long description.
Security-sensitive findings (anything that could expose personal data, break a hash-chain, or bypass an audit log) — please do not open a public issue. E-mail info@ginkelsoft.com directly with "SECURITY" in the subject line and we will respond privately.
Not on GitHub? You can also e-mail info@ginkelsoft.com with the same information.
Contact
For commercial support, integration questions, or anything that doesn't fit a GitHub issue: info@ginkelsoft.com — https://ginkelsoft.com.
License
MIT License — see LICENSE. (c) 2026 Ginkelsoft