Anonymization
When you need to erase personal data while preserving the audit trail, hard-deleting a user record is often impractical — foreign keys, audit history, and business logic depend on the record existing. AuditChain provides an anonymize() method that replaces personal data with a placeholder value, effectively erasing the personal data while preserving the record and its relationships. This directly supports GDPR Article 17 (the "right to be forgotten") and similar erasure requirements in other regulations.
Basic Usage
Call anonymize() on any auditable model to replace all annotated personal data fields with the configured replacement string:
$user->anonymize(); // Before: email => 'john@example.com', name => 'John Doe', age => 35 // After: email => '[ANONYMIZED]-a1b2c3d4', name => '[ANONYMIZED]-x9y8z7w6', age => null
Anonymization is type-aware: string fields are replaced with the configured replacement string followed by a random 8-character suffix (via Str::random(8)). Non-string fields are set to null. The random suffix avoids UNIQUE constraint violations when anonymizing multiple records — without it, two anonymized users would both have email => '[ANONYMIZED]', which would fail on a unique index.
How It Works
Under the hood, anonymize():
- Reads the configured replacement string from
audit-chain.anonymization.replacement - Gets the list of personal data fields via
getPersonalDataFields() - For each non-null personal data field:
- String fields: replaced with
-{random8}(usingStr::random(8)) - Non-string fields: set to
null
- String fields: replaced with
- Saves the model using
saveQuietly()
The use of saveQuietly() is deliberate — it prevents the save from triggering Eloquent events, which means no audit log entry is created for the anonymization itself. This is the correct behavior: you don't want the audit trail to record the transition from real data to anonymized data, as that would still expose the original personal data in the old_values column.
Configuration
Customize the replacement string in config/audit-chain.php:
'anonymization' => [ 'replacement' => '[ANONYMIZED]', // default ],
Or override it per-environment:
'anonymization' => [ 'replacement' => '[REDACTED]', ],
Null Fields Are Skipped
If a personal data field is already null, anonymize() leaves it as null rather than replacing it. This preserves the distinction between "never provided" and "was provided but has been anonymized."
Practical Example
A typical erasure request workflow:
use Illuminate\Http\JsonResponse; class ErasureController extends Controller { public function destroy(User $user): JsonResponse { $this->authorize('delete', $user); // Export data first (Article 20 - data portability) $export = $user->exportFullSubjectData(); // Anonymize personal data (Article 17 - right to erasure) $user->anonymize(); // Optionally record that an erasure was performed $user->audit('erasure_completed'); return response()->json([ 'message' => 'Personal data has been anonymized.', 'export' => $export, ]); } }
Note: The
audit('erasure_completed')call above is safe — it records a custom event on the already-anonymized model, so no personal data appears in the audit log entry.
Bulk Anonymization
Anonymize multiple records in a loop. The primary key suffix ensures no UNIQUE constraint conflicts:
$users = User::where('deleted_at', '<', now()->subYear())->get(); foreach ($users as $user) { $user->anonymize(); } // user 1: email => '[ANONYMIZED]-a1b2c3d4', name => '[ANONYMIZED]-e5f6g7h8' // user 2: email => '[ANONYMIZED]-x9y8z7w6', name => '[ANONYMIZED]-m3n4o5p6' // user 3: email => '[ANONYMIZED]-q1r2s3t4', name => '[ANONYMIZED]-u5v6w7x8'
Anonymizing Audit Logs
In addition to anonymizing the model itself, you can anonymize the personal data stored in audit log entries using anonymizeAuditLogs(). This method is part of the Auditable interface and handles light and chained logs differently:
- Light logs (where
hashisNULL): personal data fields are redacted directly in theold_valuesandnew_valuescolumns. - Chained logs (where
hashis notNULL): to preserve chain integrity, the values are not modified. Instead, the field names are stored in theanonymized_fieldscolumn and redacted at read time.
// Anonymize the model's personal data $user->anonymize(); // Also anonymize personal data in the user's audit log entries $user->anonymizeAuditLogs();
This ensures that historical audit entries no longer expose personal data, while chained entries remain verifiable.
Note:
exportFullSubjectData()automatically respects theanonymized_fieldscolumn and redacts those fields in the exported data.
Keeping the Audit Trail
Anonymization replaces the personal data on the model but — unless you call anonymizeAuditLogs() — does not alter existing audit log entries. Depending on your data retention policy and legal requirements, you may also need to prune old audit logs:
php artisan audit:prune --days=90
See Pruning for details on configuring retention periods.