b13 / db-file-storage
Store uploaded files directly in the TYPO3 database. A tiny, injectable service usable from Extbase and any PSR-15 / ActionController.
Package info
github.com/b13/db_file_storage
Type:typo3-cms-extension
pkg:composer/b13/db-file-storage
Requires
- php: ^8.2
- psr/http-factory: ^1.0
- psr/http-message: ^1.1 || ^2.0
- typo3/cms-core: ^13.4 || ^14.2
- typo3/cms-extbase: ^13.4 || ^14.2
Requires (Dev)
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^11.0
- typo3/coding-standards: ^0.8.0
- typo3/testing-framework: ^9.0
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/typo3 —
admin/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.