dragonniers/database-anonymizer
Laravel package for anonymizing database records, replacing sensitive personal data with fake values for safe staging, testing, and GDPR-friendly development. Configure column mappings and sanitize tables while preserving relationships.
This is a small, single-maintainer package (1 star, no dependents, last release Jan 2023), so treat it as a starting point rather than a turnkey tool — plan to read the source before trusting it with production-shaped data.
Install
composer require dragonniers/database-anonymizer --dev
Keep it in require-dev: anonymization is a local/staging concern, not a runtime dependency.
First steps
src/ and config/ directories. For a package this size you can read the whole thing in under 10 minutes — do that before running it.Dragonniers\DatabaseAnonymizer\...ServiceProvider) and whether it auto-registers. If not, add it to config/app.php or bootstrap/providers.php (Laravel 11+).php artisan vendor:publish --provider="Dragonniers\DatabaseAnonymizer\DatabaseAnonymizerServiceProvider"
db:anonymize, anonymize, or similar). Run php artisan list | grep -i anonym to confirm the exact signature rather than guessing.name, email, phoneNumber).First use case
The canonical workflow: restore a production dump into a local/staging database, then anonymize it before anyone touches it.
# Restore prod snapshot into staging
mysql staging_db < prod_snapshot.sql
# Anonymize in place
php artisan db:anonymize
# Or with a specific connection
php artisan db:anonymize --connection=staging
Start with one table (users) and one or two columns (email, name). Verify the result manually, then expand. Do not point it at production until you've reviewed exactly which queries it issues.
Config-driven column mapping
The package's core loop is almost certainly: read config → iterate tables → UPDATE each column with a Faker value. A typical config block looks like:
// config/anonymizer.php
return [
'users' => [
'name' => 'name',
'email' => 'safeEmail',
'phone' => 'phoneNumber',
],
'orders' => [
'billing_address' => 'address',
],
];
Map this against your actual schema: grep -r "string\|text" database/migrations/ and enumerate every PII column. The package can only anonymize what you tell it about.
Chunked updates for large tables
If the package doesn't chunk internally, wrap it or patch it. A naive UPDATE users SET email = ... on a 10M-row table will lock and time out. If you need to do it yourself, the safe pattern is cursor-based:
DB::table('users')->orderBy('id')->chunkById(1000, function ($rows) {
foreach ($rows as $row) {
DB::table('users')->where('id', $row->id)->update([
'email' => fake()->unique()->safeEmail(),
]);
}
});
Foreign keys and constraints
Anonymizing users.id will orphan every posts.user_id. Either:
SET FOREIGN_KEY_CHECKS=0 on MySQL, SET session_replication_role = replica on Postgres) and re-enable after.Add a guard in your pipeline: anonymize parents before children, or wrap the whole run in a transaction if the dataset is small enough.
Integration with a database refresh pipeline
The highest-value pattern is folding the anonymizer into a repeatable staging refresh:
#!/usr/bin/env bash
set -euo pipefail
mysql staging < latest_prod.sql
php artisan migrate --force
php artisan db:anonymize --connection=staging
php artisan cache:clear
Run this nightly or on demand. If the package's command is unreliable, this script is where you'd swap in spatie/laravel-db-dump or a hand-rolled php artisan anonymize command without touching anything else.
Seeding and factories
After anonymization, some rows may violate unique constraints or application invariants (e.g. users with email_verified_at set but nonsense emails). Pair the anonymizer with a small follow-up step to normalize state:
DB::table('users')->update(['email_verified_at' => null]);
Extension points
Since the package is tiny, the realistic "extension" is subclassing or forking. If it exposes a custom-anonymizer contract (an interface like Anonymizer with a handle() method), register your own in the config:
'email' => \App\Anonymizers\HashedEmail::class,
If no such contract exists, wrap the command in your own artisan command and call the package's service class directly.
Maturity risk is the headline. One star, zero dependents, last commit over two years ago. Audit before use:
users; DROP TABLE is a footgun.UPDATE per row or per column? Per-row on a large table is painfully slow.--env/--connection, or is the connection hardcoded?Unique columns cause collisions. fake()->email() from a small Faker pool will hit email unique constraints on a large table. Use unique() modifiers, or append a stable suffix derived from the primary key:
'email' => fn ($row) => "user{$row->id}@example.test",
Deterministic transforms like this are also far easier to debug and reproduce than random Faker output.
JSON and encrypted columns. If a column uses Laravel's encrypted cast, updating it via raw SQL corrupts it — the value must be encrypted with the app's key. Similarly, JSON columns need valid JSON in the replacement. Check whether the package uses the query builder (respects casts, sort of) or raw SQL (doesn't). For encrypted columns, run anonymization through Eloquent or skip them and rotate the key.
Config drift is silent. When someone adds a PII column next sprint, nothing fails — you just leak it. Add a test that asserts every string/text column in sensitive tables appears in the anonymizer config, or a CI check that greps migrations for email|phone|address|name and warns.
Verify, don't trust. After a run, spot-check:
DB::table('users')->select('email', 'name')->limit(5)->get();
Then run your app's test suite against the anonymized DB. Invariants (foreign keys, unique constraints, enum values, date ordering like created_at <= updated_at) are what actually break, and the package won't test them for you.
Back up before the first run. There's no dry-run flag in a package this small. mysqldump the target database first. If you need a dry run, copy the config and comment out all but one table.
Debugging. If the command exits silently or no rows change:
php artisan config:show anonymizer if publishable, or dd(config('anonymizer')) inside a tinker session).users vs user, no snake_case/camelCase mismatches.php artisan config:clear; a cached config is the most common cause of "my changes have no effect."DB::listen(fn ($q) => dump($q->sql, $q->bindings)) to see exactly what it executes.Where it fits vs. alternatives. If you need something maintained, spatie/laravel-db-dump (dump/restore without PII), fakerphp/faker + a hand-written command (30 lines, fully controlled), or a pg_dump/mysqldump filtered through mysqlpump-style column masking in your pipeline are all viable. This package is worth using if it saves you a config schema you'd otherwise rewrite — but the honest cost/benefit for a 1-star package is: read it, then decide whether to fork it into your own App\Console\Commands\AnonymizeDatabase.
Pin it.
"dragonniers/database-anonymizer": "dev-main#<commit-sha>"
Since there's no active maintenance, pin to a commit so a future force-push or abandoned branch doesn't silently change behavior under you.
How can I help you explore Laravel packages today?