ecourty / doctrine-export-bundle
A Symfony bundle for exporting Doctrine entities to various formats (CSV, JSON, XML)
Installs: 3
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
Type:symfony-bundle
pkg:composer/ecourty/doctrine-export-bundle
Requires
- php: >=8.3
- ext-xmlwriter: *
- doctrine/doctrine-bundle: ^2.18|^3.1
- doctrine/orm: ^3.0|^4.0
- psr/event-dispatcher: ^1.0
- symfony/config: ^7.0|^8.0
- symfony/dependency-injection: ^7.0|^8.0
- symfony/http-kernel: ^7.0|^8.0
- symfony/property-access: ^7.0|^8.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.40
- phpstan/phpstan: ^2.0
- phpstan/phpstan-doctrine: ^2.0
- phpstan/phpstan-phpunit: ^2.0
- phpstan/phpstan-symfony: ^2.0
- phpunit/phpunit: ^12.0
- symfony/framework-bundle: ^7.0|^8.0
- symfony/phpunit-bridge: ^7.0|^8.0
- symfony/var-exporter: ^7.0|^8.0
- symfony/yaml: ^7.0|^8.0
This package is auto-updated.
Last update: 2025-12-14 19:02:36 UTC
README
A flexible and extensible Symfony bundle for exporting Doctrine entities to various formats (CSV, JSON, XML).
Compatible with PHP 8.3+, Symfony 7.x/8.x, and Doctrine ORM 3.x/4.x 🎉
📖 Table of Contents
- Installation
- Quick Start
- Key Features
- Performance
- Usage
- Advanced Options
- Supported Formats
- Development
- Requirements
- License
Installation
composer require ecourty/doctrine-export-bundle
If you're not using Symfony Flex, enable the bundle manually:
// config/bundles.php return [ // ... Ecourty\DoctrineExportBundle\DoctrineExportBundle::class => ['all' => true], ];
Quick Start
Export entities in a few lines:
use Ecourty\DoctrineExportBundle\Contract\DoctrineExporterInterface; use Ecourty\DoctrineExportBundle\Enum\ExportFormat; // Inject the service public function __construct( private DoctrineExporterInterface $exporter ) {} // Export to CSV $this->exporter->exportToFile( entityClass: User::class, format: ExportFormat::CSV, filePath: '/tmp/users.csv' ); // Stream to browser return new StreamedResponse( $this->exporter->exportToGenerator(User::class, ExportFormat::JSON), Response::HTTP_OK, ['Content-Type' => 'application/json'] );
That's it! 🚀
✨ Key Features
- 🎯 Field Selection - Export only the fields you need
- 🔍 Advanced Filtering - Filter by criteria, pagination, ordering
- 📦 Multiple Formats - CSV, JSON, XML out of the box
- 💾 Memory Efficient - Streaming support via generators (< 5 MB for 100k entities)
- ⚡ High Performance - 42,000 entities/second (JSON), linear O(n) scaling
- 🔌 Extensible - Add custom formats with the Strategy pattern
- 🎯 Type-Safe - PHP 8.1+ enums for format specification
- 🛡️ XML Native - Uses XMLWriter for guaranteed valid XML
Performance
Benchmarked with realistic dataset (10,000 entities, 15 fields):
| Format | Time (10k entities) | Memory Usage | Throughput |
|---|---|---|---|
| CSV | 0.274s | 2.00 MB | 36,496 ent/s |
| JSON | 0.238s | < 0.1 MB | 42,017 ent/s |
| XML | 0.314s | < 0.1 MB | 31,847 ent/s |
| Generator | 0.305s | < 0.1 MB | 32,787 ent/s |
Tested: 10,000 entities × 15 fields = 150,000 data points
Linear scaling: tested with 100,000 entities in ~2.5s with < 5 MB memory 🚀
Usage
Basic Export to File
use Ecourty\DoctrineExportBundle\Contract\DoctrineExporterInterface; use Ecourty\DoctrineExportBundle\Enum\ExportFormat; use App\Entity\User; class UserExportService { public function __construct( private DoctrineExporterInterface $exporter ) {} public function exportActiveUsers(): void { $this->exporter->exportToFile( entityClass: User::class, format: ExportFormat::CSV, filePath: '/tmp/active_users.csv', criteria: ['isActive' => true], limit: 1000, orderBy: ['createdAt' => 'DESC'] ); } }
Streaming Export (Binary Response)
Simply pass the generator to a StreamedResponse:
use Symfony\Component\HttpFoundation\StreamedResponse; #[Route('/export/users')] public function export(DoctrineExporterInterface $exporter): StreamedResponse { $format = ExportFormat::CSV; return new StreamedResponse( $exporter->exportToGenerator( entityClass: User::class, format: $format, criteria: ['isActive' => true] ), 200, [ 'Content-Type' => $format->getMimeType(), 'Content-Disposition' => 'attachment; filename="users.' . $format->getExtension() . '"' ] ); }
Events
The bundle dispatches events before and after each export, allowing you to hook into the export lifecycle for logging, monitoring, or custom logic.
Events are optional - if no event dispatcher is configured, exports work normally without events.
The bundle uses the PSR-14 EventDispatcherInterface (Psr\EventDispatcher\EventDispatcherInterface), making it compatible with any PSR-14 compliant event dispatcher, not just Symfony's.
Available Events
PreExportEvent- Dispatched before export beginsPostExportEvent- Dispatched after export completes (includes count and duration)
Example: Logging Exports
use Ecourty\DoctrineExportBundle\Event\PostExportEvent; use Symfony\Component\EventDispatcher\Attribute\AsEventListener; #[AsEventListener] public function onPostExport(PostExportEvent $event): void { $this->logger->info('Export completed', [ 'entity' => $event->getEntityClass(), 'count' => $event->getExportedCount(), ]); }
Example: Performance Monitoring
use Ecourty\DoctrineExportBundle\Event\PostExportEvent; #[AsEventListener] public function onPostExport(PostExportEvent $event): void { $duration = $event->getDurationInSeconds(); $throughput = $event->getExportedCount() / $duration; $this->metrics->gauge('export.duration', $duration); $this->metrics->gauge('export.throughput', $throughput); }
Event Properties
PreExportEvent:
getEntityClass()- Entity class being exportedgetFormat()- Export format (CSV, JSON, XML)getCriteria()- Filter criteriagetLimit()- Result limitgetOffset()- Result offsetgetOrderBy()- Sort ordergetFields()- Selected fieldsgetOptions()- Export options
PostExportEvent:
- All PreExportEvent properties
getExportedCount()- Number of entities exportedgetDurationInSeconds()- Export duration measured with microsecond precision (float)
Supported Formats
| Format | Extension | Description | Use Case |
|---|---|---|---|
| CSV | .csv |
Comma-separated values | Spreadsheets, Excel |
| JSON | .json |
JSON format | APIs |
| XML | .xml |
XML with configurable structure | Legacy enterprise systems |
Format Examples
// CSV $exporter->exportToFile(User::class, ExportFormat::CSV, '/tmp/users.csv'); // JSON $exporter->exportToFile(User::class, ExportFormat::JSON, '/tmp/users.json'); // XML $exporter->exportToFile(User::class, ExportFormat::XML, '/tmp/users.xml');
Advanced Options
Custom Entity Processors
Implement custom data transformations with entity processors. They allow you to modify exported data, add virtual fields, or apply business logic during export.
Creating a Custom Processor
use Ecourty\DoctrineExportBundle\Contract\EntityProcessorInterface; class EmailMaskingProcessor implements EntityProcessorInterface { public function process(object $entity, array $data, array $options): array { // Mask email addresses if (isset($data['email'])) { $data['email'] = preg_replace('/(?<=.).(?=.*@)/', '*', $data['email']); } return $data; } }
Using Processors
$exporter->exportToFile( entityClass: User::class, format: ExportFormat::CSV, filePath: '/tmp/users.csv', processors: [new EmailMaskingProcessor()] );
Adding Virtual Fields
class UserVirtualFieldsProcessor implements EntityProcessorInterface { public function process(object $entity, array $data, array $options): array { assert($entity instanceof User); // Add computed fields $data['displayName'] = $entity->getFirstName() . ' ' . $entity->getLastName(); $data['ageCategory'] = $entity->getAge() >= 30 ? 'senior' : 'junior'; return $data; } } // Export with virtual fields $exporter->exportToFile( entityClass: User::class, format: ExportFormat::JSON, filePath: '/tmp/users.json', fields: ['firstName', 'displayName', 'ageCategory'], // Include virtual fields processors: [new UserVirtualFieldsProcessor()] );
Chaining Multiple Processors
Processors are executed in order, allowing you to compose transformations:
$exporter->exportToFile( entityClass: User::class, format: ExportFormat::CSV, filePath: '/tmp/users.csv', processors: [ new EmailMaskingProcessor(), // First: mask emails new UppercaseProcessor(), // Then: uppercase all strings new UserVirtualFieldsProcessor(), // Finally: add virtual fields ] );
Performance Optimization: Disable Default Processor
When using a fully custom processor that handles all data extraction, disable the default processor for better performance:
class FullyCustomProcessor implements EntityProcessorInterface { public function process(object $entity, array $data, array $options): array { // Handle ALL field extraction yourself $data['id'] = $entity->getId(); $data['email'] = $entity->getEmail(); // ... handle all fields return $data; } } $exporter->exportToFile( entityClass: User::class, format: ExportFormat::JSON, filePath: '/tmp/users.json', fields: ['id', 'email'], options: [ // Skip default processor - custom processor handles everything DoctrineExporterInterface::OPTION_DISABLE_DEFAULT_PROCESSOR => true, ], processors: [new FullyCustomProcessor()] );
Note: The default processor handles property access, associations, and data normalization. Only disable it when your custom processor fully replaces this functionality.
Field Selection
You can specify which fields to export. If not specified, all entity fields are exported:
// Export only specific fields $exporter->exportToFile( entityClass: User::class, format: ExportFormat::CSV, filePath: '/tmp/users_minimal.csv', fields: ['id', 'email', 'firstName', 'lastName'] // Only these fields ); // Export all fields (default) $exporter->exportToFile( entityClass: User::class, format: ExportFormat::CSV, filePath: '/tmp/users_full.csv', fields: [] // Empty = all fields );
Note: Field names are validated against the entity metadata. If you specify a field that doesn't exist, an InvalidCriteriaException will be thrown.
Export Options
use Ecourty\DoctrineExportBundle\Contract\DoctrineExporterInterface; $exporter->exportToFile( entityClass: User::class, format: ExportFormat::CSV, filePath: '/tmp/users.csv', options: [ // Boolean values as integers (default: true) DoctrineExporterInterface::OPTION_BOOLEAN_TO_INTEGER => false, // Custom datetime format (default: ATOM) DoctrineExporterInterface::OPTION_DATETIME_FORMAT => 'Y-m-d H:i:s', // Custom null value representation (default: null) DoctrineExporterInterface::OPTION_NULL_VALUE => '', // Strict field validation - throw exception if field doesn't exist (default: false) DoctrineExporterInterface::OPTION_STRICT_FIELDS => true, // Disable default processor for performance (default: false) // Only use when custom processor handles all data extraction DoctrineExporterInterface::OPTION_DISABLE_DEFAULT_PROCESSOR => true, ] );
Available Options
| Option | Type | Default | Description |
|---|---|---|---|
OPTION_BOOLEAN_TO_INTEGER |
bool |
true |
Convert boolean values to integers (1/0) instead of strings ('true'/'false') |
OPTION_DATETIME_FORMAT |
string |
DateTimeInterface::ATOM |
PHP date format for DateTime fields (e.g., 'Y-m-d H:i:s', 'c') |
OPTION_NULL_VALUE |
string|int|float |
null |
Custom representation for null values (e.g., 'NULL', 'N/A', '') |
OPTION_STRICT_FIELDS |
bool |
false |
Throw exception if a requested field doesn't exist on the entity |
OPTION_DISABLE_DEFAULT_PROCESSOR |
bool |
false |
Skip default processor when using custom processors that handle all processing |
Field Validation
The bundle automatically validates that fields in criteria and orderBy exist in the entity. If an invalid field is specified, an InvalidCriteriaException will be thrown with a helpful error message listing all available fields.
// This will throw InvalidCriteriaException if 'nonExistentField' doesn't exist $exporter->exportToFile( entityClass: User::class, format: ExportFormat::CSV, filePath: '/tmp/users.csv', criteria: ['nonExistentField' => 'value'] // ❌ Throws exception );
Memory Management
The bundle uses streaming and entity detachment for automatic memory efficiency:
// Streaming export - automatically memory efficient foreach ($exporter->exportToGenerator(User::class, ExportFormat::CSV) as $line) { echo $line; // Each entity is processed and immediately detached flush(); }
How it works:
- Uses Doctrine's
toIterable()for true streaming (one entity at a time) - Each entity is detached immediately after processing
- Detachment is safe: only affects memory tracking, not your database
- No
clear(), noflush(), no batch management needed - PHP's garbage collector handles memory automatically with the streaming approach
Why no batching? This bundle doesn't need batch processing because:
- Entities are processed one-by-one (real streaming)
- Each entity is detached immediately (no accumulation)
- Generators ensure minimal memory footprint naturally
- No circular references are created
Association Handling
Doctrine associations are automatically exported as primary keys:
// Given entities: // Article (id, title, author_id) -> ManyToOne -> User (id, name) // Article (id, title) -> ManyToMany -> Tag (id, name) $exporter->exportToFile( entityClass: Article::class, format: ExportFormat::JSON, filePath: '/tmp/articles.json', fields: ['title', 'author', 'tags'] ); // Output: // [ // {"title": "Article 1", "author": 42, "tags": [1, 2, 3]}, // {"title": "Article 2", "author": 43, "tags": [2, 4]} // ]
How it works:
- ManyToOne / OneToOne: Exported as the related entity's primary key (integer or string)
- ManyToMany / OneToMany: Exported as an array of primary keys
[1, 2, 3] - Null associations: Exported as
null - Collections: Empty collections exported as
[] - No lazy loading issues: Primary keys are extracted without triggering proxy initialization
- Format-specific rendering: JSON keeps arrays native, CSV/XML encode as JSON string
Benefits:
- Avoids N+1 queries and lazy loading issues
- Keeps export lightweight and predictable
- Easy to re-hydrate entities on import if needed
- Works seamlessly with all export formats
Development
Quality Assurance
Run all quality checks:
composer qa
Individual commands:
# Code style check composer cs-check # Fix code style composer cs-fix # Static analysis (PHPStan level 9) composer phpstan # Run tests composer test # Run all tests (including performance) composer test:all # Run only performance tests composer test:performance
Requirements
- PHP 8.3 or higher
- Symfony 7.0 or 8.0
- Doctrine ORM 3.0 or 4.0
License
MIT