PHPackages                             chamber-orchestra/file-bundle - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Database &amp; ORM](/categories/database)
4. /
5. chamber-orchestra/file-bundle

ActiveSymfony-bundle[Database &amp; ORM](/categories/database)

chamber-orchestra/file-bundle
=============================

Symfony bundle for automatic file upload handling on Doctrine ORM entities with S3, CDN, and multiple storage support

v8.0.4(1mo ago)0529↓25%2MITPHPPHP ^8.5CI passing

Since Feb 20Pushed 1mo agoCompare

[ Source](https://github.com/chamber-orchestra/file-bundle)[ Packagist](https://packagist.org/packages/chamber-orchestra/file-bundle)[ Docs](https://github.com/chamber-orchestra/file-bundle)[ RSS](/packages/chamber-orchestra-file-bundle/feed)WikiDiscussions main Synced 1mo ago

READMEChangelogDependencies (30)Versions (10)Used By (2)

ChamberOrchestra File Bundle
============================

[](#chamberorchestra-file-bundle)

[![PHP Composer](https://github.com/chamber-orchestra/file-bundle/actions/workflows/php.yml/badge.svg)](https://github.com/chamber-orchestra/file-bundle/actions/workflows/php.yml)[![PHPStan](https://camo.githubusercontent.com/745eb989b9e4903dc598fe2cc63ed4226198be55b7c729001cbd1ece7676fef6/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048505374616e2d6d61782d627269676874677265656e2e737667)](https://phpstan.org/)[![PHP-CS-Fixer](https://camo.githubusercontent.com/6ea88fbe545f6f06950dd97b31be7621fcb0a0056644de2ea36e44b7de33adc4/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f636f64652532307374796c652d5045522d2d435325323025324625323053796d666f6e792d626c75652e737667)](https://cs.symfony.com/)[![Latest Stable Version](https://camo.githubusercontent.com/e5af4c5c807e1a87897334926ae3a8b4e01fac8660049b5b77dd58152a9bba78/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6368616d6265722d6f72636865737472612f66696c652d62756e646c652e737667)](https://packagist.org/packages/chamber-orchestra/file-bundle)[![Total Downloads](https://camo.githubusercontent.com/f6e17838afa829a00e08d329d36a785598734e47512aa02293db2a4fa669c405/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f6368616d6265722d6f72636865737472612f66696c652d62756e646c652e737667)](https://packagist.org/packages/chamber-orchestra/file-bundle)[![License: MIT](https://camo.githubusercontent.com/08cef40a9105b6526ca22088bc514fbfdbc9aac1ddbf8d4e6c750e3a88a44dca/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4d49542d626c75652e737667)](LICENSE)[![PHP 8.5+](https://camo.githubusercontent.com/2371eeb1a98f81a6894947d4d7b429326ee7f4dbeb3d8940776b4ae7b8442725/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048502d382e352532422d3737374242342e737667)](https://www.php.net/)[![Symfony 8.0](https://camo.githubusercontent.com/daaa476b3cc456701380f7d0fbdc3bbe9983e89d3267f99870daa88aa719e181/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f53796d666f6e792d382e302d3030303030302e737667)](https://symfony.com/)

A Symfony bundle for automatic file upload and image upload handling on Doctrine ORM entities. Mark your entity with PHP attributes, and the bundle transparently uploads, injects, and removes files through Doctrine lifecycle events.

Supports local filesystem and Amazon S3 storage backends, multiple named storages, CDN integration, pluggable naming strategies, file archiving, and Doctrine embeddables.

### Features

[](#features)

- **Automatic file uploads** via Doctrine lifecycle events — no manual upload logic
- **Multiple storage backends** — local filesystem, Amazon S3, MinIO
- **Per-entity storage** — different entities can use different storages
- **CDN support** — serve files through CloudFront, Cloudflare, or any CDN
- **Private/secure storage** — store files outside the web root with controlled access
- **File archiving** — archive files before deletion instead of permanent removal
- **Image support** — dimensions, EXIF metadata, orientation detection
- **Doctrine embeddables** — uploadable fields inside embedded objects
- **Pluggable naming strategies** — hashing (default), original name, or custom
- **Symfony Form integration** — compound FileType with delete checkbox and file preservation
- **Symfony Serializer integration** — normalizes files to absolute URLs

Requirements
------------

[](#requirements)

- PHP 8.5+
- Symfony 8.0
- Doctrine ORM

Installation
------------

[](#installation)

```
composer require chamber-orchestra/file-bundle
```

For S3 storage support:

```
composer require aws/aws-sdk-php
```

Quick Start
-----------

[](#quick-start)

### 1. Configure the bundle

[](#1-configure-the-bundle)

```
# config/packages/chamber_orchestra_file.yaml
chamber_orchestra_file:
    storages:
        default:
            driver: file_system
            path: '%kernel.project_dir%/public/uploads'
            uri_prefix: '/uploads'
```

### 2. Add attributes to your entity

[](#2-add-attributes-to-your-entity)

```
use ChamberOrchestra\FileBundle\Mapping\Attribute\Uploadable;
use ChamberOrchestra\FileBundle\Mapping\Attribute\UploadableProperty;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\File\File;

#[ORM\Entity]
#[Uploadable]
class Composition
{
    #[ORM\Id, ORM\GeneratedValue, ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 255)]
    private string $title;

    #[UploadableProperty(mappedBy: 'scorePath')]
    private ?File $score = null;

    #[ORM\Column(length: 255, nullable: true)]
    private ?string $scorePath = null;

    // getters and setters...

    public function getScore(): ?File
    {
        return $this->score;
    }

    public function setScore(?File $score): void
    {
        $this->score = $score;
    }
}
```

### 3. Upload a file

[](#3-upload-a-file)

```
use Symfony\Component\HttpFoundation\File\UploadedFile;

$composition = new Composition();
$composition->setTitle('Symphony No. 5');
$composition->setScore($uploadedFile);

$entityManager->persist($composition);
$entityManager->flush();
```

That's it. The bundle handles the rest:

- Moves the file to the configured storage path
- Persists the relative path in `scorePath`
- On subsequent loads, injects a `Model\File` object with the resolved path and URI

### 4. Access the file

[](#4-access-the-file)

After loading the entity from the database, the `score` property holds a `ChamberOrchestra\FileBundle\Model\File` instance:

```
$composition = $entityManager->find(Composition::class, 1);

$file = $composition->getScore();
$file->getUri();      // "/uploads/symphony_no_5_a1b2c3.pdf"
$file->getPathname(); // "/var/www/public/uploads/symphony_no_5_a1b2c3.pdf"
```

Configuration
-------------

[](#configuration)

The bundle supports multiple named storages. Each storage is defined under the `storages` key and can use different drivers and settings.

### Local Filesystem

[](#local-filesystem)

```
chamber_orchestra_file:
    storages:
        default:
            driver: file_system
            path: '%kernel.project_dir%/public/uploads'
            uri_prefix: '/uploads'
```

### Amazon S3

[](#amazon-s3)

```
chamber_orchestra_file:
    storages:
        default:
            driver: s3
            bucket: my-recordings-bucket
            region: us-east-1
            uri_prefix: '/uploads'        # optional, uses S3 URLs if omitted
            endpoint: 'http://localhost:9000' # optional, for MinIO/localstack
```

### Multiple Storages

[](#multiple-storages)

Define as many storages as you need. Each entity can select which storage to use via the `#[Uploadable]` attribute:

```
chamber_orchestra_file:
    default_storage: public          # optional, first enabled storage is used if omitted
    storages:
        public:
            driver: file_system
            path: '%kernel.project_dir%/public/uploads'
            uri_prefix: '/uploads'
        secure:
            driver: file_system
            path: '%kernel.project_dir%/var/share'
        archive:
            driver: s3
            bucket: orchestra-archive
            region: eu-west-1
```

When only one storage is defined, it becomes the default automatically.

### Secure Storage (private files)

[](#secure-storage-private-files)

For files that should not be publicly accessible (contracts, invoices, internal documents), define a storage without a `uri_prefix`:

```
chamber_orchestra_file:
    storages:
        default:
            driver: file_system
            path: '%kernel.project_dir%/public/uploads'
            uri_prefix: '/uploads'
        secure:
            driver: file_system
            path: '%kernel.project_dir%/var/share'
```

Files stored via a storage with no URI prefix have `getUri()` returning `null`. To serve them, use a controller that reads the file and streams the response with appropriate access control:

```
#[Uploadable(storage: 'secure', prefix: 'contracts')]
class Contract
{
    #[UploadableProperty(mappedBy: 'documentPath')]
    private ?File $document = null;

    #[ORM\Column(nullable: true)]
    private ?string $documentPath = null;
}
```

### Disabling a Storage

[](#disabling-a-storage)

A storage can be temporarily disabled without removing its configuration:

```
chamber_orchestra_file:
    storages:
        default:
            driver: file_system
            path: '%kernel.project_dir%/public/uploads'
            uri_prefix: '/uploads'
        staging:
            enabled: false
            driver: s3
            bucket: staging-uploads
            region: us-east-1
```

### Configuration Reference

[](#configuration-reference)

OptionTypeDefaultDescription`default_storage``string|null``null`Name of the default storage. If null, the first enabled storage is used`storages``map`requiredNamed storage definitions (at least one required)`storages.*.enabled``bool``true`Whether this storage is active`storages.*.driver``string``file_system`Storage driver: `file_system` or `s3``storages.*.path``string``%kernel.project_dir%/public/uploads`Filesystem path (file\_system driver)`storages.*.uri_prefix``string|null``null`Public URI prefix. Null means files are not web-accessible`storages.*.bucket``string|null``null`S3 bucket name (required for s3 driver)`storages.*.region``string|null``null`AWS region (required for s3 driver)`storages.*.endpoint``string|null``null`Custom S3 endpoint for MinIO/localstack`archive_path``string``%kernel.project_dir%/var/archive`Local directory for archived files (`Behaviour::Archive`)Entity Attributes
-----------------

[](#entity-attributes)

### `#[Uploadable]`

[](#uploadable)

Applied to the entity class. Options:

OptionTypeDefaultDescription`prefix``string``''`Subdirectory within the storage path`namingStrategy``string``HashingNamingStrategy::class`Class implementing `NamingStrategyInterface``behaviour``Behaviour``Behaviour::Remove`What happens to files on entity update/delete`storage``string``'default'`Named storage backend to use```
use ChamberOrchestra\FileBundle\Mapping\Attribute\Uploadable;
use ChamberOrchestra\FileBundle\Mapping\Helper\Behaviour;
use ChamberOrchestra\FileBundle\NamingStrategy\OriginNamingStrategy;

#[Uploadable(
    prefix: 'scores',
    namingStrategy: OriginNamingStrategy::class,
    behaviour: Behaviour::Keep,
    storage: 'archive',
)]
class Score
{
    // ...
}
```

### `#[UploadableProperty]`

[](#uploadableproperty)

Applied to file properties. Options:

OptionTypeDescription`mappedBy``string`Name of the string property that stores the relative file pathThe `mappedBy` property must exist on the same class and be a Doctrine-mapped column.

Behaviour
---------

[](#behaviour)

The `Behaviour` enum controls what happens to files when an entity is updated or deleted:

- `Behaviour::Remove` (default) — old files are deleted from storage after a successful flush
- `Behaviour::Keep` — old files remain in storage (useful for audit trails or versioning)
- `Behaviour::Archive` — old files are moved to a local archive directory before being removed from storage

### Archiving

[](#archiving)

When using `Behaviour::Archive`, files are downloaded from their storage backend (including S3) and saved to a local archive directory before deletion. Configure the archive path:

```
chamber_orchestra_file:
    archive_path: '%kernel.project_dir%/var/archive'   # default
    storages:
        default:
            driver: file_system
            path: '%kernel.project_dir%/public/uploads'
            uri_prefix: '/uploads'
```

```
#[Uploadable(behaviour: Behaviour::Archive, prefix: 'contracts')]
class Contract
{
    // Files are archived to var/archive/contracts/ before removal
}
```

Naming Strategies
-----------------

[](#naming-strategies)

### `HashingNamingStrategy` (default)

[](#hashingnamingstrategy-default)

Generates a unique filename using MD5 hash with random bytes, preserving the guessed file extension:

```
a1b2c3d4e5f67890abcdef1234567890.pdf

```

### `OriginNamingStrategy`

[](#originnamingstrategy)

Preserves the original filename as uploaded by the client. Automatically appends a version suffix (`_1`, `_2`, etc.) when a file with the same name already exists in the target directory:

```
moonlight_sonata.pdf
moonlight_sonata_1.pdf   # if moonlight_sonata.pdf already exists
moonlight_sonata_2.pdf   # if _1 also exists

```

### Custom Naming Strategy

[](#custom-naming-strategy)

Implement `NamingStrategyInterface`:

```
use ChamberOrchestra\FileBundle\NamingStrategy\NamingStrategyInterface;
use Symfony\Component\HttpFoundation\File\File;

class TimestampNamingStrategy implements NamingStrategyInterface
{
    public function name(File $file, string $targetDir = ''): string
    {
        return \time() . '_' . \bin2hex(\random_bytes(4)) . '.' . $file->guessExtension();
    }
}
```

Then reference it in the attribute:

```
#[Uploadable(namingStrategy: TimestampNamingStrategy::class)]
```

Entity Traits
-------------

[](#entity-traits)

The bundle provides convenience traits for common file/image patterns:

```
use ChamberOrchestra\FileBundle\Entity\FileTrait;
use ChamberOrchestra\FileBundle\Mapping\Attribute\Uploadable;

#[ORM\Entity]
#[Uploadable(prefix: 'recordings')]
class Recording
{
    use FileTrait; // adds $file + $filePath + getFile()

    #[ORM\Id, ORM\GeneratedValue, ORM\Column]
    private ?int $id = null;
}
```

Available traits:

TraitPropertiesNullable`FileTrait``$file` / `$filePath`Yes`RequiredFileTrait``$file` / `$filePath`No`ImageTrait``$image` / `$imagePath`Yes`RequiredImageTrait``$image` / `$imagePath`NoMultiple File Fields
--------------------

[](#multiple-file-fields)

An entity can have multiple uploadable properties:

```
#[ORM\Entity]
#[Uploadable(prefix: 'compositions')]
class Composition
{
    #[UploadableProperty(mappedBy: 'scorePath')]
    private ?File $score = null;

    #[ORM\Column(nullable: true)]
    private ?string $scorePath = null;

    #[UploadableProperty(mappedBy: 'recordingPath')]
    private ?File $recording = null;

    #[ORM\Column(nullable: true)]
    private ?string $recordingPath = null;
}
```

Doctrine Embeddables
--------------------

[](#doctrine-embeddables)

The bundle supports uploadable fields inside Doctrine embeddables:

```
#[ORM\Embeddable]
#[Uploadable(prefix: 'media')]
class MediaEmbed
{
    #[UploadableProperty(mappedBy: 'coverPath')]
    private ?File $cover = null;

    #[ORM\Column(nullable: true)]
    private ?string $coverPath = null;
}

#[ORM\Entity]
#[Uploadable]
class Album
{
    #[ORM\Embedded(class: MediaEmbed::class)]
    private MediaEmbed $media;
}
```

Image Support
-------------

[](#image-support)

`Model\File` includes `ImageTrait` with image-specific helpers:

```
$image = $album->getCover();

if ($image->isImage()) {
    $image->getWidth();           // 1920
    $image->getHeight();          // 1080
    $image->getRatio();           // 1.78
    $image->getOrientation();     // EXIF orientation value
    $image->getOrientationAngle(); // 90, 180, -90, or 0
    $image->getMetadata();        // Full EXIF data array
}
```

Events
------

[](#events)

The bundle dispatches events around file upload and removal, allowing you to hook in for image processing, cache clearing, thumbnail cleanup, CDN invalidation, etc.

All events carry `$entity` — the entity object that triggered the event. This allows listeners to filter by entity type.

EventDispatched`PreUploadEvent`Before a file is uploaded to storage`PostUploadEvent`After a file is uploaded to storage`PreRemoveEvent`Before a file is deleted from storage`PostRemoveEvent`After a file is deleted from storage### Upload Events

[](#upload-events)

Upload events carry `$entity`, `$file`, and `$fieldName`. Use `PostUploadEvent` for post-processing like image resizing:

```
use ChamberOrchestra\FileBundle\Events\PostUploadEvent;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;

#[AsEventListener]
class ImageResizeListener
{
    public function __invoke(PostUploadEvent $event): void
    {
        // Only process images for specific entity types
        if (!$event->entity instanceof Album) {
            return;
        }

        if ($event->file->isImage()) {
            $this->resizer->resize($event->file->getPathname(), 1920, 1080);
        }
    }
}
```

### Removal Events

[](#removal-events)

Removal events carry `$entity`, `$relativePath`, `$resolvedPath`, and `$resolvedUri`:

```
use ChamberOrchestra\FileBundle\Events\PreRemoveEvent;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;

#[AsEventListener]
class ThumbnailCleanupListener
{
    public function __invoke(PreRemoveEvent $event): void
    {
        // Clear thumbnails for the file being removed
        $this->thumbnailService->purge($event->relativePath);
    }
}
```

Form Type
---------

[](#form-type)

The bundle provides a `FileType` form type for handling file uploads in Symfony forms. It is a compound form with a file input and an optional delete checkbox that preserves the original `Model\File` across submissions.

```
composer require symfony/form symfony/validator
```

### Basic Usage

[](#basic-usage)

```
use ChamberOrchestra\FileBundle\Form\Type\FileType;

$builder->add('score', FileType::class);
```

### Options

[](#options)

OptionTypeDefaultDescription`multiple``bool``false`Allow multiple file uploads`mime_types``array``[]`Allowed MIME types (sets `accept` attribute and validation)`required``bool``false`Whether a file is required`allow_delete``bool``true`Show a delete checkbox (only when `required` is `false`)`entry_options``array``[]`Options passed to the inner Symfony `FileType``delete_options``array``[]`Options passed to the delete `CheckboxType`### With MIME Type Restriction

[](#with-mime-type-restriction)

```
$builder->add('score', FileType::class, [
    'mime_types' => ['application/pdf'],
    'required' => true,
]);
```

### Template Variables

[](#template-variables)

The form view exposes:

- `original_file` — the original `Model\File` instance (for displaying the current file)
- `allow_delete` — whether the delete checkbox is shown
- `multiple` — whether multiple uploads are allowed

Serializer Integration
----------------------

[](#serializer-integration)

The bundle includes a Symfony Serializer normalizer for `Model\File` that outputs an absolute URL.

The normalizer uses the `APP_URL` environment variable as the base URL:

```
$serializer->normalize($composition);
// "score" => "https://example.com/uploads/scores/a1b2c3.pdf"
```

When a file's URI is already an absolute URL (common with S3 or CDN storages), the normalizer returns it as-is without prepending the base:

```
// S3 storage with no uri_prefix — URI is already absolute
$serializer->normalize($recording);
// "score" => "https://my-bucket.s3.amazonaws.com/scores/a1b2c3.pdf"

// Storage with CDN uri_prefix — URI is already absolute
// uri_prefix: 'https://cdn.example.com/uploads'
$serializer->normalize($recording);
// "score" => "https://cdn.example.com/uploads/scores/a1b2c3.pdf"
```

### CDN Support

[](#cdn-support)

To serve files through a CDN, set the storage's `uri_prefix` to the CDN base URL:

```
chamber_orchestra_file:
    storages:
        default:
            driver: file_system
            path: '%kernel.project_dir%/public/uploads'
            uri_prefix: 'https://cdn.example.com/uploads'
```

Files will be injected with the CDN URL as their URI, and the serializer will pass it through unchanged.

Security
--------

[](#security)

The default storage path (`%kernel.project_dir%/public/uploads`) is inside the web root. If your web server is configured to execute scripts (PHP, Python, etc.) from that directory, an uploaded file could be executed via HTTP.

**Disable script execution** in your upload directories:

Nginx:

```
location /uploads {
    location ~ \.(php|phtml|php[0-9])$ {
        deny all;
    }
}
```

Apache (`.htaccess` in the uploads directory):

```

    Require all denied

```

Alternatively, store files **outside the web root** by using a storage path like `%kernel.project_dir%/var/files` with no `uri_prefix`, and serve them through a controller with access control.

License
-------

[](#license)

MIT License. See [LICENSE](LICENSE) for details.

###  Health Score

47

—

FairBetter than 94% of packages

Maintenance89

Actively maintained with recent releases

Popularity18

Limited adoption so far

Community12

Small or concentrated contributor base

Maturity57

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 75% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~3 days

Total

9

Last Release

55d ago

Major Versions

v0.0.1 → 8.0.12026-02-20

v0.0.3 → v8.0.32026-03-13

### Community

Maintainers

![](https://www.gravatar.com/avatar/44037eb1c8dc2c4fa9871ac213653f33e22a9348dcec7132df07cc71933f2a2e?d=identicon)[wtorsi](/maintainers/wtorsi)

---

Top Contributors

[![wtorsi](https://avatars.githubusercontent.com/u/2115840?v=4)](https://github.com/wtorsi "wtorsi (3 commits)")[![baldrys-ed](https://avatars.githubusercontent.com/u/60212508?v=4)](https://github.com/baldrys-ed "baldrys-ed (1 commits)")

---

Tags

awscdndoctrinedoctrine-ormfile-managerfile-storagefile-uploadimage-uploadminioormphps3symfonysymfony-bundleuploadsymfonys3awsimageormdoctrinefilestorageuploadfile managercdnSymfony Bundlefile-uploadImage uploadfile storageminio

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/chamber-orchestra-file-bundle/health.svg)

```
[![Health](https://phpackages.com/badges/chamber-orchestra-file-bundle/health.svg)](https://phpackages.com/packages/chamber-orchestra-file-bundle)
```

###  Alternatives

[sonata-project/doctrine-orm-admin-bundle

Integrate Doctrine ORM into the SonataAdminBundle

46117.7M155](/packages/sonata-project-doctrine-orm-admin-bundle)[mostafaznv/larupload

Larupload is a ORM based file uploader for laravel to upload image, video, audio and other known files.

73403.7k3](/packages/mostafaznv-larupload)[codesleeve/stapler

Elegant and simple ORM-based file upload package for php.

538366.4k5](/packages/codesleeve-stapler)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
