File upload

Implementing file uploads / attachments to Extbase domain models has always been a bit of a challenge.

While it is straight-forward to access an existing file reference in a domain model, writing new files to the FAL (File Access Layer) takes more effort.

Accessing a file reference in an Extbase domain model

You need two components for the structural information: the Domain Model definition and the TCA entry.

The domain model definition:

EXT:my_extension/Classes/Domain/Model/Blog.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 Blog extends AbstractEntity
{
    // A single file
    protected ?FileReference $singleFile = null;

    /**
     * A collection of files.
     * @var ObjectStorage<FileReference>
     */
    protected ObjectStorage $multipleFiles;

    // When using ObjectStorages, it is vital to initialize these.
    public function __construct()
    {
        $this->multipleFiles = new ObjectStorage();
    }

    /**
     * Called again with initialize object, as fetching an entity from the DB does not use the constructor
     */
    public function initializeObject(): void
    {
        $this->multipleFiles = $this->multipleFiles ?? new ObjectStorage();
    }

    // Typical getters
    public function getSingleFile(): ?FileReference
    {
        return $this->singleFile;
    }

    /**
     * @return ObjectStorage|FileReference[]
     */
    public function getMultipleFiles(): ObjectStorage
    {
        return $this->multipleFiles;
    }

    // For later examples, the setters:
    public function setSingleFile(?FileReference $singleFile): void
    {
        $this->singleFile = $singleFile;
    }

    public function setMultipleFiles(ObjectStorage $files): void
    {
        $this->multipleFiles = $files;
    }
}
Copied!

and the TCA definition:

EXT:my_extension/Configuration/TCA/tx_myextension_domain_model_blog.php
<?php

return [
    'ctrl' => [
        // .. usual TCA fields
    ],
    'columns' => [
        // ... usual TCA columns
        'single_file' => [
            'exclude' => true,
            'label' => 'Single file',
            'config' => [
                'type' => 'file',
                'maxitems' => 1,
                'allowed' => 'common-image-types',
            ],
        ],
        'multiple_files' => [
            'exclude' => true,
            'label' => 'Multiple files',
            'config' => [
                'type' => 'file',
                'allowed' => 'common-image-types',
            ],
        ],
    ],
];
Copied!

Once this is set up, you can create/edit records through the TYPO3 backend (for example via Web > List), attach a single or multiple files in it. Then using a normal controller and Fluid template, you can display an image.

The relevant Extbase controller part:

EXT:my_extension/Classes/Controller/BlogController.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Controller;

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

class BlogController extends ActionController
{
    public function __construct(protected readonly BlogRepository $blogRepository)
    {
        // Note: The repository is a standard extbase repository, nothing specific
        //       to this example.
    }

    public function showAction(Blog $blog): ResponseInterface
    {
        $this->view->assign('blog', $blog);

        return $this->htmlResponse();
    }
}
Copied!

and the corresponding Fluid template:

EXT:my_extension/Resources/Private/Templates/Blog/Show.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">
    <p>Single image:</p>
    <f:image image="{blog.singleFile.originalFile}" />

    <p>Multiple images:</p>
    <f:for each="{blog.multipleFiles}" as="image">
        <f:image image="{image.originalFile}" />
    </f:for>

    <p>Access first image of multiple images:</p>
    <f:image image="{blog.multipleFiles[0].originalFile}" />
</f:section>
Copied!

On the PHP side within controllers, you can use the usual $blogItem->getSingleFile() and $blogItem->getMultipleFiles() Extbase getters to retrieve the FileReference object.

Writing FileReference entries

Manual handling

With TYPO3 versions 12.4 and below, attaching files to an Extbase domain model is possible by either:

  • Manually evaluating the $_FILES data, process and validate the data, use raw QueryBuilder write actions on sys_file and sys_file_reference to persist the files quickly, or use at least some API methods:

    EXT:my_extension/Classes/Controller/BlogController.php, excerpt
    <?php
    
    declare(strict_types=1);
    
    namespace MyVendor\MyExtension\Controller;
    
    use MyVendor\MyExtension\Domain\Model\Blog;
    use MyVendor\MyExtension\Domain\Repository\BlogRepository;
    use TYPO3\CMS\Core\Resource\DuplicationBehavior;
    use TYPO3\CMS\Core\Resource\ResourceFactory;
    use TYPO3\CMS\Core\Utility\GeneralUtility;
    use TYPO3\CMS\Core\Utility\StringUtility;
    use TYPO3\CMS\Extbase\Domain\Model\FileReference;
    use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
    
    class BlogController extends ActionController
    {
        public function __construct(
            protected ResourceFactory $resourceFactory,
            protected BlogRepository $blogRepository,
        ) {}
    
        public function attachFileUpload(Blog $blog): void
        {
            $falIdentifier = '1:/your_storage';
            $yourFile = '/path/to/uploaded/file.jpg';
    
            // Attach the file to the wanted storage
            $falFolder = $this->resourceFactory->retrieveFileOrFolderObject($falIdentifier);
            $fileObject = $falFolder->addFile(
                $yourFile,
                basename($yourFile),
                DuplicationBehavior::REPLACE,
            );
    
            // Initialize a new storage object
            $newObject = [
                'uid_local' => $fileObject->getUid(),
                'uid_foreign' => StringUtility::getUniqueId('NEW'),
                'uid' => StringUtility::getUniqueId('NEW'),
                'crop' => null,
            ];
    
            // Create the FileReference Object
            $fileReference = $this->resourceFactory->createFileReferenceObject($newObject);
    
            // Port the FileReference Object to an Extbase FileReference
            $fileReferenceObject = GeneralUtility::makeInstance(FileReference::class);
            $fileReferenceObject->setOriginalResource($fileReference);
    
            // Persist the created file reference object to our Blog model
            $blog->setSingleFile($fileReferenceObject);
            $this->blogRepository->update($blog);
    
            // Note: For multiple files, a wrapping ObjectStorage would be needed
        }
    }
    
    Copied!

    Instead of raw access to $_FILES, starting with TYPO3 v12 the recommendation is to utilize the UploadedFile objects instead of $_FILES. In that case, validators can be used for custom UploadedFile objects to specify restrictions on file types, file sizes and image dimensions.

  • Using (or better: adapting) a more complex implementation by using Extbase TypeConverters, as provided by Helmut Hummel's EXT:upload_example. This extension is no longer maintained and will not work without larger adaptation for TYPO3 v12 compatibility.