ginkelsoft / laravel-data-subject-access
A Laravel package that implements GDPR art. 15 / 20 subject-access (inzageverzoek) exports across configured Eloquent models, in JSON or Markdown, with a tamper-evident access log.
Package info
github.com/ginkelsoft-development/laravel-data-subject-access
pkg:composer/ginkelsoft/laravel-data-subject-access
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:17:59 UTC
README
Overview
Implements GDPR art. 15 (right of access) — and, since the output is structured, doubles as art. 20 (data portability) — for a Laravel application. Given a subject identifier, the package collects every record that any registered Eloquent model holds about that subject and renders it in JSON or Markdown. The action is strictly read-only: nothing is modified, deleted, or anonymized.
Every access is itself a verwerking, so each export writes one row per
matched model to a dedicated, append-only subject_access_log hash chain
built on the shared HashChain from
ginkelsoft/laravel-compliance-core. The log stores only the irreversible
subject hash, the source model class, a record count and the format — never
the exported data, never the subject identifier.
This is the subject access member of the GinkelSoft compliance 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 — this package |
laravel-data-consent |
art. 6(1)(a) + 7 | Consent registry |
laravel-data-breach-registry |
art. 33 + 34 | Breach registry |
laravel-compliance-hub |
art. 5(2) | Umbrella |
How it works
1. Declare which fields are exportable
Mirror of the other family patterns: an attribute for the subject column, a
property for the explicit list of fields. Models implement
Ginkelsoft\DataSubjectAccess\Contracts\Exportable and use the matching
trait.
The field list is opt-in per field: auto-including every column is unsafe (internal flags, technical foreign keys, hashed values) so this package refuses to do it.
use Ginkelsoft\DataSubjectAccess\Attributes\Exportable; use Ginkelsoft\DataSubjectAccess\Concerns\Exportable as ExportableTrait; use Ginkelsoft\DataSubjectAccess\Contracts\Exportable as ExportableContract; #[Exportable(column: 'id')] class User extends Model implements ExportableContract { use ExportableTrait; protected array $exportable = [ 'fields' => [ 'id' => 'Subject identifier', 'email' => 'E-mailadres', ], ]; } class Profile extends Model implements ExportableContract { use ExportableTrait; protected array $exportable = [ 'column' => 'user_id', 'fields' => [ 'first_name' => 'Voornaam', 'last_name' => 'Achternaam', 'email' => ['label' => 'E-mailadres'], 'logged_in_at' => [ 'label' => 'Aangemeld op', 'transform' => fn ($v) => $v?->format('Y-m-d H:i:s'), ], // internal_note is intentionally not listed: it stays out of the export. ], ]; }
2. Register the models
// config/subject-access.php return [ 'models' => [ \App\Models\User::class, \App\Models\Profile::class, ], 'include_soft_deleted' => true, ];
3. Run the export
php artisan retention:export 01HXYZ php artisan retention:export 01HXYZ --format=markdown php artisan retention:export 01HXYZ --format=json --output=storage/exports/01HXYZ.json
The command name keeps the retention: prefix for BC with the v1.x
monolithic package. Without --output the export is written to STDOUT, so
it can be piped or captured. With --output it lands in the given file
(missing intermediate directories are created). Two formats ship by
default; the Ginkelsoft\DataSubjectAccess\Contracts\Exporter interface
lets you add more (HTML, CSV, PDF) without touching the rest of the
package.
4. Verify the access chain
use Ginkelsoft\ComplianceCore\Config\LogSecret; use Ginkelsoft\ComplianceCore\Support\HashChain; use Illuminate\Support\Facades\DB; $entries = DB::table('subject_access_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
| Column | Meaning |
|---|---|
subject_hash |
SHA-256 of subject id + secret — irreversible |
model_type |
FQCN of the matched model |
record_count |
Number of records found for this model |
format |
The exporter format used (json, markdown, ...) |
performed_at |
UTC timestamp |
previous_hash / hash |
Hash chain bookkeeping |
No subject identifier, no field values.
Compliance notes
- GDPR art. 15 — Right of access. Produces a structured snapshot of every Exportable record per subject.
- GDPR art. 20 — Data portability. The JSON format is machine-readable and reusable by the subject.
- GDPR art. 5(2) — Accountability. The
subject_access_loghash chain proves the access happened, when, by which format, and over which models.
This package is not legal advice. Identity verification (is this requester actually the subject?) is your application's responsibility.
Installation
composer require ginkelsoft/laravel-data-subject-access php artisan vendor:publish --tag=compliance-config php artisan vendor:publish --tag=subject-access-config php artisan vendor:publish --tag=subject-access-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
- Identity verification is your problem. This package does not check
whether the requester is actually the subject. Running
retention:exportagainst an unverified identifier is a data breach waiting to happen — do a verified email round-trip, an authenticated session, or a manual KYC step before invoking the command. - The export is a snapshot. Records created or modified after the export are obviously not in it. If the subject asks for a fresh export tomorrow, run it again — accountability comes from the per-call log row.
- Trait conflict with right-to-be-forgotten. A model that carries both
Exportable(this package) andForgettable(laravel-data-right-to-be-forgotten) must resolve theforSubjectQueryconflict explicitly:use Ginkelsoft\DataRightToBeForgotten\Concerns\Forgettable; use Ginkelsoft\DataSubjectAccess\Concerns\Exportable; class User extends Model implements ExportableContract, ForgettableContract { use Exportable, Forgettable { Forgettable::forSubjectQuery insteadof Exportable; } }
When both policies use the same subject column (the common case), eitherinsteadofpicks works. When the columns differ, overrideforSubjectQueryon the model directly instead. - PDF is intentionally not built-in. Adding a PDF generator would pull in a heavy dependency for what is essentially an Exporter contract that you can implement in a project-specific way (Dompdf, mPDF, Browsershot). JSON and Markdown cover the common cases.
- Migrated from v1.x? In the monolithic
laravel-data-retentionv1.x package, subject-access logged intoretention_logwith a sentinelsubject_accessfield. This package writes to its ownsubject_access_logtable instead, which means existingretention_logrows from before the upgrade stay verifiable in place (the retention package still owns that chain) and new access actions start a fresh chain in the new table. No data migration needed; see UPGRADE.md.
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-subject-access/issues/new
When opening an issue, please include:
- Versions — PHP, Laravel, and the package version
(
composer show ginkelsoft/laravel-data-subject-access). - 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