LaravelPackages.net
Acme Inc.
Toggle sidebar
bernskiold/laravel-data-scrubber

A package for Laravel to clean or scrub data such as PII from models.

4
1
0.2.0
About bernskiold/laravel-data-scrubber

bernskiold/laravel-data-scrubber is a Laravel package for a package for laravel to clean or scrub data such as pii from models.. It currently has 1 GitHub stars and 4 downloads on Packagist (latest version 0.2.0). Install it with composer require bernskiold/laravel-data-scrubber. Discover more Laravel packages by bernskiold or browse all Laravel packages to compare alternatives.

Last updated

Laravel Data Scrubber

Latest Version on Packagist GitHub Tests Action Status Total Downloads

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 | [email protected] | | AnonymizeEmailWithIdStrategy | Replaces with email containing model ID | [email protected] | | HashStrategy | Hashes the value (SHA-256 by default, salted) | 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*** | | IpAnonymizeStrategy | Zeros out trailing IPv4/IPv6 octets (GDPR) | 192.168.0.0 | | 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) |

Hashing note: An unsalted hash of low-cardinality data (emails, phone numbers) can be reversed with brute-force or rainbow tables. Set a secret in data-scrubber.strategies.hash.salt (e.g. env('DATA_SCRUBBER_HASH_SALT')) to switch HashStrategy to HMAC and make the result non-reversible. Note that changing the algorithm or salt changes the output, so apply it before scrubbing.

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' => '[email protected]'],
        'anonymize_email_with_id' => ['domain' => 'anonymized.local', 'prefix' => 'deleted-'],
        'hash' => ['algorithm' => 'sha256', 'salt' => env('DATA_SCRUBBER_HASH_SALT')],
        'delete_file' => ['disk' => null],
        'mask' => ['visible_start' => 2, 'visible_end' => 2, 'mask_char' => '*'],
        'truncate' => ['keep_chars' => 3, 'suffix' => '***'],
        'ip_anonymize' => ['mask_octets' => 2],
    ],

    // Queue configuration for async processing
    'queue' => [
        'async' => env('DATA_SCRUBBER_ASYNC', true),
        'connection' => null,
        'queue' => 'data-scrubber',
        'chunk_size' => 500,
        'tries' => 3,
        'backoff' => 60,
    ],
];

Publishing a partial config: Laravel merges a published config with the package defaults at the top level only. If you publish config/data-scrubber.php and keep only some keys under a nested array (such as strategies or queue), the unspecified sub-keys fall back to their hard-coded strategy defaults rather than being merged. Keep the full nested arrays in your published file to avoid surprises.

How records are processed

When scrubbing asynchronously, each queued job scrubs a single chunk (chunk_size) of records and, if more remain, dispatches a follow-up job for the next chunk. This keeps individual jobs small on large tables and makes retries safe — a failed job only re-processes its own chunk.

The command and jobs only ever operate on records returned by your scrubbableQuery(), further narrowed to those that have not already been scrubbed (when timestamp logging is enabled). This guards non-idempotent strategies such as HashStrategy against being applied twice on retries or repeated runs.

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 LogsActivity trait
  • 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.

Star History Chart