spatie/laravel-personal-data-export
Generate GDPR-style personal data exports as ZIP files in Laravel. Define what to include via a model method, add JSON and files (local or S3), store zips privately, email users a download link, and clean up old exports with an artisan command.
Installation
composer require spatie/laravel-personal-data-export
php artisan vendor:publish --provider="Spatie\PersonalDataExport\PersonalDataExportServiceProvider"
config/personal-data-export.php) and migrations (if using the built-in storage).Configure Storage
Update config/personal-data-export.php to define:
storage (default: local, options: local, s3, ftp, rackspace).disk (e.g., public, s3).path (e.g., personal-data-exports).expiration_in_minutes (default: 60, auto-deletes old exports).First Use Case: Basic Export Trigger an export for the current user (e.g., in a controller or command):
use Spatie\PersonalDataExport\Jobs\CreatePersonalDataExportJob;
dispatch(new CreatePersonalDataExportJob(auth()->user()));
Verify Setup
storage/path/to/exports (or configured disk) for generated ZIPs.Define Exportable Data
Use the Exportable trait or implement Spatie\PersonalDataExport\Concerns\ExportsPersonalData:
use Spatie\PersonalDataExport\Concerns\ExportsPersonalData;
class User extends Authenticatable
{
use ExportsPersonalData;
public function getPersonalDataExportables(): array
{
return [
'user_data' => [
'email' => $this->email,
'name' => $this->name,
'created_at' => $this->created_at->format('Y-m-d'),
],
'related_orders' => $this->orders->map(fn ($order) => [
'id' => $order->id,
'total' => $order->total,
]),
];
}
}
getPersonalDataExportables(): Returns an associative array of data (keys become folder names in the ZIP).getPersonalDataExportFilename(): Customize the ZIP filename (default: user-{id}-export-{timestamp}.zip).Customize Export Format
Spatie\PersonalDataExport\PersonalDataExporter to modify serialization (e.g., JSON vs. CSV):
public function export(array $data): string
{
return json_encode($data, JSON_PRETTY_PRINT);
}
README.md or LICENSE.txt in the ZIP:
public function getPersonalDataExportAdditionalFiles(): array
{
return [
'README.md' => 'This file contains your personal data.',
];
}
Trigger Exports Programmatically
dispatch(new CreatePersonalDataExportJob($user, $customFilename));
UserDeleted::dispatch($user);
// In a listener:
CreatePersonalDataExportJob::dispatch($user)->delay(now()->addMinutes(5));
Route::post('/export-data', function () {
$this->authorize('export-data');
CreatePersonalDataExportJob::dispatch(auth()->user());
return response()->json(['message' => 'Export initiated']);
});
Handle Large Exports
cursor() or chunk():
public function getPersonalDataExportables(): array
{
return [
'posts' => $this->posts->cursor()->map(fn ($post) => [
'title' => $post->title,
]),
];
}
Spatie\PersonalDataExport\PersonalDataExporter to stream files directly to the ZIP (avoids memory issues):
public function createZip(): void
{
$zip = new ZipArchive;
$zip->open('path/to/export.zip', ZipArchive::CREATE | ZipArchive::OVERWRITE);
foreach ($this->data as $folder => $items) {
foreach ($items as $filename => $content) {
$zip->addFromString("{$folder}/{$filename}.json", json_encode($content));
}
}
$zip->close();
}
Integrate with Notifications
php artisan vendor:publish --tag=personal-data-export-views
app/Notifications/PersonalDataExportNotification.php:
public function toMail($notifiable)
{
$url = route('personal-data-export.download', [
'export' => $this->export->id,
'signature' => $this->export->signature,
]);
return (new MailMessage)
->subject('Your Personal Data Export')
->line("Click here to download your data: {$url}");
}
Storage Permissions
public) has write permissions:
chmod -R 775 storage/app/public/personal-data-exports
.env and test connectivity:
php artisan storage:link # If using public disk
Memory Limits
memory_limit. Increase it in php.ini or .env:
memory_limit = 512M
Expiration Quirks
expiration_in_minutes are auto-deleted by the DeleteOldPersonalDataExports command.php artisan personal-data-export:delete-old
schedule:run).Authentication Bypass
public_download_url in config to a signed URL (e.g., S3 pre-signed URL).Route::get('/download-export/{export}', [ExportController::class, 'download'])
->middleware('guest'); // Remove auth if needed
Data Serialization Issues
JsonSerializable objects to getPersonalDataExportables().->toArray() or json_encode() to flatten relationships:
'profile' => $this->profile->toArray(),
Job Failures
failed_jobs table for queued job errors.(new CreatePersonalDataExportJob($user))->handle();
Log Export Contents Add a temporary method to inspect data before export:
public function debugExportData()
{
dd($this->getPersonalDataExportables());
}
Test with Minimal Data Start with a single model and small dataset to verify the pipeline works.
Check Disk Events
Listen for personal-data-export.created events to log export paths:
PersonalDataExportCreated::listen(function ($export) {
Log::info('Export created:', ['path' => $export->path]);
});
Custom Exporters Create a custom exporter for non-standard formats (e.g., XML):
namespace App\Exports;
use Spatie\PersonalDataExport\PersonalDataExporter;
class XmlExporter extends PersonalDataExporter
{
protected function export(array $data): string
{
return $this->arrayToXml($data);
}
private function arrayToXml(array $data): string
{
// Implement XML conversion logic
}
}
Register it in config/personal-data-export.php:
'exporter' => \App\Exports\XmlExporter::class,
Dynamic Data Sources Fetch data dynamically (e.g., from an API) in `getPersonalData
How can I help you explore Laravel packages today?