Customize and Extend
File Export has these extension points:
- Exporter types produce the file itself (
CSV,XML,JSON). Add your own format. - Transmitters deliver the finished file to a destination.
- Output formatters turn asset relation values into the string written to the file.
- Exporter services own the whole export process, including the workspace query condition.
- Events hook into individual steps without replacing a service.
Additional Transmitters
Transmitters deliver the generated file to its destination (local directory, remote location, etc.). Add one with a PHP service class:
<?php
namespace App\DataHubFileExport\Exporter\Transmitter;
class MyCustomTransmitter extends \Pimcore\Bundle\DataHubFileExportBundle\Exporter\Transmitter\AbstractTransmitter {
public function execute()
{
$exporter = $this->getExporter();
$config = $exporter->getDataHubConfiguration()->getConfiguration();
$transmitterConfig = $config['deliveryDestination']['transmitter_myCustomTransmitter'];
// Process the transmitter configuration and push data to your destination
// $transmitterConfig contains the settings from the configuration dialog
}
}
... and add it as a Service with the tag pimcore.datahub.fileExport.exporter.transmitter
App\DataHubFileExport\Exporter\Transmitter\MyCustomTransmitter:
shared: false
tags:
- {
name: "pimcore.datahub.fileExport.exporter.transmitter",
type: "myCustomTransmitter",
}
The transmitter reads its own settings from deliveryDestination.transmitter_<type>, so a service registered with
type: myCustomTransmitter reads deliveryDestination.transmitter_myCustomTransmitter. Keep that naming convention.
The export pipeline runs your transmitter as soon as it is tagged as above, but nobody can select it in Pimcore Studio until you also register it there.
Making the Transmitter Selectable in Pimcore Studio
The Delivery Type dropdown and the settings form shown below it are driven by a registry in the Studio dependency injection container. Each entry is a dynamic type; the six built-in transmitters are registered the same way. Add yours from your own Pimcore Studio plugin (for the plugin scaffolding itself, see the Studio UI Bundle extension documentation).
The File Export plugin exposes its SDK as the module federation remote pimcore_datahubfileexport_bundle. Add it to the
remotes of your plugin's rsbuild.config.ts next to the Studio UI remote:
remotes: {
'@pimcore/studio-ui-bundle': createDynamicRemote('pimcore_studio_ui_bundle'),
'@pimcore/data-hub-file-export': createDynamicRemote('pimcore_datahubfileexport_bundle'),
},
Create a dynamic type that extends DynamicTypeTransmitterAbstract. Its id has to match the type of the service
tag, because the settings are stored under deliveryDestination.transmitter_<id>, which is where the PHP transmitter
reads them from. label is a translation key, and renderSettings() returns the form fields, each nested under
['deliveryDestination', 'transmitter_<id>', ...]. Override getDefaultValues() for settings a new configuration
should start with:
import React from 'react'
import { Form, Input } from '@pimcore/studio-ui-bundle/components'
import { injectable, useTranslation } from '@pimcore/studio-ui-bundle/app'
import { DynamicTypeTransmitterAbstract, type TransmitterSettingsProps } from '@pimcore/data-hub-file-export'
const MyCustomTransmitterSettings = ({ isWriteable }: TransmitterSettingsProps): React.JSX.Element => {
const { t } = useTranslation()
return (
<Form.Item
label={ t('app.transmitter.my-custom.endpoint') }
name={ ['deliveryDestination', 'transmitter_myCustomTransmitter', 'endpoint'] }
>
<Input disabled={ !isWriteable } />
</Form.Item>
)
}
@injectable()
export class DynamicTypeTransmitterMyCustom extends DynamicTypeTransmitterAbstract {
readonly id = 'myCustomTransmitter'
readonly label = 'app.transmitter.my-custom'
renderSettings (props: TransmitterSettingsProps): React.JSX.Element {
return <MyCustomTransmitterSettings { ...props } />
}
getDefaultValues (): Record<string, unknown> {
return { endpoint: '' }
}
}
Bind the type in your plugin's onInit and register it with the transmitter registry in a module registered during
onStartup. The registry is resolved from the container by the service identifier
DataHubFileExport/DynamicTypes/Transmitter/Registry, available as
localServiceIds['DataHubFileExport/DynamicTypes/Transmitter/Registry'] from the SDK:
import { type IAbstractPlugin } from '@pimcore/studio-ui-bundle'
import { container } from '@pimcore/studio-ui-bundle/app'
import { type DynamicTypeTransmitterRegistry, localServiceIds } from '@pimcore/data-hub-file-export'
import { DynamicTypeTransmitterMyCustom } from './dynamic-type-transmitter-my-custom'
export const MyPlugin: IAbstractPlugin = {
name: 'my-plugin',
onInit: ({ container: pluginContainer }): void => {
pluginContainer.bind('MyPlugin/DynamicTypes/Transmitter/MyCustom').to(DynamicTypeTransmitterMyCustom).inSingletonScope()
},
onStartup: ({ moduleSystem }): void => {
moduleSystem.registerModule({
onInit: (): void => {
const transmitterRegistry = container.get<DynamicTypeTransmitterRegistry>(
localServiceIds['DataHubFileExport/DynamicTypes/Transmitter/Registry']
)
transmitterRegistry.registerDynamicType(container.get('MyPlugin/DynamicTypes/Transmitter/MyCustom'))
}
})
}
}
The order of registration determines the order of the options in the dropdown. A configuration whose delivery type has no registered dynamic type keeps its stored settings and shows the raw type instead of a settings form.
The SDK is consumed at runtime through module federation; no npm package with type declarations is published for it yet.
Until there is one, declare the module in your plugin (for example in a types/data-hub-file-export.d.ts) with the
members you use, or type the imports loosely.
Additional Exporter Types
To add an export format besides CSV, XML and JSON, extend
Pimcore\Bundle\DataHubFileExportBundle\Exporter\Type\AbstractExporter and register the service with the tag
pimcore.datahub.fileExport.exporter.type. The type attribute becomes the entry in the File Type dropdown, which
is built from the registered types, so no frontend change is needed.
App\DataHubFileExport\Exporter\Type\MyJson:
shared: false
tags:
- { name: "pimcore.datahub.fileExport.exporter.type", type: "MY_JSON" }
Types are collected into a map keyed by the type attribute, so registering two services with the same type leaves
the last one registered in effect. That is how the override below works, and why a new format needs a distinct type.
Customize a Built-in Exporter Type or Transmitter
Override the existing service definition. The shipped definitions are in
src/Resources/config/services.yml.
To override the XML exporter type, create your service class:
<?php
namespace App\DataHubFileExport\Exporter\Type;
class XML extends \Pimcore\Bundle\DataHubFileExportBundle\Exporter\Type\XML
{
protected $itemKey = 'myCustomItemKey';
protected function writeData($data)
{
return parent::writeData($data);
}
protected function getExistingData(): array
{
return parent::getExistingData();
}
}
... and override the service definition in your services.yml with:
Pimcore\Bundle\DataHubFileExportBundle\Exporter\Type\XML:
class: App\DataHubFileExport\Exporter\Type\XML
shared: false
tags:
- { name: "pimcore.datahub.fileExport.exporter.type", type: "XML" }
This changes the XML node item key from item to myCustomItemKey.
Extending the JSON Exporter
Nest the data under a key so the top level is an object instead of an array:
class JSONNested extends \Pimcore\Bundle\DataHubFileExportBundle\Exporter\Type\JSON
{
protected $itemKey = 'items';
protected function writeData($data)
{
return parent::writeData([$this->itemKey => $data]);
}
protected function extractData($data)
{
return $data[$this->itemKey];
}
}
This works because extractData() is protected: it is the counterpart of writeData() and unwraps the nesting
again when an existing file is re-read.
Output Formatters
Output formatters turn an asset relation value into the string written to the file. The bundle ships formatters for
image, hotspotimage, imageGallery and video; each resolves the asset and applies the Image Thumbnail or
Video Thumbnail from the schema, falling back to the original asset path.
Add your own by implementing
Pimcore\Bundle\DataHubFileExportBundle\Exporter\OutputFormatter\OutputFormatterInterface and tagging the service:
App\DataHubFileExport\OutputFormatter\MyFormatter:
tags:
- { name: "pimcore.datahub.fileExport.output_formatter" }
getType() returns the column type the formatter handles.
format(string $value, string $configName, array $config) returns the exported value. Formatters are indexed by type,
so registering one for an existing type replaces the shipped formatter for that type.
This replaced the fixAssetRelations() override that existed before 2026.1. See
Upgrade Notes.
Custom Exporter Services
The exporter service controls the whole export process: the workspace query condition, which items are relevant, how their data is read, and which transmitter runs. The service is resolved per configuration, in this order:
- Custom Export Service on the Workspaces tab, if set. The dropdown lists every service tagged
pimcore.datahub.fileExport.exporter. pimcore.datahub.fileExport.exporter.file, for every configuration using the Pimcore Studio column format.- The
defaultExportercontainer setting, for legacy configurations that still carry the pre-3.4 tree-based schema.
pimcore_data_hub_file_export:
defaultExporter: "pimcore.datahub.fileExport.exporter.file"
The value above is the shipped default. Because step 2 takes precedence, changing defaultExporter has no effect on
configurations already migrated to the Studio format; set Custom Export Service for those.
Extend Pimcore\Bundle\DataHubFileExportBundle\Exporter\File, which reads the data through the Pimcore Studio
GridServiceInterface:
<?php
namespace App\DataHubFileExport\Exporter;
use Pimcore\Bundle\DataHubFileExportBundle\Exporter\File;
use Pimcore\Model\DataObject;
class MyFile extends File
{
public function getItemData(DataObject\Concrete $object): bool|array
{
$data = parent::getItemData($object);
if ($data === false) {
return false;
}
// Add a computed field
$data['Full Name'] = ($data['First Name'] ?? '') . ' ' . ($data['Last Name'] ?? '');
return $data;
}
}
Register it as public and not shared, and tag it:
app.datahub.my-exporter:
class: App\DataHubFileExport\Exporter\MyFile
public: true
shared: false
tags:
- { name: "pimcore.datahub.fileExport.exporter" }
The service must be public: true and shared: false. A shared service leaks state between export runs, and a private
one cannot be resolved by id from the configuration.
Override getQueryCondition() to change which objects the workspace selection resolves to. It returns the SQL
condition applied to the object listing. The parent condition is an empty string when no workspace is configured, and
an unparenthesized OR chain when several are, so group it before appending your own AND:
protected function getQueryCondition()
{
$parent = parent::getQueryCondition();
$own = '`published` = 1';
return $parent ? '(' . $parent . ') AND ' . $own : $own;
}
Overriding execute() wraps the delivery step:
public function execute($data)
{
// Perform custom processing before calling the parent
return parent::execute($data);
}
Filename Date
useDownloadDateForFileName controls which date fills the %s placeholder of a Download filename. It defaults to
true.
pimcore_data_hub_file_export:
useDownloadDateForFileName: true
With true the name carries the date and time of the download request. With false it carries the last modified date
taken from the exported file's meta information.
Next Steps
- Events: hook into individual export steps without replacing a service.