signdeck / veil
Laravel package to anonymize database exports
Installs: 2
Dependents: 0
Suggesters: 0
Security: 0
Stars: 13
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/signdeck/veil
Requires
- php: ^8.3
- phpmyadmin/sql-parser: ^5.7
- spatie/laravel-db-snapshots: ^2.0
Requires (Dev)
- orchestra/testbench: ^9.0
- phpunit/phpunit: ^10.1
README
Veil is a Laravel package that helps you export database data to anonymize sensitive columns.
It's useful when you need to:
- Share production-like data with developers or contractors
- Create safe accurate data for local or staging environments
- Debug real-world issues without exposing personal data
- Comply with privacy and data protection requirements
Veil lets you define anonymization rules per table and column, ensuring sensitive values are replaced consistently during export. The exported SQL file contains only INSERT statements, making it easy to import into an existing database that already has the schema defined via Laravel migrations.
It uses spatie/laravel-db-snapshots and phpmyadmin/sql-parser under the hood and focuses on keeping the workflow simple and predictable.
"This package was created and maintained by the team behind SignDeck — a lightweight e-signature platform for collecting documents and signatures."
Supports Laravel version 11+
Installation
You can install the package via Composer:
composer require signdeck/veil
Publish the configuration file:
php artisan vendor:publish --tag=veil-config
This will create a config/veil.php file where you can configure your export settings.
Make sure you also have a filesystem disk configured for storing exports. By default, Veil uses the local disk.
Usage
1. Create a Veil Table Class
Generate a new Veil table class using the artisan command:
php artisan veil:make-table users
This creates app/Veil/VeilUsersTable.php:
<?php namespace App\Veil; use SignDeck\Veil\Veil; use SignDeck\Veil\Contracts\VeilTable; class VeilUsersTable implements VeilTable { public function table(): string { return 'users'; } public function columns(): array { return [ 'id' => Veil::unchanged(), // Keep original value 'email' => 'user@example.com', // Replace with this value ]; } }
2. Define Columns to Export
In the columns() method, specify which columns to include in the export:
public function columns(): array { return [ 'id' => Veil::unchanged(), // Keep original value 'name' => 'John Doe', // Replace all names with "John Doe" 'email' => 'redacted@example.com', // Replace all emails 'phone' => '000-000-0000', // Replace all phone numbers // 'password' - not listed, so it won't be exported ]; }
Important: Only columns defined in columns() will be included in the export. Any columns not listed will be excluded from the exported SQL.
Using Callables for Dynamic Values
You can use closures or callables to generate unique values per row. The callable receives:
$original— the original value of the column$row— an array of all column values in the current row
public function columns(): array { return [ 'id' => Veil::unchanged(), // Generate unique fake email for each row 'email' => fn ($original) => fake()->unique()->safeEmail(), // Transform the original value 'name' => fn ($original) => strtoupper($original), // Access other columns via $row parameter 'email' => fn ($original, $row) => "user{$row['id']}@example.com", // Combine multiple column values 'display_name' => fn ($original, $row) => "{$row['name']} (ID: {$row['id']})", ]; }
This is useful when you need unique anonymized values per row or want to reference other columns in the transformation.
Important: If you want to use Faker or other generators to create different values per row, you must wrap them in a closure:
// ❌ Wrong - executes once, same value for all rows 'first_name' => app(Generator::class)->firstName(), // ✅ Correct - executes per row, different value for each row 'first_name' => fn () => app(Generator::class)->firstName(), // or 'first_name' => fn () => fake()->firstName(),
⚠️ Foreign Key Consistency
When exporting multiple related tables, always use Veil::unchanged() for primary keys and foreign keys to maintain referential integrity.
// ✅ Correct - IDs are preserved, relationships remain intact class VeilUsersTable implements VeilTable { public function columns(): array { return [ 'id' => Veil::unchanged(), // Primary key - keep unchanged 'name' => 'John Doe', 'email' => 'user@example.com', ]; } } class VeilPostsTable implements VeilTable { public function columns(): array { return [ 'id' => Veil::unchanged(), // Primary key - keep unchanged 'user_id' => Veil::unchanged(), // Foreign key - keep unchanged 'title' => 'Anonymized Title', ]; } }
// ❌ Wrong - This will break foreign key relationships class VeilUsersTable implements VeilTable { public function columns(): array { return [ 'id' => fn () => fake()->randomNumber(), // Don't anonymize IDs! 'name' => 'John Doe', ]; } }
Why? If you change a user's id from 1 to 999, all their posts with user_id = 1 will become orphaned because the foreign key no longer matches.
Rule of thumb: Only anonymize data columns (names, emails, addresses), never identifier columns (IDs, UUIDs, foreign keys).
Row Filtering (Query Scope)
You must define a query() method to filter which rows are exported. Return null to export all rows:
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Query\Builder as QueryBuilder; use Illuminate\Support\Facades\DB; use SignDeck\Veil\Veil; use SignDeck\Veil\Contracts\VeilTable; class VeilUsersTable implements VeilTable { public function table(): string { return 'users'; } public function columns(): array { return [ 'id' => Veil::unchanged(), 'email' => 'redacted@example.com', ]; } /** * Only export users created in the last year. * Return null to export all rows. */ public function query(): Builder|QueryBuilder|null { return DB::table('users') ->where('created_at', '>', now()->subYear()); // Or return null to export all rows: // return null; } }
The query should return a Laravel query builder instance that filters the rows you want to export. Return null to export all rows.
Note: Filtering is based on the primary key (usually id). The query is executed to get matching IDs, and only rows with those IDs are included in the export.
3. Register Your Tables
Add your Veil table classes to config/veil.php:
'tables' => [ \App\Veil\VeilUsersTable::class, \App\Veil\VeilOrdersTable::class, // Add more tables as needed ],
4. Run the Export
Execute the export command:
php artisan veil:export
This will create a timestamped SQL file (e.g., veil_2025-01-15_10-30-00.sql) on your configured disk with all specified tables and anonymized column values.
You can also specify a custom name for the export:
php artisan veil:export --name=staging-export
This will create staging-export.sql instead of the timestamped filename.
What Gets Exported?
Veil exports data only. It does not export:
- CREATE TABLE statements
- DROP TABLE statements
- ALTER TABLE statements
- Database schema definitions
Why? Laravel manages database schema through migrations, so Veil focuses solely on exporting and anonymizing data. This approach:
- Keeps exported files smaller and focused on data
- Aligns with Laravel's migration-based schema management
- Makes it easy to import data into existing databases that already have the schema
To use the exported data:
- Ensure your target database has the schema (run migrations)
- Import the Veil export file to populate the data
Events
Veil fires events before and after the export process, allowing you to hook into the export lifecycle.
Available Events
SignDeck\Veil\Events\ExportStarted- Fired before the export beginsSignDeck\Veil\Events\ExportCompleted- Fired after the export completes
Listening to Events
You can listen to these events in your EventServiceProvider:
use SignDeck\Veil\Events\ExportStarted; use SignDeck\Veil\Events\ExportCompleted; protected $listen = [ ExportStarted::class => [ // Your listeners here ], ExportCompleted::class => [ // Your listeners here ], ];
Event Properties
ExportStarted event contains:
$snapshotName- The custom name provided (ornullif using default)$tableNames- Array of table names being exported
ExportCompleted event contains:
$fileName- The filename of the created snapshot$snapshotName- The custom name provided (ornullif using default)$tableNames- Array of table names that were exported
Example: Logging Exports
use SignDeck\Veil\Events\ExportCompleted; use Illuminate\Support\Facades\Log; class LogExportCompleted { public function handle(ExportCompleted $event): void { Log::info('Database export completed', [ 'file' => $event->fileName, 'tables' => $event->tableNames, ]); } }
Dry Run Mode
You can preview what would be exported without actually creating the file:
php artisan veil:export --dry-run
This will show:
- Which tables will be exported
- Which columns will be included
- Estimated row counts
- What filename would be created
No files are created in dry-run mode, making it safe to test your configuration.
Security
If you discover any security related issues, please send the author an email instead of using the issue tracker.
License
Please see the license file for more information.