PHP code example of tobento / app-import-export

1. Go to this page and download the library: Download tobento/app-import-export library. Choose the download type require.

2. Extract the ZIP file and open the index.php.

3. Add this code to the index.php.
    
        
<?php
require_once('vendor/autoload.php');

/* Start to develop here. Best regards https://php-download.com/ */

    

tobento / app-import-export example snippets


use Tobento\App\AppFactory;
use Tobento\App\ImportExport\HooksInterface;
use Tobento\App\ImportExport\RegistriesInterface;
use Tobento\App\ImportExport\Queue;
use Tobento\App\ImportExport\Repo;

// Create the app
$app = new AppFactory()->createApp();

// Add directories:
$app->dirs()
    ->dir(realpath(__DIR__.'/../'), 'root')
    ->dir(realpath(__DIR__.'/../app/'), 'app')
    ->dir($app->dir('app').'config', 'config', group: 'config')
    ->dir($app->dir('root').'public', 'public')
    ->dir($app->dir('root').'vendor', 'vendor');

// Adding boots
$app->boot(\Tobento\App\ImportExport\Boot\ImportExport::class);
$app->booting();

// Implemented interfaces:
$hooks = $app->get(HooksInterface::class);
$registries = $app->get(RegistriesInterface::class);
$jobRepository = $app->get(Repo\JobRepositoryInterface::class);
$jobResultRepository = $app->get(Repo\JobResultRepositoryInterface::class);
$exportedFileRepository = $app->get(Repo\ExportedFileRepositoryInterface::class);
$queueHandler = $app->get(Queue\QueueHandlerInterface::class);

// Run the app
$app->run();

use Tobento\App\AppFactory;
use Tobento\App\ImportExport\HooksInterface;
use Tobento\App\ImportExport\RegistriesInterface;
use Tobento\App\ImportExport\Queue;
use Tobento\App\ImportExport\Repo;

// Create the app
$app = new AppFactory()->createApp();

// Add directories:
$app->dirs()
    ->dir(realpath(__DIR__.'/../'), 'root')
    ->dir(realpath(__DIR__.'/../app/'), 'app')
    ->dir($app->dir('app').'config', 'config', group: 'config')
    ->dir($app->dir('root').'public', 'public')
    ->dir($app->dir('root').'vendor', 'vendor');

// Adding boots
$app->boot(\Tobento\App\ImportExport\Boot\HttpErrorHandler::class);
$app->boot(\Tobento\App\ImportExport\Boot\ImportExport::class);
$app->booting();

// Run the app
$app->run();

'features' => [
    new Feature\ImportExport(
        // A menu name to show the link or null if none.
        menu: 'main',
        menuLabel: 'Import & Export Jobs',
        // A menu parent name (e.g. 'system') or null if none.
        menuParent: null,
        
        // you may disable the ACL while testing for instance,
        // otherwise only users with the right permissions can access the page.
        withAcl: false,
    ),
],

'interfaces' => [
    Queue\QueueHandlerInterface::class =>
    static function(QueueInterface $queue): Queue\QueueHandlerInterface {
        return new Queue\TimeBudgetQueueHandler(
            queue: $queue,
            
            // You may define a specific queue name. If null, the default queue is used.
            queueName: null,
            
            // Maximum processing time per execution (in seconds)
            timeBudget: 20,
        );
    },
],

'features' => [
    new Feature\JobResults(
        // A menu name to show the link or null if none.
        menu: 'main',
        menuLabel: 'Job Results',
        // A menu parent name (e.g. 'system') or null if none.
        menuParent: null,
        
        // you may disable the ACL while testing for instance,
        // otherwise only users with the right permissions can access the page.
        withAcl: false,
    ),
],

'features' => [
    new Feature\ExportedFiles(
        // A menu name to show the link or null if none.
        menu: 'main',
        menuLabel: 'Exported Files',
        // A menu parent name (e.g. 'system') or null if none.
        menuParent: null,
        
        // Automatically register the signed media features instance,
        // otherwise only users with the right permissions can access the page.
        withAcl: false,
    ),
],

autoRegisterMediaFeatures: false

signedUrlExpiresInMinutes: 5

'features' => [
    new Feature\ImportedFiles(
        // A menu name to show the link or null if none.
        menu: 'main',
        menuLabel: 'Imported Files',
        // A menu parent name (e.g. 'system') or null if none.
        menuParent: null,
        
        // Automatically register the signed media features         // When enabled, only users with the correct permissions
        // can access the Imported Files page.
        withAcl: false,
    ),
],

autoRegisterMediaFeatures: false

signedUrlExpiresInMinutes: 5

use Psr\Container\ContainerInterface;
use Tobento\App\ImportExport\Registry;
use Tobento\Service\Language\AreaLanguagesInterface;
use Tobento\Service\Language\LanguagesInterface;

class ProductRepositoryWriterRegistry extends Registry\RepositoryWriter
{
    protected function resolveLanguages(ContainerInterface $container): LanguagesInterface
    {
        // Default implementation:
        $areaLanguages = $container->get(AreaLanguagesInterface::class);
        
        if ($areaLanguages->has('resources')) {
            return $areaLanguages->get('resources');
        }
        
        return $container->get(LanguagesInterface::class);
    }
}

use Psr\Container\ContainerInterface;
use Tobento\App\ImportExport\Registry;
use Tobento\Service\Language\LanguageFactory;
use Tobento.Service\Language\Languages;
use Tobento\Service\Language\LanguagesInterface;

class ProductRepositoryWriterRegistry extends Registry\RepositoryWriter
{
    protected function resolveLanguages(ContainerInterface $container): LanguagesInterface
    {
        $factory = new LanguageFactory();

        return new Languages(
            $factory->createLanguage(locale: 'en', default: true),
            $factory->createLanguage(locale: 'de', fallback: 'en'),
            $factory->createLanguage(locale: 'de-CH', fallback: 'de'),
            $factory->createLanguage(locale: 'fr', fallback: 'en', active: false),
            $factory->createLanguage(locale: 'it', fallback: 'de', active: false),
        );
    }
}

'registries' => [
    'products.controller.writer' => new Registry\CrudControllerWriter(
        // Set a display name:
        name: 'Products',
        
        // Set the CRUD controller:
        controller: ProductCrudController::class,
        
        // Define the fields that should be writable:
        withFields: ['sku', 'status', 'title', 'image', 'created_at'],
        
        // Fields that represent uploaded files and should be processed
        // by UploadedFileModifier before writing. See the modifier for details.
        withFileFields: ['image'],
    ),
]

use Psr\Container\ContainerInterface;
use Tobento\App\Crud\Action\ActionInterface;
use Tobento\App\Crud\CrudWriteRepository;
use Tobento\App\Crud\Field;
use Tobento\App\Crud\Field\FieldInterface;
use Tobento\App\Crud\Field\FieldsInterface;
use Tobento\App\ImportExport\Registry\CrudControllerWriter;
use Tobento\App\ImportExport\Exception\WriterCreateException;
use Tobento\App\ImportExport\Modifier\DotNotationToArrayModifier;
use Tobento\App\ImportExport\Modifier\UploadedFileModifier;
use Tobento\App\ImportExport\Repo\JobEntityInterface;
use Tobento\Service\ReadWrite\Modifier;
use Tobento\Service\ReadWrite\ModifiersInterface;
use Tobento\Service\ReadWrite\RowInterface;
use Tobento\Service\ReadWrite\Writer;
use Tobento\Service\ReadWrite\WriterInterface;
use function Tobento\App\Translation\trans;

class CustomProductWriter extends CrudControllerWriter
{
    public function configureFields(ActionInterface $action): iterable|FieldsInterface
    {
        // Default fields (e.g. dry-run):
        yield from parent::configureFields($action);

        // Custom option:
        yield new Field\Radios(
            name: 'writer.skip_validation',
            label: trans('Skip validation'),
        )
            ->group(trans('Writer'))
            ->options(['0' => trans('No'), '1' => trans('Yes')])
            ->selected(value: '0', action: 'create|edit');
    }

    public function createWriter(ContainerInterface $container, JobEntityInterface $jobEntity): WriterInterface
    {
        $writer = parent::createWriter($container, $jobEntity);

        // Example: wrap or adjust writer based on custom option:
        if ($jobEntity->get('writer.skip_validation') === '1') {
            $controller = $container->get($this->controller);
            
            // Use the controller's raw repository without validation (custom behavior)
            return new Writer\RepositoryWriter(
                repository: $controller->repository(),
                idName: $controller->entityIdName(),
                columns: $this->withFields,
            );
        }

        return $writer;
    }

    public function createModifiers(ContainerInterface $container, JobEntityInterface $jobEntity): ModifiersInterface
    {
        // Start with the default modifiers:
        $modifiers = new Modifier\Modifiers(
            new Modifier\ColumnMap(map: $jobEntity->columnMap()),
            new DotNotationToArrayModifier(),
            new UploadedFileModifier($this->withFileFields(), $container),
        );

        // Add a custom modifier, e.g. trim title:
        // Note: Reader applies reader-specific modifiers.
        // ColumnMap and writer-specific modifiers are applied here.
        $modifiers->add(
            new Modifier\Format(
                field: 'title',
                formatter: function ($value, RowInterface $row): string {
                    return strtolower(trim((string)$value));
                }
            )
        );

        return $modifiers;
    }
}

'registries' => [
    'products.custom.writer' => new CustomProductWriter(
        name: 'Products (Custom)',
        controller: ProductCrudController::class,
        withFields: ['sku', 'status', 'title', 'created_at'],
    ),
]

'registries' => [
    'csv.file.storage.writer' => new Registry\CsvFileStorageWriter(
        // Set a name:
        name: 'CSV File',

        // File storage where processed files are stored:
        storageName: 'uploads-private', // default

        // Folder inside the storage where files are placed:
        storageFolder: 'exports', // default
    ),
]

'registries' => [
    'csv.file.reader' => new Registry\CsvFileUploadReader(
        // Set a name:
        name: 'CSV File Upload',
        
        // Limit the maximum upload size (in KB) or null for no limit:
        maxFileSizeInKb: null, // default
        
        // File storage where uploaded files are stored:
        storageName: 'uploads-private', // default
        
        // Folder inside the storage where files are placed:
        storageFolder: 'imports', // default
    ),
]

'interfaces' => [
    Repo\ImportedFileRepositoryInterface::class =>
    static function(StoragesInterface $storages): Repo\ImportedFileRepositoryInterface {
        return new Repo\ImportedFileRepository(
            storage: $storages->get('uploads-private'),
            rootFolder: 'imports',
        );
    },
]

'registries' => [
    'storage.file.reader' => new Registry\FileStorageReader(
        // File storage where importable files are located:
        storageName: 'uploads-private',

        // Folder inside the storage (null = root):
        storageFolder: 'imports',

        // Whether to  // default
    ),
]

'interfaces' => [
    Repo\ImportedFileRepositoryInterface::class =>
    static function(StoragesInterface $storages): Repo\ImportedFileRepositoryInterface {
        return new Repo\ImportedFileRepository(
            storage: $storages->get('uploads-private'),
            rootFolder: 'imports',
        );
    },
]

'registries' => [
    'html.file.storage.writer' => new Registry\HtmlFileStorageWriter(
        // Set a display name:
        name: 'HTML File',

        // Set the allowed file extensions:
        allowedFileExtensions: ['html'],

        // File storage where processed files are stored:
        storageName: 'uploads-private', // default

        // Folder inside the storage where files are placed:
        storageFolder: 'exports', // default
    ),
]

'interfaces' => [
    Repo\ExportedFileRepositoryInterface::class =>
    static function(StoragesInterface $storages): Repo\ExportedFileRepositoryInterface {
        return new Repo\ExportedFileRepository(
            storage: $storages->get('uploads-private'),
            rootFolder: 'exports',
        );
    },
]

'registries' => [
    'html.file.storage.writer' => new Registry\HtmlFileStorageWriter(
        name: 'HTML File',
    )
        // Optionally define reader‑specific templates:
        ->views([
            'my.custom.reader' => 'custom/template',
        ]),
]

'registries' => [
    // JSON-only writer
    'json.file.storage.writer' => new Registry\JsonFileStorageWriter(
        // Set a name:
        name: 'JSON File',
        
        // Set the allowed file extensions:
        allowedFileExtensions: ['json'],
        
        // File storage where processed files are stored:
        storageName: 'uploads-private', // default
        
        // Folder inside the storage where files are placed:
        storageFolder: 'exports', // default
    ),
    
    // NDJSON-only writer
    'ndjson.file.storage.writer' => new Registry\JsonFileStorageWriter(
        // Set a name:
        name: 'NDJSON File',
        
        // Set the allowed file extensions:
        allowedFileExtensions: ['ndjson'],
        
        // File storage where processed files are stored:
        storageName: 'uploads-private', // default
        
        // Folder inside the storage where files are placed:
        storageFolder: 'exports', // default
    ),
    
    // Combined writer with selectable output format
    'json.nd.file.storage.writer' => new Registry\JsonFileStorageWriter(
        name: 'JSON or NDJSON File',
        allowedFileExtensions: ['json', 'ndjson'],
    ),    
]

'interfaces' => [
    Repo\ExportedFileRepositoryInterface::class =>
    static function(StoragesInterface $storages): Repo\ExportedFileRepositoryInterface {
        return new Repo\ExportedFileRepository(
            storage: $storages->get('uploads-private'),
            rootFolder: 'exports',
        );
    },
]

'registries' => [
    // JSON-only upload reader
    'json.file.reader' => new Registry\JsonFileUploadReader(
        // Set a name:
        name: 'JSON File Upload',
        
        // Set the allowed file extensions:
        allowedFileExtensions: ['json'],
        
        // Limit the maximum upload size (in KB) or null for no limit:
        maxFileSizeInKb: null, // default
        
        // File storage where uploaded files are stored:
        storageName: 'uploads-private', // default
        
        // Folder inside the storage where files are placed:
        storageFolder: 'imports', // default
    ),
    
    // NDJSON-only upload reader
    'ndjson.file.reader' => new Registry\JsonFileUploadReader(
        // Set a name:
        name: 'NDJSON File Upload',
        
        // Set the allowed file extensions:
        allowedFileExtensions: ['ndjson'],
        
        // Limit the maximum upload size (in KB) or null for no limit:
        maxFileSizeInKb: null, // default
        
        // File storage where uploaded files are stored:
        storageName: 'uploads-private', // default
        
        // Folder inside the storage where files are placed:
        storageFolder: 'imports', // default
    ),
    
    // Reader that accepts both JSON and NDJSON uploads
    'json.nd.file.reader' => new Registry\JsonFileUploadReader(
        name: 'JSON or NDJSON File Upload',
        allowedFileExtensions: ['json', 'ndjson'],
    ),    
]

'interfaces' => [
    Repo\ImportedFileRepositoryInterface::class =>
    static function(StoragesInterface $storages): Repo\ImportedFileRepositoryInterface {
        return new Repo\ImportedFileRepository(
            storage: $storages->get('uploads-private'),
            rootFolder: 'imports',
        );
    },
]

'registries' => [
    'pdf.file.storage.writer' => new Registry\PdfFileStorageWriter(
        // Set a name:
        name: 'PDF File',
        
        // File storage where processed files are stored:
        storageName: 'uploads-private', // default
        
        // Folder inside the storage where files are placed:
        storageFolder: 'exports', // default
    ),
]

'interfaces' => [
    Repo\ExportedFileRepositoryInterface::class =>
    static function(StoragesInterface $storages): Repo\ExportedFileRepositoryInterface {
        return new Repo\ExportedFileRepository(
            storage: $storages->get('uploads-private'),
            rootFolder: 'exports',
        );
    },
]

'registries' => [
    'pdf.file.storage.writer' => new Registry\PdfFileStorageWriter(
        name: 'PDF File',
    )
        // Optionally define reader‑specific templates:
        ->views([
            'my.custom.reader' => 'custom/pdf-template',
        ]),
]

'registries' => [
    'products.repository.reader' => new Registry\RepositoryReader(
        // Repository class to read from:
        repository: ProductReadRepository::class,

        // Optional default filtering:
        defaultWhere: ['status' => 'active'],

        // Optional default sorting:
        defaultOrderBy: ['created_at' => 'DESC'],

        // Optional entity-to-array converter:
        objectToArray: fn(Product $p) => [
            'id' => $p->id(),
            'sku' => $p->sku(),
            'title' => $p->title(),
        ],

        // Optional preview row count:
        previewRows: 5,

        // Display name:
        name: 'Active Products',
    ),
]

'registries' => [
    'products.repository.writer' => new Registry\RepositoryWriter(
        // Repository class to write to:
        repository: ProductWriteRepository::class,

        // Name of the identifier column:
        idName: 'id',

        // Columns that may be written:
        withFields: ['sku', 'title', 'price', 'status'],

        // Optional display name:
        name: 'Product Writer',
    ),
]

'registries' => [
    'products.storage.reader' => new Registry\StorageReader(
        // Storage service ID or class name:
        storage: 'storage.mysql',

        // Table to read from:
        table: 'products',

        // Optional query callable:
        query: function($query) {
            return $query->where('status', '=', 'active');
        },

        // Optional preview row count:
        previewRows: 5,

        // Optional display name:
        name: 'Active Products Table',
    ),
]

'registries' => [
    'products.storage.writer' => new Registry\StorageWriter(
        // Storage service ID or class name:
        storage: 'storage.mysql',

        // Table to write to:
        table: 'products',

        // Name of the identifier column:
        idName: 'id',

        // Columns that may be written:
        columns: ['sku', 'title', 'price', 'status'],

        // Optional display name:
        name: 'Products Storage Writer',
    ),
]

'registries' => [
    'xml.file.storage.writer' => new Registry\XmlFileStorageWriter(
        // Set a name:
        name: 'XML File',

        // File storage where processed files are stored:
        storageName: 'uploads-private', // default in AbstractFileStorageWriter

        // Folder inside the storage where files are placed:
        storageFolder: 'exports', // default in AbstractFileStorageWriter

        // Optionally restrict allowed file extensions (defaults to ['xml']):
        allowedFileExtensions: ['xml'],
    ),
]

'interfaces' => [
    Repo\ExportedFileRepositoryInterface::class =>
    static function(StoragesInterface $storages): Repo\ExportedFileRepositoryInterface {
        return new Repo\ExportedFileRepository(
            storage: $storages->get('uploads-private'),
            rootFolder: 'exports',
        );
    },
]

namespace Tobento\App\ImportExport\Queue;

use Tobento\App\ImportExport\Hook;
use Tobento\Service\Queue\JobHandlerInterface;

class TimeBudgetJobHandler implements JobHandlerInterface
{
    protected function mandatoryHooks(): array
    {
        return [
            new Hook\JobLifecycle(name: 'Job Lifecycle'),
        ];
    }
}

use Tobento\App\ImportExport\Hook;

class CustomizedTimeBudgetQueueHandler extends TimeBudgetJobHandler
{
    protected function mandatoryHooks(): array
    {
        return [
            new MyCustomJobLifecycleHook(name: 'Custom Job Lifecycle'),
        ];
    }
}

'interfaces' => [
    Queue\QueueHandlerInterface::class =>
    static function(QueueInterface $queue): Queue\QueueHandlerInterface {
        return new CustomizedTimeBudgetQueueHandler(
            queue: $queue,
            queueName: null,
            timeBudget: 20, // in seconds
        );
    },
],

use Tobento\Service\Notifier\Recipient;

'hooks' => [

    'notify.dev' => new Hook\Notify(
        // Set a name:
        name: 'Notify Developer via Mail and SMS',
        
        // Define the recipient:
        recipient: new Recipient(
            email: '[email protected]',
            phone: '15556666666',
            channels: ['mail', 'sms'],
        ),
        
        // Customize the notification subject (optional):
        notificationSubject: 'Job :name :event',
        
        // Events to notify on:
        notifyOn: ['started', 'partialProcess', 'completed', 'failed'],
        
        // Send the notification via queue (optional):
        queueName: 'file', // null by default
        
        // Should this hook be pre-selected in the UI?
        defaultSelected: false,
        
        // Optional grouping label used in the UI (default: 'Notify'):
        group: 'Notify',
    ),
]

'hooks' => [

    'notify.current-user' => new Hook\NotifyCurrentUser(
        // Set a name:
        name: 'Keep me updated about this job',
        
        // Define the channels to notify the current user:
        channels: ['browser'],
        
        // Customize the notification subject (optional):
        notificationSubject: 'Job :name :event',
        
        // Events to notify on (optional):
        notifyOn: ['started', 'partialProcess', 'completed', 'failed'],
        
        // Send the notification via queue (optional):
        queueName: 'file', // null by default
        
        // Should this hook be pre-selected in the UI?
        defaultSelected: true,
        
        // Optional grouping label used in the UI (default: 'Notify'):
        group: 'Notify',
    ),
]

'hooks' => [

    'notify.administrators' => new Hook\NotifyUsers(
        // Set a name:
        name: 'Notify Administrators via Account and Browser',
        
        // Define which user roles should be notified:
        roles: ['administrator'],
        
        // Define the channels to notify:
        channels: ['storage', 'browser'],
        
        // Customize the notification subject (optional):
        notificationSubject: 'Job :name :event',
        
        // Maximum number of users to notify:
        limit: 100,
        
        // Send the notification via queue (optional):
        queueName: 'file', // null by default
        
        // Events to notify on:
        notifyOn: ['started', 'partialProcess', 'completed', 'failed'],
        
        // Should this hook be pre-selected in the UI?
        defaultSelected: false,
        
        // Optional grouping label used in the UI (default: 'Notify'):
        group: 'Notify',
    ),
]

'hooks' => [
    'save.result.sussessful' => new Hook\SaveJobResult(
        // Set a name:
        name: 'Save Successful Rows',
        
        // Store rows that were processed successfully:
        mode: 'successful',
        
        // Optional grouping label used in the UI (default: 'Row'):
        group: 'Row',
    ),
    
    'save.result.skipped' => new Hook\SaveJobResult(
        name: 'Save Skipped Rows',
        mode: 'skipped',
    ),
    
    'save.result.failed' => new Hook\SaveJobResult(
        name: 'Save Failed Rows',
        mode: 'failed',
    ),
],

new ColumnModeModifier([
    'items' => 'split-dot',
    'gallery' => 'comma-separated',
    'attributes' => 'json',
]);

['color' => 'red', 'size' => 'L']

'{"color":"red","size":"L"}'

['red', 'green', 'blue']

'red, green, blue'

[
    'src' => [
        'en' => 'image-en.jpg',
        'de' => 'image-de.jpg',
    ],
]

[
    'image.src.en' => 'image-en.jpg',
    'image.src.de' => 'image-de.jpg',
]

[
    'src' => [
        'en' => 'image-en.jpg',
        'de' => 'image-de.jpg',
    ],
]

[
    'image_src_en' => 'image-en.jpg',
    'image_src_de' => 'image-de.jpg',
]

use Tobento\App\ImportExport\Modifier\ColumnModeModifier;
use Tobento\Service\ReadWrite\Modifier;

$modifiers = new Modifier\Modifiers(
    new ColumnModeModifier([
        'gallery' => 'comma-separated',
        'attributes' => 'json',
        'image' => 'split-dot',
    ]),
);

[
    'title' => 'Product A',
    'image.src.en' => 'products/en/image.jpg',
    'image.src.de' => 'products/de/image.jpg',
    'attributes.color' => 'red',
]

[
    'title' => 'Product A',
    'image' => [
        'src' => [
            'en' => 'products/en/image.jpg',
            'de' => 'products/de/image.jpg',
        ],
    ],
    'attributes' => [
        'color' => 'red',
    ],
]

use Tobento\App\ImportExport\Modifier\DotNotationToArrayModifier;
use Tobento\Service\ReadWrite\Modifier;

$modifiers = new Modifier\Modifiers(
    new DotNotationToArrayModifier(),
);

use Tobento\App\ImportExport\Modifier\FileExportModifier;

new FileExportModifier(
    outputMode: 'url', // 'raw', 'url', or 'base64'
    signedUrlsExpiresInMinutes: 60, // for private storage
    container: $container,
);

'path/image.jpg'

'https://example.com/media/uploads-public/products/image.jpg'

'https://example.com/media/signed/...&expires=1712345678'

'data:image/png;base64,iVBORw0KGgoAAA...'

[
    'src' => 'products/image.jpg',
    'storage' => 'uploads-public',
]

[
    'src' => [
        'en' => 'products/image-en.jpg',
        'de' => 'products/image-de.jpg',    
    ],
]

[
    ['src' => 'gallery/1.jpg', 'storage' => 'uploads-public'],
    ['src' => 'gallery/2.jpg', 'storage' => 'uploads-public'],
]

[
    [
        'src' => [
            'en' => 'gallery/en/1.jpg',
            'de' => 'gallery/de/1.jpg',
        ],
        'storage' => 'uploads-public',
    ],
    [
        'src' => [
            'en' => 'gallery/en/2.jpg',
            'de' => 'gallery/de/2.jpg',
        ],
        'storage' => 'uploads-public',
    ],
]

[
    [
        'src' => [
            'en' => 'https://example.com/.../gallery/en/1.jpg',
            'de' => 'https://example.com/.../gallery/de/1.jpg',
        ],
        'storage' => 'uploads-public',
    ],
    ...
]

'features' => [
    new Feature\FileDisplay(
        // define the supported storages:
        supportedStorages: ['uploads-public'],
    ),
],

['src' => 'image.jpg', 'storage' => 'uploads-public']

['src' => ['en' => 'image.jpg'], 'storage' => 'uploads-public']

[
    ['src' => 'image1.jpg', 'storage' => 'uploads-public'],
    ['src' => 'image2.jpg', 'storage' => 'uploads-public'],
]

[
    ['src' => ['en' => 'image.jpg'], 'storage' => 'uploads-public'],
    ['src' => ['en' => 'image.jpg'], 'storage' => 'uploads-public'],
]

'https://example.com/image.jpg'

[
    'en' => 'https://example.com/image-en.jpg',
    'de' => 'https://example.com/image-de.jpg',
]

$modifier = new ImageModifier(
    mode: 'raw',
    selectedLanguages: ['en'],
    languages: $languages,
    imgWidth: 200,
    imgHeight: 150,
    container: $container,
);

use Tobento\App\ImportExport\Modifier\UploadedFileModifier;

new UploadedFileModifier($this->withFileFields(), $container)

'https://example.com/image.jpg'

'data:image/png;base64,iVBORw0KGgoAAA...'

'iVBORw0KGgoAAAANSUhEUgAA...'

use Tobento\App\Crud\Action\ActionsInterface;
use Tobento\App\ImportExport\Crud\ImportBulkAction;

protected function configureActions(): iterable|ActionsInterface
{
    // other actions ...
    
    yield new ImportBulkAction(
        // Unique identifier for the bulk action (must be unique per CRUD resource)
        name: 'import',

        // The label shown in the bulk-action dropdown
        title: 'Import',

        // Fields available for mapping and writing during the import
        withFields: ['id', 'sku', 'title'],
        
        withFileFields: ['image'], // processed by UploadedFileModifier
    );
}

'image' => 'https://example.com/image.jpg'

'image' => [
    'src' => 'https://example.com/image.jpg'
]

'image' => [
    'src' => [
        'en' => 'https://example.com/en.jpg',
        'de' => 'https://example.com/de.jpg',
    ]
]

'gallery' => [
    ['src' => 'https://example.com/1.jpg'],
    ['src' => 'https://example.com/2.jpg'],
]

new Field\Files(name: 'files')
    ->translatable()
    ->file(function(Field\File $file): void {
        $file->translatable();
    });

'gallery' => [
    [
        'src' => [
            'en' => 'https://example.com/en.jpg',
            'de' => 'https://example.com/de.jpg',
        ],
    ],
]

'manual' => [
    'en' => 'https://example.com/manual-en.pdf',
    'de' => 'https://example.com/manual-de.pdf',
]

use Psr\Container\ContainerInterface;
use Tobento\App\Crud\Action\ActionsInterface;
use Tobento\App\ImportExport\Crud\ImportBulkAction;

protected function configureActions(): iterable|ActionsInterface
{
    // other actions ...
    
    yield new ImportBulkAction(
        name: 'import',
        title: 'Import',

        // Fields available for mapping and writing during the import
        withFields: ['id', 'sku', 'title', 'image'],

        // Fields that should be processed by UploadedFileModifier
        withFileFields: ['image'],
    );
}

public function createModifiers(ContainerInterface $container, JobEntityInterface $jobEntity): ModifiersInterface
{
    return new Modifier\Modifiers(
        // Maps incoming columns to the fields defined in withFields()
        new Modifier\ColumnMap(map: $jobEntity->columnMap()),

        // Converts dot-notation keys (e.g. "meta.image") into nested arrays
        new DotNotationToArrayModifier(),

        // Converts file values (URLs, base64, data URIs) into UploadedFile instances
        new UploadedFileModifier($this->withFileFields(), $container),
    );
}

use Psr\Container\ContainerInterface;
use Tobento\App\Crud\Action\ActionsInterface;
use Tobento\App\ImportExport\Crud\ExportBulkAction;
use Tobento\App\ImportExport\Repo\JobEntityInterface;
use Tobento\Service\ReadWrite\Modifier;
use Tobento\Service\ReadWrite\ModifiersInterface;
use Tobento\Service\ReadWrite\Reader\RepositoryReader;
use Tobento\Service\ReadWrite\RowInterface;

protected function configureActions(): iterable|ActionsInterface
{
    // other actions ...
    
    yield new ExportBulkAction(
        // Unique identifier for the bulk action (must be unique per CRUD resource)
        name: 'export',
        
        // The label shown in the bulk-action dropdown
        title: 'Export'
    )
        // Optional: apply modifiers to transform fields before export
        ->modifiers(static function (ContainerInterface $container, JobEntityInterface $jobEntity): ModifiersInterface {
            // You may conditionally adjust modifiers depending on the writer selected for the job
            // $writerId = $jobEntity->writerId(); // e.g. 'pdf.file.storage.writer'
            return new Modifier\Modifiers(
                // Example: transform the 'sku' field before writing
                new Modifier\Format(
                    field: 'sku',
                    formatter: function ($value, RowInterface $row) {
                        return strtoupper(trim((string)$value));
                    }
                ),
            );
        })
        
        // Optional: create custom reader
        ->reader(static function (ExportBulkAction $action, JobEntityInterface $jobEntity): RepositoryReader {
            return new RepositoryReader(
                repository: $action->controller()->repository(),
                where: ['type' => 'products'], // base filters
                objectToArray: static function (object $entity) use ($action): array {
                    return $action->controller()->createEntityFromObject($entity)->toArray();
                },
                previewRows: 3,
            );
        });
}

use Psr\Container\ContainerInterface;
use Tobento\App\Crud\Action\ActionsInterface;
use Tobento\App\Crud\Field;
use Tobento\App\Crud\Field\Fields;
use Tobento\App\Crud\Field\FieldsInterface;
use Tobento\App\ImportExport\Crud\ExportBulkAction;
use Tobento\App\ImportExport\Repo\JobEntityInterface;
use Tobento\Service\ReadWrite\Modifier;
use Tobento\Service\ReadWrite\ModifiersInterface;
use Tobento\Service\ReadWrite\RowInterface;

protected function configureActions(): iterable|ActionsInterface
{
    // other actions ...

    yield new ExportBulkAction(
        name: 'export',
        title: 'Export'
    )
        ->modifyFields(function (
            ActionInterface $action,
            FieldsInterface $fields,
            ExportBulkAction $export
        ): iterable|FieldsInterface {

            // Convert to array for modification
            $all = $fields->all();

            // Add a custom import mode selector
            $all[$export->fieldName('options.mode')] = new Field\Select(
                name: $export->fieldName('options.mode'),
                label: 'Export Mode',
            )
                ->options([
                    'basic' => 'Basic',
                    'advanced' => 'Advanced',
                    'custom' => 'Custom',
                ]);

            // Return new Fields instance
            return Fields::fromIterable($all);
        })

        // Apply modifiers depending on the mode:
        ->modifiers(static function (ContainerInterface $container, JobEntityInterface $jobEntity): ModifiersInterface {
            // Read the selected mode from job options
            $mode = $jobEntity->get('options.mode');
            
            // Apply conditional modifiers
            if ($mode === 'advanced') {
                return new Modifier\Modifiers(
                    new Modifier\Format(
                        field: 'sku',
                        formatter: function ($value, RowInterface $row) {
                            return strtoupper(trim((string)$value));
                        }
                    ),
                );
            }
            
            return new Modifier\Modifiers();
        })
}

use Psr\Container\ContainerInterface;
use Tobento\App\Crud\Action\ActionsInterface;
use Tobento\App\ImportExport\Crud\ImportBulkAction;
use Tobento\App\ImportExport\Modifier\DotNotationToArrayModifier;
use Tobento\App\ImportExport\Modifier\UploadedFileModifier;
use Tobento\App\ImportExport\Repo\JobEntityInterface;
use Tobento\Service\ReadWrite\Modifier;
use Tobento\Service\ReadWrite\ModifiersInterface;
use Tobento\Service\ReadWrite\RowInterface;

protected function configureActions(): iterable|ActionsInterface
{
    // other actions ...
    
    yield new ImportBulkAction(
        // Unique identifier for the bulk action (must be unique per CRUD resource)
        name: 'import',

        // The label shown in the bulk-action dropdown
        title: 'Import',

        // Fields available for mapping and writing during the import
        withFields: ['id', 'sku', 'title'],

        // Fields that should be processed by UploadedFileModifier
        withFileFields: ['image'],
    )
        // Optional: apply modifiers to transform fields before import
        ->modifiers(static function (ContainerInterface $container, JobEntityInterface $jobEntity): ModifiersInterface {
            // You may conditionally adjust modifiers depending on the reader selected for the job
            // $readerId = $jobEntity->readerId(); // e.g. 'csv.file.reader'
            return new Modifier\Modifiers(
                // Keep default modifiers
                new DotNotationToArrayModifier(),
                new Modifier\ColumnMap(map: $jobEntity->columnMap()),
                new UploadedFileModifier($jobEntity->withFileFields(), $container),
                
                // Example: transform the 'sku' field before writing
                new Modifier\Format(
                    field: 'sku',
                    formatter: function ($value, RowInterface $row) {
                        return strtoupper(trim((string)$value));
                    }
                ),
            );
        });
}

use Tobento\App\Crud\Action\ActionsInterface;
use Tobento\App\Crud\ActionProcessorInterface;
use Tobento\App\Crud\CrudWriteRepository;
use Tobento\App\ImportExport\Crud\ImportBulkAction;
use Tobento\App\ImportExport\Repo\JobEntityInterface;
use Tobento\Service\ReadWrite\Writer\RepositoryWriter;
use Tobento\Service\Repository\NullRepository;

protected function configureActions(): iterable|ActionsInterface
{
    // other actions ...
     
    yield new ImportBulkAction(
        name: 'import',
        title: 'Import',
        withFields: ['id', 'sku', 'title'],
    )
        // This example shows default implementation using CrudWriteRepository::class
        ->writer(static function (ImportBulkAction $action, JobEntityInterface $jobEntity): RepositoryWriter {
            $controller = $action->controller();
            $container = $action->container();
            $actionProcessor = $container->get(ActionProcessorInterface::class);
            
            // Dry run: replace the repository so no write operations are performed
            if ($jobEntity->get('options.dry_run') === '1') {
                $controller = $controller->withRepository(new NullRepository());
            }
        
            $repository = new CrudWriteRepository(
                controller: $controller,
                actionProcessor: $actionProcessor,
            )->onlyFields(...$action->withFields());

            return new RepositoryWriter(
                repository: $repository,
                idName: $controller->entityIdName(),
                columns: $action->withFields(),
            );
        })
        
        // Example without using CrudWriteRepository::class
        ->writer(static function (ImportBulkAction $action, JobEntityInterface $jobEntity): RepositoryWriter {
            $controller = $action->controller();

            return new RepositoryWriter(
                repository: $controller->repository(),
                idName: $controller->entityIdName(),
                columns: $action->withFields(),
            );
        });
}

use Psr\Container\ContainerInterface;
use Tobento\App\Crud\Action\ActionsInterface;
use Tobento\App\Crud\Field;
use Tobento\App\Crud\Field\Fields;
use Tobento\App\Crud\Field\FieldsInterface;
use Tobento\App\ImportExport\Crud\ImportBulkAction;
use Tobento\App\ImportExport\Repo\JobEntityInterface;
use Tobento\Service\ReadWrite\Modifier;
use Tobento\Service\ReadWrite\ModifiersInterface;
use Tobento\Service\ReadWrite\RowInterface;

protected function configureActions(): iterable|ActionsInterface
{
    // other actions ...

    yield new ImportBulkAction(
        name: 'import',
        title: 'Import',
        withFields: ['id', 'sku', 'title'],
    )
        ->modifyFields(function (
            ActionInterface $action,
            FieldsInterface $fields,
            ImportBulkAction $import,
            int $step,
            null|JobEntityInterface $jobEntity
        ): iterable|FieldsInterface {
            // Only modify fields on step 2 (mapping step)
            if ($step !== 2) {
                return $fields;
            }
            
            // Convert to array for modification
            $all = $fields->all();

            // Add a custom import mode selector
            $all[$import->fieldName('options.mode')] = new Field\Select(
                name: $import->fieldName('options.mode'),
                label: 'Import Mode',
            )
                ->options([
                    'basic' => 'Basic',
                    'advanced' => 'Advanced',
                    'custom' => 'Custom',
                ]);

            // Return new Fields instance
            return Fields::fromIterable($all);
        })

        // Apply modifiers depending on the mode:
        ->modifiers(static function (ContainerInterface $container, JobEntityInterface $jobEntity): ModifiersInterface {
            // Read the selected mode from job options
            $mode = $jobEntity->get('options.mode');
            
            // Apply conditional modifiers
            if ($mode === 'advanced') {
                return new Modifier\Modifiers(
                    new Modifier\Format(
                        field: 'sku',
                        formatter: function ($value, RowInterface $row) {
                            return strtoupper(trim((string)$value));
                        }
                    ),
                );            
            }
            
            return new Modifier\Modifiers();
        })
}

use Tobento\App\Crud\AbstractCrudController;
use Tobento\App\Crud\Action\ActionsInterface;
use Tobento\App\ImportExport\Crud\ImportBulkAction;
use Tobento\Service\Acl\AclInterface;

class ProductController extends AbstractCrudController
{
    public const RESOURCE_NAME = 'products';
    
    /**
     * Create a new ProductController.
     *
     * @param RepositoryInterface $repository
     */
    public function __construct(
        ProductRepository $repository,
        protected AclInterface $acl,
    ) {
        $this->repository = $repository;
    }

    protected function configureActions(): iterable|ActionsInterface
    {
        // Other actions...

        // Only register the import bulk action if the user is allowed to run imports
        if ($this->acl->can('import-export.run')) {
            yield new ImportBulkAction(
                name: 'import',
                title: 'Import',
                withFields: ['id', 'sku', 'title'],
            );
        }
    }
}

use Tobento\App\Task\RegistriesInterface;
use Tobento\App\Task\Registry\CommandTask;

$app->on(
    RegistriesInterface::class,
    static function(RegistriesInterface $registries): void {
        $registries->add(id: 'prune.auth.tokens', registry: new CommandTask(
            name: 'Prune Auth Tokens',
            command: 'auth:purge-tokens',
        ));
    }
);

use Tobento\App\Task\HooksInterface;
use Tobento\App\Task\Hook\Mail;

$app->on(
    HooksInterface::class,
    static function(HooksInterface $hooks): void {
        $hooks->add(id: 'mail.dev', registry: new Mail(
            name: 'Mail Developer',
            email: '[email protected]',
        ));
    }
);