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+
You can install the package via Composer:
composer require signdeck/veilPublish the configuration file:
php artisan vendor:publish --tag=veil-configThis 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.
Generate a new Veil table class using the artisan command:
php artisan veil:make-table usersThis 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
];
}
}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.
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(),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).
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.
Add your Veil table classes to config/veil.php:
'tables' => [
\App\Veil\VeilUsersTable::class,
\App\Veil\VeilOrdersTable::class,
// Add more tables as needed
],Execute the export command:
php artisan veil:exportThis 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-exportThis will create staging-export.sql instead of the timestamped filename.
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
Veil fires events before and after the export process, allowing you to hook into the export lifecycle.
SignDeck\Veil\Events\ExportStarted- Fired before the export beginsSignDeck\Veil\Events\ExportCompleted- Fired after the export completes
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
],
];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
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,
]);
}
}You can preview what would be exported without actually creating the file:
php artisan veil:export --dry-runThis 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.
If you discover any security related issues, please send the author an email instead of using the issue tracker.
Please see the license file for more information.