bernskiold / laravel-data-scrubber
A package for Laravel to clean or scrub data such as PII from models.
Installs: 0
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/bernskiold/laravel-data-scrubber
Requires
- php: ^8.2
- illuminate/contracts: ^11.0|^12.0
- laravel/prompts: ^0.1.18|^0.2.0|^0.3.0
Requires (Dev)
- laravel/pint: ^1.0
- nunomaduro/collision: ^8.0
- orchestra/testbench: ^9.0|^10.0
- pestphp/pest: ^2.0|^3.0
- pestphp/pest-plugin-laravel: ^2.0|^v3.0
Suggests
- spatie/laravel-activitylog: Required for automatic activity log scrubbing (^4.0)
This package is auto-updated.
Last update: 2026-01-17 18:08:28 UTC
README
A Laravel package for scrubbing PII (Personally Identifiable Information) and sensitive data from Eloquent models. Useful for GDPR compliance and data retention policies.
Installation
Install the package via composer:
composer require bernskiold/laravel-data-scrubber
Optionally publish the configuration file:
php artisan vendor:publish --provider="Bernskiold\LaravelDataScrubber\DataScrubberServiceProvider" --tag="config"
If you're using Laravel Horizon, add the data-scrubber queue to your config/horizon.php:
'environments' => [ 'production' => [ 'supervisor-1' => [ 'queues' => ['default', 'data-scrubber'], // ... ], ], ],
Usage
Implementing Scrubbable on a Model
To make a model scrubbable, implement the Scrubbable interface and use the ScrubsData trait:
<?php namespace App\Models; use Bernskiold\LaravelDataScrubber\Concerns\ScrubsData; use Bernskiold\LaravelDataScrubber\Contracts\Scrubbable; use Bernskiold\LaravelDataScrubber\Data\ScrubbableFields; use Bernskiold\LaravelDataScrubber\Strategies\AnonymizeEmailWithIdStrategy; use Bernskiold\LaravelDataScrubber\Strategies\AnonymizeFirstNameStrategy; use Bernskiold\LaravelDataScrubber\Strategies\AnonymizeLastNameStrategy; use Bernskiold\LaravelDataScrubber\Strategies\DeleteFileStrategy; use Bernskiold\LaravelDataScrubber\Strategies\HashStrategy; use Bernskiold\LaravelDataScrubber\Strategies\NullStrategy; use Bernskiold\LaravelDataScrubber\Strategies\RedactedStrategy; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; class User extends Model implements Scrubbable { use ScrubsData; use SoftDeletes; /** * Define which records should be scrubbed. * * In this example, we scrub soft-deleted records that are older than 30 days * and haven't already been scrubbed. */ public function scrubbableQuery(): Builder { return static::query() ->onlyTrashed() ->where('deleted_at', '<', now()->subDays(30)) ->whereNull('scrubbed_at'); } /** * Define which fields should be scrubbed and how. */ public function scrubbableFields(): ScrubbableFields { return ScrubbableFields::make([ 'email' => AnonymizeEmailWithIdStrategy::class, 'first_name' => AnonymizeFirstNameStrategy::class, 'last_name' => AnonymizeLastNameStrategy::class, 'phone' => NullStrategy::class, 'ssn' => RedactedStrategy::class, 'address' => HashStrategy::class, 'profile_photo' => DeleteFileStrategy::class, ]); } }
Available Scrubbing Strategies
| Strategy | Description | Example Result |
|---|---|---|
NullStrategy |
Sets the value to null |
null |
RedactedStrategy |
Replaces with [REDACTED] |
[REDACTED] |
AnonymizeFirstNameStrategy |
Replaces with "Deleted" | Deleted |
AnonymizeLastNameStrategy |
Replaces with "User" | User |
AnonymizeEmailStrategy |
Replaces with a generic email | anonymized@deleted.local |
AnonymizeEmailWithIdStrategy |
Replaces with email containing model ID | deleted-123@anonymized.local |
HashStrategy |
Hashes the value using SHA-256 | a8f5f167f44f4964e6c998dee827110c... |
DeleteFileStrategy |
Deletes file from storage and sets to null |
null |
MaskStrategy |
Masks middle characters, showing start and end | 12******90 |
TruncateStrategy |
Keeps first N characters and adds suffix | Jon*** |
JsonFieldStrategy |
Scrubs specific keys in JSON/array data | (varies per key) |
ConditionalStrategy |
Applies different strategies based on conditions | (depends on condition) |
CallbackStrategy |
Uses a custom closure handler | (depends on handler) |
Using Strategy Classes Directly
For more control, you can instantiate strategy classes directly with custom parameters:
use Bernskiold\LaravelDataScrubber\Data\ScrubbableFields; use Bernskiold\LaravelDataScrubber\Strategies\MaskStrategy; use Bernskiold\LaravelDataScrubber\Strategies\TruncateStrategy; use Bernskiold\LaravelDataScrubber\Strategies\JsonFieldStrategy; use Bernskiold\LaravelDataScrubber\Strategies\ConditionalStrategy; use Bernskiold\LaravelDataScrubber\Strategies\NullStrategy; use Bernskiold\LaravelDataScrubber\Strategies\RedactedStrategy; public function scrubbableFields(): ScrubbableFields { return ScrubbableFields::make([ // Mask phone numbers: "1234567890" → "12******90" 'phone' => new MaskStrategy(visibleStart: 2, visibleEnd: 2, maskChar: '*'), // Mask SSN showing only last 4: "123-45-6789" → "XXXXXXX6789" 'ssn' => new MaskStrategy(visibleStart: 0, visibleEnd: 4, maskChar: 'X'), // Truncate names: "Jonathan" → "Jon***" 'name' => new TruncateStrategy(keepChars: 3, suffix: '***'), // Truncate addresses: "123 Main Street" → "123 M..." 'address' => new TruncateStrategy(keepChars: 5, suffix: '...'), // Scrub specific keys in JSON data 'metadata' => new JsonFieldStrategy([ 'phone' => new MaskStrategy(2, 2), 'ssn' => NullStrategy::class, 'address' => RedactedStrategy::class, ]), // Apply different strategies based on conditions 'sensitive_data' => new ConditionalStrategy( condition: fn ($value, $model) => $model->requires_full_redaction, thenStrategy: new RedactedStrategy, elseStrategy: new MaskStrategy(2, 2), ), ]); }
Using Custom Callbacks
For more complex scrubbing logic, use the CallbackStrategy:
use Bernskiold\LaravelDataScrubber\Data\ScrubbableFields; use Bernskiold\LaravelDataScrubber\Strategies\AnonymizeEmailWithIdStrategy; use Bernskiold\LaravelDataScrubber\Strategies\CallbackStrategy; public function scrubbableFields(): ScrubbableFields { return ScrubbableFields::make([ 'email' => AnonymizeEmailWithIdStrategy::class, 'metadata' => new CallbackStrategy(function ($value, $model, $field) { // Custom logic here return json_encode(['scrubbed' => true, 'at' => now()->toIso8601String()]); }), ]); }
You can also use the fluent builder pattern:
public function scrubbableFields(): ScrubbableFields { return ScrubbableFields::make() ->add('email', AnonymizeEmailWithIdStrategy::class) ->add('first_name', AnonymizeFirstNameStrategy::class); }
Timestamp Logging
By default, the package logs when a record was scrubbed by updating a scrubbed_at column. This allows you to:
- Track which records have been scrubbed
- Prevent re-scrubbing already scrubbed records
- Query for scrubbed/unscrubbed records
Add the column to your migration using the provided Blueprint macro:
$table->scrubbedAt();
This creates a nullable timestamp column using the name from your config (data-scrubber.timestamp_column, defaults to scrubbed_at).
To customize timestamp logging behavior, override getScrubOptions() in your model:
use Bernskiold\LaravelDataScrubber\Data\ScrubOptions; public function getScrubOptions(): ScrubOptions { return ScrubOptions::defaults() ->dontLogScrubTimestamp(); // Disable timestamp logging }
To use a different column name:
public function getScrubOptions(): ScrubOptions { return ScrubOptions::defaults() ->useTimestampColumn('data_cleaned_at'); }
Query Scopes
When timestamp logging is enabled, the trait provides query scopes:
// Get records that haven't been scrubbed User::notScrubbed()->get(); // Get records that have been scrubbed User::scrubbed()->get();
Scrubbing Individual Records
You can scrub a single record programmatically:
$user = User::find(1); // Preview what will be scrubbed $preview = $user->previewScrub(); // Perform the scrub $user->scrub(); // Check if already scrubbed if ($user->hasBeenScrubbed()) { // Already scrubbed }
Artisan Commands
The package provides two artisan commands for managing data scrubbing.
Scrub Command
Run the scrubber to process all eligible records:
# Preview what would be scrubbed (dry run) php artisan data-scrubbing:scrub --dry-run # Scrub all eligible records (with confirmation prompt) php artisan data-scrubbing:scrub # Scrub without confirmation php artisan data-scrubbing:scrub --force # Scrub only a specific model php artisan data-scrubbing:scrub --model=User # Run synchronously instead of queuing jobs php artisan data-scrubbing:scrub --sync
By default, scrubbing is performed asynchronously using Laravel's queue system. You can configure this behavior globally in the config file or per-model via getScrubOptions().
Config Report Command
View the configuration of all Scrubbable models:
# Display configuration for all models php artisan data-scrubbing:config # Filter to a specific model php artisan data-scrubbing:config --model=User # Output as JSON php artisan data-scrubbing:config --json
This command displays a summary of all configured models, their fields, scrubbing strategies, and processing options.
Configuration
The configuration file allows you to customize the package behavior:
// config/data-scrubber.php return [ // Paths to scan for models implementing Scrubbable 'model_paths' => [ app_path('Models'), // Add additional paths as needed ], // Default column name for storing scrub timestamps 'timestamp_column' => 'scrubbed_at', // Default values for built-in scrubbing strategies 'strategies' => [ 'redacted' => ['replacement' => '[REDACTED]'], 'anonymize_first_name' => ['replacement' => 'Deleted'], 'anonymize_last_name' => ['replacement' => 'User'], 'anonymize_email' => ['replacement' => 'anonymized@deleted.local'], 'anonymize_email_with_id' => ['domain' => 'anonymized.local', 'prefix' => 'deleted-'], 'hash' => ['algorithm' => 'sha256'], 'delete_file' => ['disk' => null], 'mask' => ['visible_start' => 2, 'visible_end' => 2, 'mask_char' => '*'], 'truncate' => ['keep_chars' => 3, 'suffix' => '***'], ], // Queue configuration for async processing 'queue' => [ 'async' => env('DATA_SCRUBBER_ASYNC', true), 'connection' => null, 'queue' => 'data-scrubber', 'chunk_size' => 500, 'tries' => 3, 'backoff' => 60, ], ];
You can override options on a per-model basis by implementing getScrubOptions() in your model:
public function getScrubOptions(): ScrubOptions { return ScrubOptions::defaults() ->useTimestampColumn('data_cleaned_at') ->useChunkSize(100) ->scrubSynchronously(); // or scrubAsynchronously() }
Events
When a model is scrubbed, the package dispatches a Scrubbed event. You can register listeners for this event to perform additional actions such as notifying external systems, clearing caches, or triggering other workflows.
Activity Log Integration (Optional)
If you have Spatie Activity Log installed, the package provides two optional listeners for different purposes.
Logging Scrub Activity
To log when records are scrubbed, register the LogScrubbedActivity listener in your EventServiceProvider:
use Bernskiold\LaravelDataScrubber\Events\Scrubbed; use Bernskiold\LaravelDataScrubber\Listeners\LogScrubbedActivity; protected $listen = [ Scrubbed::class => [ LogScrubbedActivity::class, ], ];
This listener will:
- Only log if Spatie Activity Log is installed
- Only log if the model uses the
LogsActivitytrait - Log the field names and strategy names that were applied
- Never log the actual data (neither previous nor scrubbed values)
You can customize the event name and description in your config:
// config/data-scrubber.php 'activity_log' => [ 'event' => 'data_scrubbed', 'description' => 'Record data was scrubbed', ],
Scrubbing Activity Log Entries
Activity logs often store PII in their properties JSON column (e.g., old/new attribute values). To automatically scrub this data when a model is scrubbed, register the ScrubActivityLogListener:
use Bernskiold\LaravelDataScrubber\Events\Scrubbed; use Bernskiold\LaravelDataScrubber\Listeners\ScrubActivityLogListener; protected $listen = [ Scrubbed::class => [ ScrubActivityLogListener::class, ], ];
This listener will scrub the configured property keys (default: old and attributes) in all activity log entries for the scrubbed model, using the same strategies defined in scrubbableFields().
You can customize which property keys are scrubbed in your config:
// config/data-scrubber.php 'activity_log' => [ 'property_keys' => ['old', 'attributes'], ],
To customize the behavior per-model, implement the ScrubsActivityLog interface:
use Bernskiold\LaravelDataScrubber\Contracts\ScrubsActivityLog; use Bernskiold\LaravelDataScrubber\Data\ScrubbableFields; class User extends Model implements Scrubbable, ScrubsActivityLog { // Opt out of activity log scrubbing entirely public function shouldScrubActivityLog(): bool { return true; // or false to skip } // Use different strategies for activity log scrubbing public function activityLogScrubbableFields(): ?ScrubbableFields { return ScrubbableFields::make([ 'email' => RedactedStrategy::class, // Different from model's strategy ]); // Return null to use the same strategies as scrubbableFields() } }
Testing
composer test
Changelog
Please see CHANGELOG for more information on what has changed recently.
Contributing
Please see CONTRIBUTING for details.
Credits
License
The MIT License (MIT). Please see License File for more information.