Skip to main content
Version: Next

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.

note

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:

  1. Custom Export Service on the Workspaces tab, if set. The dropdown lists every service tagged pimcore.datahub.fileExport.exporter.
  2. pimcore.datahub.fileExport.exporter.file, for every configuration using the Pimcore Studio column format.
  3. The defaultExporter container 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" }
warning

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.