Install the Bundle
composer require chamber-orchestra/file-bundle
Add to config/bundles.php:
return [
// ...
ChamberOrchestra\FileBundle\ChamberOrchestraFileBundle::class => ['all' => true],
];
Configure Storage
Define storage backends in config/packages/chamber_orchestra_file.yaml:
chamber_orchestra_file:
storages:
default:
type: 'local' # or 's3'
options:
directory: '%kernel.project_dir%/public/uploads'
s3_backup:
type: 's3'
options:
bucket: 'my-bucket'
region: 'us-east-1'
key: 'AKIAXXXXX'
secret: 'XXXXXXXXXXXXXXXXXXXX'
Annotate an Entity
Use the #[File] attribute on a property:
use ChamberOrchestra\FileBundle\Attribute\File;
#[ORM\Entity]
class Product {
#[File(storage: 'default')]
private ?FileInfo $image;
}
First Upload
Use a form with enctype="multipart/form-data" and submit to a controller. The bundle handles the rest via Doctrine lifecycle events.
// src/Controller/ProductController.php
#[Route('/product/{id}/upload', name: 'upload_product_image')]
public function uploadImage(Product $product, Request $request): Response
{
$form = $this->createFormBuilder($product)
->add('image', FileType::class)
->getForm();
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->entityManager->flush(); // Bundle handles upload automatically
$this->addFlash('success', 'Image uploaded!');
}
return $this->redirectToRoute('product_show', ['id' => $product->getId()]);
}
Define the Entity
Use #[File] with optional parameters:
#[File(
storage: 's3_backup',
cdn: 'https://cdn.example.com',
namingStrategy: 'uuid',
allowedTypes: ['image/jpeg', 'image/png'],
maxSize: 5 * 1024 * 1024 // 5MB
)]
private ?FileInfo $thumbnail;
Form Integration
Use Symfony’s FileType in forms:
$builder->add('document', FileType::class, [
'label' => 'PDF Document',
'mapped' => false, // If not directly mapped to entity
]);
Lifecycle Events
The bundle hooks into prePersist, preUpdate, and preRemove to:
persist/update.remove.Accessing File Info
Retrieve metadata via FileInfo:
$product->getImage()->getPath(); // e.g., 'uploads/products/123.jpg'
$product->getImage()->getUrl(); // e.g., 'https://cdn.example.com/uploads/123.jpg'
Multiple Storage Backends Route files dynamically based on logic:
#[File(storage: $product->isPremium() ? 's3_backup' : 'default')]
private ?FileInfo $media;
CDN and URL Generation Generate public URLs with CDN support:
$this->twig->render('product/show.html.twig', [
'imageUrl' => $product->getImage()->getUrl(), // Uses CDN if configured
]);
Validation Leverage Symfony’s constraints:
#[File(
allowedTypes: ['image/jpeg', 'image/png'],
maxSize: 2097152, // 2MB
)]
#[Assert\File(maxSize: "2M")]
private ?FileInfo $avatar;
Embeddables Use with Doctrine embeddables for reusable file logic:
#[ORM\Embeddable]
class MediaAsset {
#[File(storage: 'default')]
private ?FileInfo $file;
}
Custom Naming Strategies
Extend NamingStrategyInterface:
class CustomNamingStrategy implements NamingStrategyInterface {
public function generateName(FileInfo $file): string {
return 'custom_' . uniqid() . '.' . $file->getExtension();
}
}
Register in config:
chamber_orchestra_file:
naming_strategies:
custom: App\Strategy\CustomNamingStrategy
Use in entity:
#[File(namingStrategy: 'custom')]
Doctrine Events Override
Avoid manually handling prePersist/preUpdate for file fields—let the bundle manage it. Overriding these may break uploads.
File Deletion on Entity Remove The bundle deletes files when the entity is removed. To bypass this, use:
$entityManager->remove($entity);
$entityManager->flush(); // File is deleted
$entityManager->clear(); // Prevents accidental deletion
S3 Permissions Ensure your S3 IAM user has:
s3:PutObjects3:GetObjects3:DeleteObject
Permissions for the specified bucket.File Overwrites
By default, files are overwritten if the filename collides. To prevent this, use a unique namingStrategy (e.g., uuid).
Symfony Cache Clear the cache after changing bundle configurations:
php bin/console cache:clear
Enable Debug Mode
Set debug: true in config to log uploads/deletions:
chamber_orchestra_file:
debug: true
Check Events Verify Doctrine events are firing:
// In a subscriber or listener
public function onPrePersist(LifecycleEventArgs $args) {
$entity = $args->getObject();
if ($entity instanceof Product) {
dump($entity->getImage()); // Debug file info
}
}
Storage Issues
directory permissions (chmod -R 775 public/uploads).php bin/console debug:container chamber_orchestra_file.storage.s3_backup
Custom Storage Backends
Implement StorageInterface:
class CustomStorage implements StorageInterface {
public function upload(FileInfo $file, string $path): void { /* ... */ }
public function delete(string $path): void { /* ... */ }
public function getUrl(string $path): string { /* ... */ }
}
Register in config:
chamber_orchestra_file:
storages:
custom:
type: 'custom'
class: App\Storage\CustomStorage
Event Subscribers
Listen to FileUploadEvent or FileDeleteEvent:
#[AsEventListener(event: FileUploadEvent::class)]
public function onFileUpload(FileUploadEvent $event) {
if ($event->getFile()->getMimeType() === 'image/jpeg') {
// Resize image, etc.
}
}
File Archiving
Use the archive option to move old files to a separate directory:
chamber_orchestra_file:
archive:
enabled: true
directory: '%kernel.project_dir%/public/uploads/archive'
Doctrine Types
Extend the FileType for custom behavior:
#[Type]
class CustomFileType extends FileType {
public function convertToPHPValue($value, AbstractPlatform $platform) {
// Custom logic
return parent::convertToPHPValue($value, $platform);
}
}
Register in doctrine.yaml:
dbal:
types:
custom_file: App\Doctrine\DBAL\Types\CustomFileType
How can I help you explore Laravel packages today?