jorisdugue / easyadmin-extra-bundle
Advanced data tools for EasyAdmin 5, including exports, security helpers, and future bulk operations.
Package info
github.com/jorisdugue/easyadmin-extra-bundle
Type:symfony-bundle
pkg:composer/jorisdugue/easyadmin-extra-bundle
Requires
- php: >=8.2
- ext-dom: *
- easycorp/easyadmin-bundle: ^5.0
- phpoffice/phpspreadsheet: ^5.0
- symfony/config: ^7.4 || ^8.0
- symfony/dependency-injection: ^7.4 || ^8.0
- symfony/finder: ^7.4 || ^8.0
- symfony/framework-bundle: ^7.4 || ^8.0
- symfony/http-foundation: ^7.4 || ^8.0
- symfony/http-kernel: ^7.4 || ^8.0
- symfony/property-access: ^7.4 || ^8.0
- symfony/routing: ^7.4 || ^8.0
- symfony/security-bundle: ^7.4 || ^8.0
- symfony/security-core: ^7.4 || ^8.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.94
- phpstan/phpstan: ^2.1
- phpstan/phpstan-doctrine: ^2.0
- phpstan/phpstan-symfony: ^2.0
- phpunit/phpunit: ^11.0
README
Export and data safety tools for EasyAdmin 5 (Symfony 7.4/8, PHP 8.2+, PHP 8.5 ready)
π§ Overview
EasyAdmin Extra Bundle extends EasyAdmin with advanced export capabilities and data control tools, while staying aligned with EasyAdmin conventions and keeping behavior explicit.
It provides:
- π€ Structured data exports (CSV, XLSX, JSON, XML)
- π§© An independent export field system
- ποΈ Export sets / profiles for multiple export schemas per CRUD
- π Strong security defaults (masking, formula protection, role-based access)
- π Optional preview flow before download
- π¦ Batch export for selected rows directly from EasyAdmin
- βοΈ Flexible action display (buttons or dropdown)
- β‘ High performance for large datasets
π― Core Idea
EasyAdmin fields describe the admin UI.
Export fields describe the export contract.
These two layers are intentionally independent.
This allows you to:
- export fields not displayed in EasyAdmin
- compute custom values at export time
- apply masking and transformations only for export
- control column order independently
- define multiple export schemas for the same CRUD
- keep your admin UI simple while exposing richer data externally
β Why this bundle?
EasyAdmin is great for CRUD operations, but real-world backoffices often need more:
- exporting large datasets safely
- exporting only a selected subset of rows
- exposing different export schemas for different teams or use cases
- masking sensitive data (GDPR, finance, healthcareβ¦)
- applying transformations at export time
- handling large datasets efficiently
π This bundle focuses on data control, safety, and developer ergonomics.
It is designed to be non-intrusive: opt-in with #[AdminExport], explicit behavior, no hidden magic.
β¨ Features
π€ Export
- CSV export (streamed, memory efficient)
- XLSX export
- JSON export
- XML export
- Full export or filtered export (EasyAdmin context-aware)
- Optional export preview per format
- Batch export for selected rows
- Custom filename support
- Configurable max rows
- Field-level transformations
- Custom export schema independent from EasyAdmin fields
- Multiple export sets / profiles per CRUD
ποΈ Export sets / profiles
A single CRUD can expose multiple export configurations.
Typical use cases:
- default export for day-to-day operations
- gdpr export with masked or restricted fields
- finance export with accounting-specific columns
- support export with operational data only
Each export set can define:
- its own field list
- its own labels
- its own role restrictions
- its own visibility in the UI
β οΈ If you implement export sets explicitly, a default set is required.
Export set names are validated and must use lowercase letters, digits, _ or -.
π Security
-
Protection against spreadsheet formula injection (CSV & XLSX)
-
Safe defaults (formulas disabled by default)
-
Role-based access control (
requiredRole/requiredRoles) -
Role-restricted export sets
-
Role-based field visibility
-
Opt-in export via attribute
-
Full sanitization of exported values
-
Strict row count validation to enforce export limits safely
-
Built-in masking helpers:
mask()redact()partialMask()
β‘ Performance
- CSV exports are streamed (
php://output) - Uses Doctrine
toIterable()(no full dataset in memory) - Optional EntityManager clearing during iteration
- Strict row count validation before export
- Designed for large datasets
π§© Developer Experience
- Attribute-based configuration (
#[AdminExport]) - Independent export field system
- Transformers & formatters
- Export sets / profile metadata
- Extensible architecture with dedicated exporters
π¦ Installation
composer require jorisdugue/easyadmin-extra-bundle
Symfony Flex should auto-register the bundle.
β οΈ Routes configuration
This bundle uses a custom route loader.
# config/routes/easyadmin_extra.yaml easyadmin_extra: resource: . type: jorisdugue_easyadmin_extra.routes
Without this configuration, export routes will not be generated.
βοΈ Configuration (optional)
By default, the bundle scans the following directory to discover dashboards and CRUD controllers:
src/Controller
If your project uses a custom structure (recommended for modular or DDD architectures), you can configure additional discovery paths:
# config/packages/easyadmin_extra.yaml joris_dugue_easyadmin_extra: discovery_paths: - '%kernel.project_dir%/src/Controller' - '%kernel.project_dir%/src/Admin' - '%kernel.project_dir%/modules'
Default action display
You can configure how export actions are displayed globally:
# config/packages/easyadmin_extra.yaml joris_dugue_easyadmin_extra: export: action_display: dropdown
Supported values:
dropdownbuttons
π§ How discovery works
The bundle will:
- scan all configured directories
- detect EasyAdmin dashboards using
#[AdminDashboard] - detect exportable CRUD controllers using
#[AdminExport]
π No specific folder structure is required.
π This makes the bundle compatible with:
- multi-dashboard applications
- modular architectures
- monorepos or packages
π Quick Start
use JorisDugue\EasyAdminExtraBundle\Attribute\AdminExport; #[AdminExport( filename: 'users_export', formats: ['csv', 'xlsx', 'json', 'xml'], maxRows: 10000, previewEnabled: true, previewLimit: 30, batchExport: true, )] class UserCrudController extends AbstractCrudController { }
π Export routes and actions are automatically generated.
π§Ύ #[AdminExport] options
The #[AdminExport] attribute lets you configure the export behavior of a CRUD controller.
Common options include:
filenameformatsmaxRowsfullExportfilteredExportpreviewEnabledpreviewLimitpreviewLabelbatchExportbatchExportLabelrequiredRolerequiredRolescsvLabelxlsxLabeljsonLabelxmlLabelactionDisplayrouteNameroutePathallowSpreadsheetFormulas
Example:
#[AdminExport(
filename: 'users_export',
formats: ['csv', 'xlsx', 'json'],
maxRows: 10000,
previewEnabled: true,
previewLabel: 'Preview export',
batchExport: true,
batchExportLabel: 'Export selection',
requiredRoles: ['ROLE_ADMIN', 'ROLE_MANAGER'],
csvLabel: 'CSV download',
actionDisplay: 'dropdown',
)]
class UserCrudController extends AbstractCrudController
{
}
π§© Export Fields
Define your export schema independently from EasyAdmin:
use JorisDugue\EasyAdminExtraBundle\Contract\ExportFieldsProviderInterface; use JorisDugue\EasyAdminExtraBundle\Field\DateTimeExportField; use JorisDugue\EasyAdminExtraBundle\Field\TextExportField; class UserCrudController extends AbstractCrudController implements ExportFieldsProviderInterface { public static function getExportFields(?string $exportSet = null): array { return match ($exportSet) { 'gdpr' => [ TextExportField::new('email', 'Email')->mask(), TextExportField::new('phone', 'Phone')->partialMask(2, 2), DateTimeExportField::new('createdAt', 'Created at'), ], default => [ TextExportField::new('email', 'Email'), TextExportField::new('phone', 'Phone'), DateTimeExportField::new('createdAt', 'Created at'), ], }; } }
ποΈ Export Sets / Profiles
Use export sets to expose multiple export schemas for the same CRUD controller.
use JorisDugue\EasyAdminExtraBundle\Contract\ExportFieldsProviderInterface; use JorisDugue\EasyAdminExtraBundle\Contract\ExportSetMetadataProviderInterface; use JorisDugue\EasyAdminExtraBundle\Dto\ExportSetMetadata; use JorisDugue\EasyAdminExtraBundle\Field\DateTimeExportField; use JorisDugue\EasyAdminExtraBundle\Field\MoneyExportField; use JorisDugue\EasyAdminExtraBundle\Field\TextExportField; class UserCrudController extends AbstractCrudController implements ExportFieldsProviderInterface, ExportSetMetadataProviderInterface { public static function getExportSetMetadata(): array { return [ new ExportSetMetadata('default', 'Standard export'), new ExportSetMetadata('gdpr', 'GDPR export', ['ROLE_ADMIN']), new ExportSetMetadata('finance', 'Finance export', ['ROLE_FINANCE']), ]; } public static function getExportFields(?string $exportSet = null): array { return match ($exportSet) { 'gdpr' => [ TextExportField::new('email', 'Email')->mask(), TextExportField::new('phone', 'Phone')->partialMask(2, 2), DateTimeExportField::new('createdAt', 'Created at'), ], 'finance' => [ TextExportField::new('email', 'Email'), MoneyExportField::new('balance', 'Balance'), DateTimeExportField::new('createdAt', 'Created at'), ], default => [ TextExportField::new('email', 'Email'), TextExportField::new('phone', 'Phone'), DateTimeExportField::new('createdAt', 'Created at'), ], }; } }
Why use export sets?
Export sets are useful when different consumers need different export contracts:
- internal operations vs compliance
- finance vs support
- raw data vs masked data
- full export vs reduced export
π Export sets are resolved consistently across standard export, preview, and batch export flows.
β‘ Advanced Usage
Custom computed values
TextExportField::new('fullName') ->setTransformer(fn ($value, $entity) => $entity->getFirstName().' '.$entity->getLastName() );
Conditional fallback
->setTransformer(fn ($value) => $value ?? 'N/A');
π§© Nested properties & null safety
By default, property access is strict.
If a nested property path is invalid or contains a null value, an exception is thrown:
TextExportField::new('company.address.city', 'City');
β
Safe access with nullSafe()
You can safely access nested relations using nullSafe():
TextExportField::new('company.address.city', 'City') ->nullSafe();
If any part of the path is null or inaccessible, the value will be null instead of throwing an exception.
π Combine with default values
TextExportField::new('manager.email', 'Manager') ->nullSafe() ->setDefault('N/A');
Result:
- valid value β used as-is
- null or missing β
'N/A'
β οΈ Important
nullSafe() catches property access errors, including:
- null intermediate relations
- invalid property paths (typos)
- inaccessible properties
π Use with caution during development to avoid hiding mistakes.
π Masking and transformations
TextExportField::new('email')->mask(); TextExportField::new('phone')->partialMask(2, 2); TextExportField::new('ssn')->redact();
You can also combine masking with format-specific behavior and custom transformers.
π Field visibility and labels
Fields can be customized depending on roles and formats.
By role
You can:
- show fields only for some roles
- hide fields for some roles
- override labels depending on the current role
Typical helpers include:
onlyForRole()/onlyForRoles()hideForRole()/hideForRoles()showForRole()/showForRoles()setLabelForRole()/setLabelsForRoles()
By format
You can also customize field visibility and labels depending on the export format:
- show fields only in some formats
- hide fields in some formats
- override labels depending on the current format
Typical helpers include:
onlyOnFormat()/onlyOnFormats()hideOnFormat()/hideOnFormats()showOnFormat()/showOnFormats()setLabelForFormat()/setLabelsForFormats()
This makes it possible to expose different columns and labels depending on the target audience and output format.
π§ Field ordering
By default, fields are exported in declaration order.
You can override this using position():
TextExportField::new('email')->position(10); TextExportField::new('name')->position(5);
Fields with a defined position are sorted first, then remaining fields keep their declaration order.
π Custom row mapping
If you need full control over the exported row structure, you can implement CustomExportRowMapperInterface.
use JorisDugue\EasyAdminExtraBundle\Contract\CustomExportRowMapperInterface; class UserCrudController extends AbstractCrudController implements CustomExportRowMapperInterface { public function mapExportRow(object $entity): array { return [ 'email' => $entity->getEmail(), 'phone' => $entity->getPhone(), 'createdAt' => $entity->getCreatedAt()?->format('Y-m-d H:i:s'), ]; } }
Important behavior
- returned rows must be keyed by the configured export property names
- all configured export properties must be present
- missing keys trigger an explicit exception
- additional keys are ignored
π This contract is useful when you want to fully control how export rows are built while still reusing the bundleβs field ordering, labels, masking, and exporter pipeline.
π¦ Batch export
You can export only selected entities directly from EasyAdmin using batch actions.
#[AdminExport(
formats: ['csv', 'xlsx'],
batchExport: true,
)]
class UserCrudController extends AbstractCrudController
{
}
π This automatically adds batch export actions in EasyAdmin for supported formats.
How batch export works
- select rows in the EasyAdmin list view
- use the batch action dropdown
- choose an export format
- only selected entities are exported
Batch export behavior
- uses Doctrine metadata to detect identifier type
- supports integer and string identifiers
- rejects composite identifiers with an explicit exception
- fully reuses the export configuration (fields, masking, limits, sets, formats)
π Preview flow
You can enable an export preview page before download:
#[AdminExport(
formats: ['csv', 'xml'],
previewEnabled: true,
previewLimit: 20,
)]
class UserCrudController extends AbstractCrudController
{
}
Preview uses the same export configuration as the final export:
- field visibility
- masking
- labels
- formats
- export set selection
π What users validate is aligned with actual export output.
π’ Custom export count
In some cases, the default export count strategy cannot reliably determine how many rows will be exported (for example grouped queries or complex joins).
To handle these situations, you can provide your own count query:
use Doctrine\ORM\QueryBuilder; use JorisDugue\EasyAdminExtraBundle\Contract\CustomExportCountQueryBuilderInterface; class OrderCrudController extends AbstractCrudController implements CustomExportCountQueryBuilderInterface { public function createExportCountQueryBuilder(): QueryBuilder { return $this->getEntityManager() ->createQueryBuilder() ->select('COUNT(o.id)') ->from(Order::class, 'o'); } }
π This custom query always takes precedence over the default strategy.
π EasyAdmin Integration
This bundle integrates directly with EasyAdmin:
- automatically uses current filters
- respects search queries
- respects sorting
- works directly with CRUD controllers
- supports exporting selected rows via batch actions
- supports export sets across the UI flow
- no manual query handling required
π Export reflects the current admin context.
π§ How it works
EasyAdmin CRUD
β
Filters / Search / QueryBuilder
β
Export Engine
β
Export Fields (custom schema)
β
Output (CSV / XLSX / JSON / XML)
π Supported Formats
| Format | Notes |
|---|---|
| CSV | Streamed, best for large datasets |
| XLSX | Spreadsheet export |
| JSON | Structured data |
| XML | Structured, interoperable markup |
π Security
Spreadsheet formula injection
By default, all exports are protected.
To allow formulas:
allowSpreadsheetFormulas: true
β οΈ Warning: This can expose users to security risks if exported data is untrusted.
Role restrictions
You can restrict:
- the export itself via
requiredRoleorrequiredRoles - individual export sets via metadata roles
- individual fields via field-level visibility rules
This makes it possible to expose different exports depending on the current user.
β οΈ Limitations
- CSV is recommended for large datasets
- XLSX uses more memory
Export row count
The bundle uses a strict and safe row counting strategy before exporting data.
The default strategy supports:
- a single root entity
- a single scalar identifier
The following cases are not supported by the default count strategy:
GROUP BYHAVING- composite identifiers
- complex queries altering the root entity cardinality
In these situations, the export will fail with an explicit exception.
π To handle these cases, implement a custom count query (see above).
π§ Design choices
Strict counting strategy
The bundle intentionally uses a strict counting strategy.
Instead of returning potentially incorrect counts, it will:
- fail explicitly on ambiguous queries
- require a custom count implementation when needed
This ensures:
- accurate export limits (
maxRows) - predictable behavior
- safer data handling
π Comparison
| Feature | Native EasyAdmin | This Bundle |
|---|---|---|
| Export | β | β |
| CSV streaming | β | β |
| XLSX export | β | β |
| JSON export | β | β |
| XML export | β | β |
| Preview before export | β | β |
| Data masking | β | β |
| Formula protection | β | β |
| Custom export schema | β | β |
| Export sets / profiles | β | β |
| Batch export (selected rows) | β | β |
π§ Philosophy
- Stay close to EasyAdmin conventions
- Avoid magic and hidden behaviors
- Keep behavior explicit and opt-in
- Provide safe defaults
- Focus on real-world backoffice needs
π£οΈ Roadmap
- Batch export (selected rows)
- Export sets / profiles
- Role-based field visibility
- Additional exporter-level configuration options
- Additional field helpers
- Advanced batch operations (update / delete / workflows)
- Audit trail for exports
- Async exports for heavy datasets
π€ Contributing
PRs and feedback are welcome.
π License
MIT