webhubworks/laravel-secure-db-dump

Allows you to dump your db and anonymize its content.

Maintainers

Package info

github.com/webhubworks/laravel-secure-db-dump

Homepage

pkg:composer/webhubworks/laravel-secure-db-dump

Statistics

Installs: 77

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

1.0.11 2026-05-19 09:20 UTC

This package is auto-updated.

Last update: 2026-05-19 09:21:05 UTC


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:

  1. Setup – resolves the source DB connection (db_connection config, falls back to default), the storage disk, and builds the dump file paths ({database}_{Ymd_His}.sql.gz).
  2. Dump original database – exports the source DB via spatie/db-dumper (gzip-compressed) to original_dump_*.sql.gz on the configured disk.
  3. 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.
  4. Import dump – pipes the original dump through gunzip | mysql into temp_secure_db_dump, then truncates any tables listed in ignore_tables.
  5. Anonymize – iterates the configured anonymize_fields table-by-table, applying each AnonymizerConfig (faker or static, optional where filters) via UPDATE statements on the temp DB. After every row's field rules, any configured row_hooks callables for that table are invoked (see Row hooks).
  6. Dump secure database – exports temp_secure_db_dump (gzip-compressed) to secure_dump_*.sql.gz. Schema is omitted when only_content is enabled.
  7. Clean up – drops temp_secure_db_dump (skipped on local env; falls back to dropping individual tables if DROP DATABASE fails).
  8. 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) and secure_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]);
        },
    ],
],