b13/db-file-storage

Store uploaded files directly in the TYPO3 database. A tiny, injectable service usable from Extbase and any PSR-15 / ActionController.

Maintainers

Package info

github.com/b13/db_file_storage

Homepage

Type:typo3-cms-extension

pkg:composer/b13/db-file-storage

Statistics

Installs: 3

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

dev-main 2026-04-09 11:32 UTC

This package is auto-updated.

Last update: 2026-04-09 12:09:10 UTC


README

Store uploaded files directly in the TYPO3 database.

A tiny, injectable service that persists files into a single DB table (with a LONGBLOB content column) and hands them back out as PSR-7 responses. Works from Extbase controllers, PSR-15 middlewares, CLI commands, or anything the DI container builds.

  • TYPO3: v13 LTS / v14
  • PHP: 8.2+
  • Composer: b13/db-file-storage

Installation

composer require b13/db-file-storage
vendor/bin/typo3 extension:setup

API

Service — DatabaseFileStorage

The main entry point. Inject via constructor:

public function store(UploadedFileInterface $file): StoredFile;
public function storeContents(string $filename, string $contents, ?string $mimeType = null): StoredFile;
public function get(int $uid): ?StoredFile;
public function require(int $uid): StoredFile; // throws FileNotFoundException
public function delete(int $uid): bool; // soft-delete (sets deleted=1)
public function createResponse(int $uid, bool $forceDownload = false): ResponseInterface;

StoredFile is an immutable value object (uid, filename, mimeType, size, sha1, contents, createdAt). Every method after store*() takes the uid returned by it.

Extbase entity — StoredFileReference

A metadata-only Extbase entity for the same table, suitable for ObjectStorage<StoredFileReference> properties and MM relations. Does not map the content LONGBLOB — bytes stay behind DatabaseFileStorage::get().

Ships with a repository (StoredFileReferenceRepository), a minimal hideTable / readOnly TCA stub, and a Persistence/Classes.php mapping that's auto-merged into every consumer extension.

Example: Extbase controller

use B13\DbFileStorage\Service\DatabaseFileStorage;
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;

final class InvoiceController extends ActionController
{
    public function __construct(
        private readonly DatabaseFileStorage $databaseFileStorage,
    ) {}

    public function uploadAction(): ResponseInterface
    {
        $uploadedFile = $this->request->getUploadedFiles()['invoice'] ?? null;
        if ($uploadedFile === null) {
            return (new \TYPO3\CMS\Core\Http\Response())->withStatus(400);
        }

        $stored = $this->databaseFileStorage->store($uploadedFile);

        return $this->redirect('show', null, null, ['file' => $stored->uid]);
    }

    public function downloadAction(int $file): ResponseInterface
    {
        return $this->databaseFileStorage->createResponse($file, forceDownload: true);
    }
}

Example: storing generated bytes

$stored = $this->databaseFileStorage->storeContents(
    filename: 'invoice-' . $invoice->getNumber() . '.pdf',
    contents: $pdfBytes,
    mimeType: 'application/pdf',
);
$invoice->setFileUid($stored->uid);

Example: MM relation to a domain model

Add a group + MM column in your consumer TCA:

'attachments' => [
    'label' => 'Attachments',
    'config' => [
        'type' => 'group',
        'relationship' => 'manyToMany',
        'allowed' => 'tx_dbfilestorage_domain_model_file',
        'foreign_table' => 'tx_dbfilestorage_domain_model_file',
        'MM' => 'tx_myext_task_file_mm',
    ],
],

The MM table is auto-created by TYPO3's schema analyzer. On the Extbase side, use StoredFileReference in your model:

use B13\DbFileStorage\Domain\Model\StoredFileReference;
use B13\DbFileStorage\Domain\Repository\StoredFileReferenceRepository;

// In your model:
/** @param ObjectStorage<StoredFileReference> $attachments */
protected ObjectStorage $attachments;

// In your upload action:
$stored = $this->databaseFileStorage->store($uploadedFile);
$ref = $this->storedFileReferenceRepository->findByUid($stored->uid);
$task->addAttachment($ref);
$this->taskRepository->update($task);

Extbase writes the MM rows automatically on persistAll().

Note on cascade delete: Extbase's #[Cascade('remove')] only works for 1:n relations (HAS_MANY), not for M:N (HAS_AND_BELONGS_TO_MANY). To soft-delete the file row when removing a relation, call $databaseFileStorage->delete($ref->getUid()) explicitly in your controller.

Schema

One table — ext_tables.sql:

Column Type Notes
uid int auto_increment Primary key
deleted tinyint Soft-delete flag
filename varchar(255) Original client filename
mime_type varchar(127) Detected via finfo / TYPO3 map
size bigint Size in bytes
sha1 varchar(40) Content hash, indexed
content longblob The file bytes
crdate int Creation timestamp

MariaDB/MySQL's max_allowed_packet governs the maximum insert size.

Development

ddev start          # installs TYPO3, creates tables, ready to use
  • Backend: https://db-file-storage.ddev.site/typo3admin / Password.1
  • Tests:
    ddev exec --dir /var/www/html/Build \
      typo3DatabaseDriver=pdo_sqlite \
      vendor/bin/phpunit -c ../Build/phpunit/FunctionalTests.xml

Credits

This extension was created by Jochen Roth in 2025 for b13 GmbH, Stuttgart.