php-shark-tank/anonymizer
Anonymizer for Laravel Eloquent models: define per-model rules to replace sensitive personal data with safe values, helping with GDPR compliance and anonymized test or export data.
This is the initial public release (1.0.2), so treat it as a greenfield adoption — there is no migration path to worry about, but also no battle-tested conventions beyond what ships here.
Install and wire up
composer require <vendor>/<package> # 1.0.2
The service provider is auto-discovered. Publish the config early — this is where the defaults you'll fight with later live:
php artisan vendor:publish --tag=<package>-config
Check three keys first: the default handler, the exclusion strategy list, and the event-dispatching flag.
First use case
Start with the smallest possible object — a DTO or a model you already have a factory for — and let the default handler (Faker) populate it:
$populator = app(Populator::class);
$user = $populator->populate(UserData::class);
Then tighten it property by property. The four shipped handlers map cleanly onto the usual test/seed needs:
name, email, address)fn ($ctx) => $ctx->email for username, computed totals, correlated values)Where to look next, in order: the config file (defaults + exclusion list), the handler interface (before you write a custom one), and the lifecycle callback registration point (that's where persistence hooks go).
Per-property handler mapping. The core workflow is a definition that assigns a handler to each property and falls back to the configured default for everything else. Keep definitions declarative and boring; put branching logic in Callback handlers, not in if statements around the definition.
public function definition(): array
{
return [
'name' => Handler::faker()->name(),
'email' => Handler::faker()->safeEmail(),
'password' => Handler::hash(),
'nickname' => Handler::null(),
'slug' => Handler::callback(fn ($ctx) => Str::slug($ctx->name)),
];
}
Exclusion strategies are the recursion guard. With object exclusions and property exclusions both available, the standard pattern is:
Carbon, UploadedFile, enums, backed value objects.id, created_at, updated_at, deleted_at, and anything you want the database to own.Do this once in config rather than in every definition; it's the difference between a populator that works on nested graphs and one that stack-overflows the first time a model has a belongsTo back to itself.
Lifecycle callbacks for the boring glue. Use pre-populate callbacks for normalization (trim, lowercase emails) and post-populate callbacks for persistence or wrapping. The common Laravel shape is: populate → post-callback wraps in DB::transaction() → save. Don't put persistence in the definition; definitions should stay pure data.
Event dispatching is opt-in — decide once. Because dispatching is optional, listeners you register won't fire unless it's enabled. Two viable stances:
RefreshDatabase transaction.Whichever you pick, set it in config per environment (testing off, local/staging on) rather than toggling ad hoc in code.
Integration points. The natural fits are test data builders inside feature tests, seeders that need coherent relational data, and factories where you want generated values without writing a fake() call per attribute. If you already use Laravel factories, use this package to generate the attributes and let factories handle persistence — mixing both persistence layers in one flow is where duplicate inserts come from.
Handler precedence is the first thing to verify. Explicit per-property handlers must beat the default; if you see Faker output where you declared Null, your property key and the definition's key disagree (nested arrays vs. dot notation is the usual culprit).
Exclusion by property name is string-matching. It won't respect class hierarchies and it won't know that user_id and user are related. Keep a flat, audited list in config, and prefer object-type exclusions wherever you can — they survive renames.
Faker state is not per-call. unique() and locale are process-wide; a unique modifier used inside a Callback can exhaust its pool across a long seeding run and throw mid-run. Reset state between batches, or scope uniqueness with an explicit large pool.
Hash handler cost. Hashing every one of 50,000 seeded rows with a real bcrypt cost is minutes of CPU. For bulk seeding, either drop the hash cost in the config or swap in a null/fixed-value handler for the bulk path and hashing only for the handful of users that need to log in.
Don't dispatch events inside an open transaction you might roll back. Queued listeners see the write as committed when it isn't. If you enable dispatching in a seeding command, either dispatch after commit or disable it and dispatch deliberately at the end.
Callbacks run per property, per object. Anything doing a query or an HTTP call inside a Callback multiplies fast and produces textbook N+1. Pre-fetch lookups into the populator context once and have callbacks read from it.
Debugging a wrong value. The fastest diagnostic is a temporary log at the handler-resolution point printing property name → resolved handler class. Nine times out of ten the value came from the default handler, not the one you thought you registered.
Extension points worth knowing on day one: a custom handler implementing the handler interface (for Ulid, Uuid, money objects), and a custom exclusion strategy if your domain has a base class whose descendants should never be traversed. Both are registration-time config, not per-definition — register them once and every definition inherits the behavior.
How can I help you explore Laravel packages today?