webhubworks / laravel-secure-db-dump
Allows you to dump your db and anonymize its content.
Package info
github.com/webhubworks/laravel-secure-db-dump
pkg:composer/webhubworks/laravel-secure-db-dump
Requires
- php: ^8.2|^8.3
- fakerphp/faker: ^1.24
- illuminate/contracts: ^10.0||^11.0||^12.0
- spatie/db-dumper: ^3.8
- spatie/laravel-package-tools: ^1.16
Requires (Dev)
- laravel/pint: ^1.14
- nunomaduro/collision: ^8.1.1||^7.10.0
- orchestra/testbench: ^10.0.0||^9.0.0||^8.22.0
- pestphp/pest: ^3.0
- pestphp/pest-plugin-arch: ^3.0
- pestphp/pest-plugin-laravel: ^3.0
- spatie/laravel-ray: ^1.35
README
Call artisan secure-db-dump:run to export data based on the following config.
Note
The original database is never modified. It is only read (dumped); all anonymization happens on the separate temp_secure_db_dump database.
The command performs these steps:
- Setup – resolves the source DB connection (
db_connectionconfig, falls back to default), the storagedisk, and builds the dump file paths ({database}_{Ymd_His}.sql.gz). - Dump original database – exports the source DB via
spatie/db-dumper(gzip-compressed) tooriginal_dump_*.sql.gzon the configured disk. - Setup temp database – runs
CREATE DATABASE IF NOT EXISTS temp_secure_db_dump, registers it as a runtime connection, and switches the default connection to it. - Import dump – pipes the original dump through
gunzip | mysqlintotemp_secure_db_dump, then truncates any tables listed inignore_tables. - Anonymize – iterates the configured
anonymize_fieldstable-by-table, applying eachAnonymizerConfig(fakerorstatic, optionalwherefilters) viaUPDATEstatements on the temp DB. After every row's field rules, any configuredrow_hookscallables for that table are invoked (see Row hooks). - Dump secure database – exports
temp_secure_db_dump(gzip-compressed) tosecure_dump_*.sql.gz. Schema is omitted whenonly_contentis enabled. - Clean up – drops
temp_secure_db_dump(skipped on local env; falls back to dropping individual tables ifDROP DATABASEfails). - Prompt – asks whether to delete the original dump file, then prints the path to the secure dump.
Use --only-anonymize to skip steps 2, 4, 6, 7 and 8 and just (re-)anonymize an already-imported temp_secure_db_dump.
Outcome:
- Source database: read-only (dumped, never modified).
temp_secure_db_dump: created, populated, mutated, then dropped (kept around on local env for inspection).- Files written to the configured disk (default
local):original_dump_{database}_{timestamp}.sql.gz(optionally deleted at the end) andsecure_dump_{database}_{timestamp}.sql.gz(the final anonymized dump).
DDEV
In case you get a permission error when trying to CREATE or DROP a database, add this post-start hook to your .ddev/config.yaml:
... hooks: post-start: - exec: mysql -uroot -proot -e "GRANT ALL ON *.* TO 'db'@'%'; FLUSH PRIVILEGES;" service: db
Config
Publish the config file via artisan vendor:publish --tag=secure-db-dump-config.
Anonymize fields
This package uses Faker to anonymize fields. You can find the available formatters/methods here: https://fakerphp.org/formatters/
Per field you want to anonymize you have to define the field and a type. Possible values are: faker or static.
Type: static
You will need to provide a value for the field.
Type: faker
You will need to provide a method and optionally args (an array) for the Faker method.
Examples
... 'anonymize_fields' => [ # Specify the table name 'users' => [ # This will run $faker->name() for the 'name' field AnonymizerConfig::make() ->field('name') ->type('faker') ->method('name'), # This will run $faker->email() for the 'email' field AnonymizerConfig::make() ->field('email') ->type('faker') ->method('email'), # This will set the 'password' field to a static value AnonymizerConfig::make() ->field('password') ->type('static') ->value('$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi'), # You can also add (multiple) where conditions. AnonymizerConfig::make() ->field('some_field') ->type('faker') ->method('sentence') ->where('some_field', fn($value) => $value === 'some_value'), ->where('some_other_field', fn($value) => ! str($value)->endsWith('@webhub.de')), ], 'cars' => [ # This will run $faker->regexify('LG [A-Z]{2} [0-9]{2,4}') for the 'licence_plate' field AnonymizerConfig::make() ->field('licence_plate') ->type('faker') ->method('regexify') ->args(['LG [A-Z]{2} [0-9]{2,4}']), ], ], ...
Row hooks
anonymize_fields covers per-field replacements only. When you need logic that goes beyond replacing a single column — for example inserting related records, reusing one generated value across several tables, or any cross-row coordination — register a row hook under the row_hooks config key.
Each hook is a callable(\Faker\Generator $faker, object $row): void registered against a table. The runner iterates the table's rows once and, for each row, first applies every matching anonymize_fields rule and then invokes every registered hook in order. The $row passed to a hook is the value read from the cursor (pre-anonymization); to read freshly anonymized values, re-query the row from the temp database, e.g. DB::table('projects')->where('id', $row->id)->first().
A table may appear in row_hooks without any anonymize_fields entry — the hook will still fire for every row in that table.
Like anonymize_fields, row_hooks accepts either an inline array or a fully qualified class name. The class must be invokable and return the same array shape.
Example
For each projects row, generate a unique 4-digit number, create a fresh cost_centers record titled "{number} - {project title}", and attach it to the project:
use Faker\Generator; use Illuminate\Support\Facades\DB; use Illuminate\Support\Str; 'row_hooks' => [ 'projects' => [ function (Generator $faker, object $row): void { $project = DB::table('projects')->where('id', $row->id)->first(); do { $number = (string) random_int(1000, 9999); } while (DB::table('cost_centers')->where('number', $number)->exists()); $costCenterId = (string) Str::uuid(); DB::table('cost_centers')->insert([ 'id' => $costCenterId, 'cost_center_group_id' => DB::table('cost_center_groups')->value('id'), 'number' => $number, 'title' => "{$number} - {$project->title}", 'created_at' => now(), 'updated_at' => now(), ]); DB::table('projects') ->where('id', $row->id) ->update(['cost_center_id' => $costCenterId]); }, ], ],