ashallendesign/redactable-models
Ash Allen's Laravel package makes it easy to redact sensitive Eloquent model data. Mark attributes as redactable and automatically mask or hide values when models are serialized, logged, or returned from APIs, helping prevent leaks of passwords, emails, and other private information.
Install the package and publish its config and migration:
composer require ashallendesign/redactable-models
php artisan vendor:publish --tag="redactable-models-config"
php artisan vendor:publish --tag="redactable-models-migrations"
php artisan migrate
The first place to look is the published config/redactable-models.php. It is the control panel: you map each redactable model to the attributes that should be redacted and the redactor class responsible for each attribute.
// config/redactable-models.php
'redactors' => [
App\Models\User::class => [
'name' => App\Redactors\NameRedactor::class,
'email' => App\Redactors\EmailRedactor::class,
'phone' => App\Redactors\PhoneRedactor::class,
],
],
Then add the trait to the model:
use AshAllenDesign\RedactableModels\Traits\Redactable;
class User extends Model
{
use Redactable;
}
The first practical use case is a GDPR/CCPA erasure request for a customer who has orders. Do not delete the user and orphan the order history. Instead:
$user->redact();
The user row remains, foreign keys remain intact, and the configured PII columns are replaced with redacted values. The package stores the original values in its redactions table so you can inspect what changed and, where supported, restore later. Check that table after your first tinker test:
$user->redact();
$user->refresh()->only(['name', 'email', 'phone']);
Also read the trait source and the built-in redactor classes before writing your own. They show the exact redactor contract, whether partial redaction is supported, and how restoration is handled.
Orchestrate redaction in a job or service, not a controller. Redaction often touches several models and should be retryable and auditable:
class RedactUserData implements ShouldQueue
{
use SerializesModels;
public function __construct(public int $userId) {}
public function handle(): void
{
$user = User::withTrashed()->findOrFail($this->userId);
DB::transaction(function () use ($user) {
$user->redact();
$user->addresses()->each->redact();
$user->profile?->redact();
});
}
}
Treat the config as your PII data map. Keep model-to-field mappings in the package config, and keep custom redactors small and deterministic. A redactor should return a safe replacement value, not perform side effects. For example, an email redactor can return redacted+{$user->id}@example.com so the value stays unique per record.
Make redaction idempotent. Jobs retry. A record may already be redacted when the job runs again. Guard against overwriting an already-redacted value with a second redaction, or make redactors safe to run repeatedly. Record the redaction event in your own audit table with actor, timestamp, request ID, and affected fields.
Redact the whole data subject, not just the users row. A customer’s PII usually lives in addresses, profiles, consents, payment_methods, support_tickets, and JSON payloads. Walk relationships deliberately, or build a RedactDataSubject service that owns the full graph. Avoid relying on a single $user->redact() call to be complete.
Use a per-record placeholder strategy where uniqueness matters. If email is unique, redacting every user to [email protected] will fail on the second record. Either generate unique placeholders, set the column to null where the unique index permits multiple nulls, or remove the unique constraint before redacting. The same applies to usernames, handles, and referral codes.
Keep restoration behind authorization. If the package can restore original values, wrap that in a policy and log it as a sensitive action. Restoration is effectively a data re-identification event.
Integrate with downstream systems explicitly. Redaction changes database rows; it does not automatically update Meilisearch/Algolia, Redis caches, CDN responses, analytics exports, or a warehouse. After redaction, clear caches, reindex searchable models, and push a redaction event to downstream consumers.
Test the redaction path with RefreshDatabase. Assert both the redacted column values and the presence of original values in the redactions table. Use Queue::fake() when testing the orchestration job, and include a test for re-running the job twice.
Backfill existing records with a chunked command. For existing data, write an Artisan command using chunkById() and dispatch one job per model or per batch. Do not load the whole users table into memory.
php artisan config:cache in deployment. In tests, if you mutate config at runtime, clear the config cache or use config()->set() before the model is resolved.update()/save(), firing saving, updating, and updated. Observers may re-populate redacted fields, fail validation, or overwrite placeholders. Audit observers on redactable models and consider quiet saves where appropriate.redactions table stores model class names. If you rename models or introduce a morphMap, old redaction rows may no longer resolve to the right model. Keep morph aliases stable.withTrashed(). Default queries will not find them. Include soft-deleted records in your data-subject redaction flow if they contain PII.APP_KEY and any package-specific key, and document the recovery procedure.redactions table contents.SerializesModels carefully and re-fetch inside handle(). A retried job may see an already-redacted model or a model that has since been deleted.email validation may fail. Bypass validation for redaction writes or use a valid-format placeholder.How can I help you explore Laravel packages today?