---
title: "File uploads in Extbase domain models"
manual: "TYPO3 Explained"
version: "main"
permalink: "https://docs.typo3.org/permalink/t3coreapi:extbase-fileupload@main"
source: "ExtensionArchitecture/Extbase/Domain/FileUpload.rst"
rendered: "2026-09-23T17:05:17+00:00"
---

# File uploads in Extbase domain models {#extbase-fileupload}

Extbase can handle file uploads as part of its normal property
mapping and persistence flow by using the
[FAL (File Abstraction Layer)](https://docs.typo3.org/permalink/t3coreapi:using-fal@main) for storage.
Two distinct topics are covered here: reading existing
`\TYPO3\CMS\Core\Resource\FileReference` properties from a domain
model, and writing newly uploaded files using the `#[FileUpload]`
attribute.

**On this page**

-   [Reading a file reference from a domain model](https://docs.typo3.org/permalink/t3coreapi:reading-a-file-reference-from-a-domain-model@main)
-   [Writing uploaded files with #[FileUpload]](https://docs.typo3.org/permalink/t3coreapi:writing-uploaded-files-with-fileupload@main)
-   [Configuring the #[FileUpload] attribute](https://docs.typo3.org/permalink/t3coreapi:configuring-the-fileupload-attribute@main)
-   [File upload validation](https://docs.typo3.org/permalink/t3coreapi:file-upload-validation@main)
-   [Manual file upload configuration](https://docs.typo3.org/permalink/t3coreapi:manual-file-upload-configuration@main)
-   [Deleting uploaded files](https://docs.typo3.org/permalink/t3coreapi:deleting-uploaded-files@main)
-   [Modifying the target filename before persistence](https://docs.typo3.org/permalink/t3coreapi:modifying-the-target-filename-before-persistence@main)
-   [File uploads in multi-step forms](https://docs.typo3.org/permalink/t3coreapi:file-uploads-in-multi-step-forms@main)

## Reading a file reference from a domain model {#extbase-fileupload-accessing}

Reading an existing file reference requires two components: the model property
and the corresponding TCA column definition.

The domain model:

**EXT:my_extension/Classes/Domain/Model/Conference.php**

```php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Model;

use TYPO3\CMS\Extbase\Domain\Model\FileReference;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;

class Conference extends AbstractEntity
{
  protected ?FileReference $logo = null;

  /**
   * @var ObjectStorage<FileReference>
   */
  protected ObjectStorage $impressions;

  public function __construct()
  {
    $this->impressions = new ObjectStorage();
  }

  public function initializeObject(): void
  {
    $this->impressions = $this->impressions ?? new ObjectStorage();
  }

  public function getLogo(): ?FileReference
  {
    return $this->logo;
  }

  public function setLogo(?FileReference $logo): void
  {
    $this->logo = $logo;
  }

  /**
   * @return ObjectStorage<FileReference>
   */
  public function getImpressions(): ObjectStorage
  {
    return $this->impressions;
  }

  public function setImpressions(ObjectStorage $impressions): void
  {
    $this->impressions = $impressions;
  }
}

```

The TCA definition:

**EXT:my_extension/Configuration/TCA/tx_myextension_domain_model_conference.php**

```php
<?php

return [
  'ctrl' => [
    // ... table-specific ctrl fields
  ],
  'columns' => [
    // ... table-specific columns
    'logo' => [
      'exclude' => true,
      'label' => 'Logo',
      'config' => [
        'type' => 'file',
        'maxitems' => 1,
        'allowed' => 'common-image-types',
      ],
    ],
    'impressions' => [
      'exclude' => true,
      'label' => 'Impressions',
      'config' => [
        'type' => 'file',
        'allowed' => 'common-image-types',
      ],
    ],
  ],
];

```

Once a record has files attached via the TYPO3 backend, the controller
retrieves them through the standard Extbase getters and the Fluid template
renders them:

**EXT:my_extension/Resources/Private/Templates/Conference/Show.fluid.html**

```html
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
  data-namespace-typo3-fluid="true">

<f:layout name="Default" />

<f:section name="main">
  <f:if condition="{conference.logo}">
    <f:image image="{conference.logo}" cropVariant="default" alt="{conference.title}" />
  </f:if>

  <f:for each="{conference.impressions}" as="impression">
    <f:image image="{impression}" cropVariant="default" alt="{conference.title}" />
  </f:for>
</f:section>

</html>

```

## Writing uploaded files with `#[FileUpload]` {#extbase-fileupload-writing}

The `\TYPO3\CMS\Extbase\Attribute\FileUpload` attribute on a model
property is all that is needed for upload processing. Extbase validates
the upload, moves the file into the configured
FAL storage folder, and creates the
`\TYPO3\CMS\Core\Resource\FileReference` record. This is all
carried out automatically after property mapping succeeds.

Here is a domain model with `#[FileUpload]` on both properties:

**EXT:my_extension/Classes/Domain/Model/Conference.php**

```php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Domain\Model;

use TYPO3\CMS\Extbase\Attribute\FileUpload;
use TYPO3\CMS\Extbase\Domain\Model\FileReference;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;

class Conference extends AbstractEntity
{
  #[FileUpload(
    validation: [
      'required' => false,
      'maxFiles' => 1,
      'fileSize' => ['minimum' => '10K', 'maximum' => '2M'],
      'mimeType' => ['allowedMimeTypes' => ['image/jpeg', 'image/png']],
      'fileExtension' => ['allowedFileExtensions' => ['jpg', 'jpeg', 'png']],
    ],
    uploadFolder: '1:/user_upload/conference_logos/',
  )]
  protected ?FileReference $logo = null;

  /**
   * @var ObjectStorage<FileReference>
   */
  #[FileUpload(
    validation: [
      'fileSize' => ['minimum' => '10K', 'maximum' => '10M'],
      'mimeType' => ['allowedMimeTypes' => ['image/jpeg', 'image/png']],
      'fileExtension' => ['allowedFileExtensions' => ['jpg', 'jpeg', 'png']],
    ],
    uploadFolder: '1:/user_upload/conference_impressions/',
  )]
  protected ObjectStorage $impressions;

  public function __construct()
  {
    $this->impressions = new ObjectStorage();
  }

  public function initializeObject(): void
  {
    $this->impressions = $this->impressions ?? new ObjectStorage();
  }

  public function getLogo(): ?FileReference
  {
    return $this->logo;
  }

  public function setLogo(?FileReference $logo): void
  {
    $this->logo = $logo;
  }

  /**
   * @return ObjectStorage<FileReference>
   */
  public function getImpressions(): ObjectStorage
  {
    return $this->impressions;
  }

  public function setImpressions(ObjectStorage $impressions): void
  {
    $this->impressions = $impressions;
  }
}

```

The TCA definition is identical to the read-only case above. No special TCA
configuration is required for upload handling.

The upload form must declare `enctype="multipart/form-data"` and use
[f:form.upload](https://docs.typo3.org/other/typo3/view-helper-reference/main/en-us/Global/Form/Upload.html#typo3-fluid-form-upload) for each file
property:

**EXT:my_extension/Resources/Private/Templates/Conference/New.fluid.html**

```html
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
  data-namespace-typo3-fluid="true">

<f:layout name="Default" />

<f:section name="main">
  <f:form action="create" name="conference" object="{conference}" enctype="multipart/form-data">
    <div>
      <label>Logo</label>
      <f:form.upload property="logo" />
    </div>

    <div>
      <label>Impressions</label>
      <f:form.upload property="impressions" multiple="1" />
    </div>

    <f:form.submit value="Save" />
  </f:form>
</f:section>

</html>

```

The controller actions do not require any special upload code. Extbase handles
everything that happens between property mapping and persistence. The only requirement is
that the model is persisted within the same request:

**EXT:my_extension/Classes/Controller/ConferenceController.php**

```php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use MyVendor\MyExtension\Domain\Model\Conference;
use MyVendor\MyExtension\Domain\Repository\ConferenceRepository;
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;

class ConferenceController extends ActionController
{
  public function __construct(
    protected readonly ConferenceRepository $conferenceRepository,
  ) {}

  public function newAction(): ResponseInterface
  {
    $this->view->assign('conference', new Conference());
    return $this->htmlResponse();
  }

  public function createAction(Conference $conference): ResponseInterface
  {
    $this->conferenceRepository->add($conference);
    return $this->redirect('list');
  }

  public function editAction(Conference $conference): ResponseInterface
  {
    $this->view->assign('conference', $conference);
    return $this->htmlResponse();
  }

  public function updateAction(Conference $conference): ResponseInterface
  {
    $this->conferenceRepository->update($conference);
    return $this->redirect('list');
  }
}

```

> [!IMPORTANT]
> The model must be persisted in the same request that processes
> the upload (via `$conferenceRepository->add()` or
> `$conferenceRepository->update()`). Without persistence, dangling `sys_file` records are
> created without a corresponding `FileReference`, leaving stale
> temporary files that require manual cleanup.

Upload processing runs after property mapping completes. If a model property
fails validation, the file is not uploaded and the user must re-upload on the
next attempt. This avoids stale temporary files.

> [!NOTE]
> File upload handling for nested domain models — for example
> `conference.speaker.photo` — is not supported. The
> `#[FileUpload]` attribute must be placed on model properties
> of the direct argument of the action.

## Configuring the `#[FileUpload]` attribute {#extbase-fileupload-writing-attributes}

The `#[FileUpload]` attribute accepts named arguments as follows:

-   **`validation`**

    Array configuring built-in file upload validators. See
    [File upload validation](https://docs.typo3.org/permalink/t3coreapi:extbase-domain-fileupload-validation@main) below.

-   **`uploadFolder`**

    Destination as a FAL storage path, for example
    `'1:/user_upload/conference_logos/'`. The folder is created
    automatically unless `createUploadFolderIfNotExist` is set to
    `false`.

-   **`createUploadFolderIfNotExist`**

    `bool`, default `true`.

-   **`addRandomSuffix`**

    `bool`, default `true`. Appends a random 16-character hash to
    the persisted filename to prevent enumeration. Recommended to keep enabled.

-   **`duplicationBehavior`**

    FAL behavior when a file with the same name already exists in the target
    folder:
    `\TYPO3\CMS\Core\Resource\Enum\DuplicationBehavior::RENAME`
    (default),
    `\TYPO3\CMS\Core\Resource\Enum\DuplicationBehavior::REPLACE`,
    or
    `\TYPO3\CMS\Core\Resource\Enum\DuplicationBehavior::CANCEL`.

## File upload validation {#extbase-fileupload-attribute-validationkeys-shorthand-allowedmimetypes}

Two validators are enforced automatically for every `#[FileUpload]`
property and cannot be removed:

-   [FileName validator](https://docs.typo3.org/permalink/t3coreapi:extbase-validation-builtin-filename@main) — rejects
    files with names
    matching dangerous executable extensions such as `.php` or
    `.phar`. This is a hard security boundary. There is no legitimate
    reason to bypass it.
-   [FileExtensionMimeTypeConsistency validator](https://docs.typo3.org/permalink/t3coreapi:extbase-validation-builtin-fileextensionmimetypeconsistency@main)
    —
    cross-checks that the file extension and the detected MIME type are
    consistent, guarding against disguised uploads such as a PHP script renamed
    to `image.jpg`.

Beyond these two validators, the `validation` array configures additional validators
via shorthand keys. The most important ones for a public upload
form are:

**fileSize** — always set a meaningful lower and upper bound. A minimum of
`10K` rejects empty or near-empty files that would otherwise pass
silently. An upper bound prevents storage exhaustion:

**fileSize example**

```php
'fileSize' => ['minimum' => '10K', 'maximum' => '2M'],
```

**mimeType** — restricts accepted content types by inspecting the file
content as well as the file name. Always combine with `fileExtension` to cover
both the client-supplied extension and the server-detected type:

**mimeType example**

```php
'mimeType' => ['allowedMimeTypes' => ['image/jpeg', 'image/png']],
```

**fileExtension** — validates the file extension as supplied by the client.
The `allowedFileExtensions` list should match `allowedMimeTypes` exactly:

**fileExtension example**

```php
'fileExtension' => ['allowedFileExtensions' => ['jpg', 'jpeg', 'png']],
```

**imageDimensions** — for image uploads, caps width and height to prevent
oversized images from reaching the server's image processing pipeline:

**imageDimensions example**

```php
'imageDimensions' => ['maxWidth' => 4096, 'maxHeight' => 4096],
```

The remaining keys — `required`, `minFiles`, `maxFiles` — control
upload count. For the full option reference see
[File upload validators](https://docs.typo3.org/permalink/t3coreapi:extbase-validation-builtin-file@main).

> [!WARNING]
> Keep the allowed MIME types and file extensions configured in
> `#[FileUpload]` in sync with the [TCA type=file 'allowed' key](https://docs.typo3.org/m/typo3/reference-tca/main/en-us/ColumnsConfig/Type/File/Index.html#columns-file) on the same column. If they diverge, a file accepted
> by the frontend form may be rejected by the TYPO3 backend, or a file
> uploaded through the backend may not match the frontend validation rules.
> Either mismatch leads to confusing behavior that is hard to debug. Define
> the allowed types once, in both places.
>
> Also set the same list on `<f:form.upload accept="...">` so the
> browser's file picker pre-filters the selectable types for a better user
> experience — though this is a client-side hint only and not a security
> control.

## Manual file upload configuration {#extbase-fileupload-writing-manual}

When `#[FileUpload]` is not flexible enough — for example if you want to add a custom
validator class — create a
`\TYPO3\CMS\Extbase\Mvc\Controller\FileUploadConfiguration` object
manually in an `initialize*Action()`:

**EXT:my_extension/Classes/Controller/ConferenceController.php (excerpt)**

```php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

use MyVendor\MyExtension\Domain\Repository\ConferenceRepository;
use MyVendor\MyExtension\Validation\Validator\LogoDimensionsValidator;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
use TYPO3\CMS\Extbase\Mvc\Controller\FileUploadConfiguration;

class ConferenceController extends ActionController
{
  public function __construct(
    protected readonly ConferenceRepository $conferenceRepository,
  ) {}

  public function initializeCreateAction(): void
  {
    $logoValidator = GeneralUtility::makeInstance(LogoDimensionsValidator::class);

    $fileHandlingServiceConfiguration = $this->arguments
        ->getArgument('conference')
        ->getFileHandlingServiceConfiguration();

    $fileHandlingServiceConfiguration->addFileUploadConfiguration(
      (new FileUploadConfiguration('logo'))
            ->setMaxFiles(1)
            ->addValidator($logoValidator)
            ->setUploadFolder('1:/user_upload/conference_logos/'),
    );

    $this->arguments->getArgument('conference')
        ->getPropertyMappingConfiguration()
        ->skipProperties('logo');
  }
}

```

Note the `skipProperties()` call: Extbase's property mapping must not
operate on the file property when manual upload configuration is used.
The `#[FileUpload]` attribute handles this internally; with manual
configuration it must be done explicitly.

To modify configuration that is already defined by `#[FileUpload]` — for example
to read the upload folder out of the TypoScript settings — retrieve the existing
configuration object instead of creating a new one:

**EXT:my_extension/Classes/Controller/ConferenceController.php**

```php
public function initializeCreateAction(): void
{
    $argument = $this->arguments->getArgument('conference');
    $configuration = $argument
        ->getFileHandlingServiceConfiguration()
        ->getFileUploadConfigurationForProperty('logo');
    $configuration?->setUploadFolder(
        $this->settings['logoUploadFolder'] ?? '1:/user_upload/conference_logos/',
    );
}
```

To strip all application-level validators and start from a clean slate, call
`$configuration->resetValidators()`. The two mandatory validators
([FileName validator](https://docs.typo3.org/permalink/t3coreapi:extbase-validation-builtin-filename@main) and
[FileExtensionMimeTypeConsistency validator](https://docs.typo3.org/permalink/t3coreapi:extbase-validation-builtin-fileextensionmimetypeconsistency@main))
are always re-added and cannot be removed. Calling `resetValidators()` on a
public-facing upload form without immediately adding back type and size
restrictions will leave the upload open to any file content. Do this only when
you have a deliberate, application-specific reason and compensate with other
controls.

## Deleting uploaded files {#extbase-fileupload-attribute-deletion}

The
[f:form.uploadDeleteCheckbox](https://docs.typo3.org/other/typo3/view-helper-reference/main/en-us/Global/Form/UploadDeleteCheckbox.html#typo3-fluid-form-uploaddeletecheckbox)
ViewHelper renders a **Delete file** checkbox. When submitted, Extbase
deletes the `sys_file_reference` record and the underlying file before
persisting the updated model:

**EXT:my_extension/Resources/Private/Templates/Conference/Edit.fluid.html**

```html
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
  data-namespace-typo3-fluid="true">

<f:layout name="Default" />

<f:section name="main">
  <f:form action="update" name="conference" object="{conference}" enctype="multipart/form-data">
    <div>
      <label>Logo</label>
      <f:if condition="{conference.logo}">
        <f:form.uploadDeleteCheckbox
          id="deleteLogo"
          property="logo"
          fileReference="{conference.logo}" />
        <label for="deleteLogo">Delete logo</label>
      </f:if>
      <f:form.upload property="logo" />
    </div>

    <div>
      <label>Impressions</label>
      <f:for each="{conference.impressions}" as="impression" iteration="i">
        <f:form.uploadDeleteCheckbox
          property="impressions.{i.index}"
          fileReference="{impression}" />
        <label>Delete impression</label>
      </f:for>
      <f:form.upload property="impressions" multiple="1" />
    </div>

    <f:form.submit value="Save" />
  </f:form>
</f:section>

</html>

```

> [!WARNING]
> Extbase deletes the file from
> FAL **without checking whether any other
> record references the same file**. This is different from the TYPO3 backend
> which prevents the deletion of files that still have consumers. If the same
> `\TYPO3\CMS\Core\Resource\FileReference` is referenced by
> multiple domain objects, deleting it via this mechanism will break the
> other references silently. Ensure files are not shared before enabling
> deletion.

## Modifying the target filename before persistence {#extbase-fileupload-attribute-psr-event}

`\TYPO3\CMS\Extbase\Event\Service\ModifyUploadedFileTargetFilenameEvent`
allows event listeners to change the filename of an uploaded file before it is
written to FAL. Use `getTargetFilename()`
to read the current name and `setTargetFilename()` to rename it. The
active `FileUploadConfiguration` is available via
`getConfiguration()`.

## File uploads in multi-step forms {#extbase-fileupload-multistep}

Extbase file upload handling is coupled to the persistence step of a single
action. If a multi-step form must carry file state across requests before final
persistence, two patterns will work:

-   **DTO approach** — persist the uploaded file as a standalone domain object
    (or a dedicated DTO with its own `FileReference`) in the first step.
    Pass its `uid` as a hidden field through subsequent steps. In the
    final step, attach the `FileReference` to the domain model.
-   **Client-side preview** — use the JavaScript `FileReader()` API to
    render a preview of the selected file in the browser before the form is
    submitted, without a server round-trip.

> [!NOTE]
> **See also**
>
> -   [File upload validators](https://docs.typo3.org/permalink/t3coreapi:extbase-validation-builtin-file@main) — full
>     reference for all
>     file upload validators and their options.
> -   [Using FAL](https://docs.typo3.org/permalink/t3coreapi:using-fal@main) — the File Abstraction Layer in
>     TYPO3.
