A Symfony bundle that handles file uploads via forms and automatically stores the resulting URL in your entity. No more manual file handling in controllers!
- π Zero-configuration file uploads - Just add an option to your form field
- π¦ Flysystem integration - Works with any storage backend (local, S3, GCS, SFTP, etc.)
- π Automatic URL storage - The file URL is stored directly in your entity
- π― Custom naming strategies - Full control over uploaded file names
- ποΈ Auto-cleanup - Optionally delete previous files when uploading new ones
- π¨ Twig integration - Preview uploaded images in your forms
| Version | PHP | Symfony |
|---|---|---|
| 2.x | β₯ 8.1 | 6.4, 7.x, 8.x |
| 1.x | β₯ 8.1 | 6.x, 7.x |
composer require tiloweb/uploaded-file-type-bundleIf you're not using Symfony Flex, add the bundle manually:
// config/bundles.php
return [
// ...
Tiloweb\UploadedFileTypeBundle\UploadedFileTypeBundle::class => ['all' => true],
];First, configure your filesystem adapter using the OneupFlysystemBundle:
# config/packages/oneup_flysystem.yaml
oneup_flysystem:
adapters:
default_adapter:
local:
location: '%kernel.project_dir%/public/uploads'
filesystems:
default_filesystem:
adapter: default_adapter
alias: League\Flysystem\Filesystem# config/packages/uploaded_file_type.yaml
uploaded_file_type:
configurations:
# Default configuration for general uploads
default:
filesystem: 'oneup_flysystem.default_filesystem'
base_uri: 'https://example.com/uploads'
path: '/files'
# Separate configuration for user avatars
avatars:
filesystem: 'oneup_flysystem.default_filesystem'
base_uri: 'https://example.com/uploads'
path: '/avatars'
# S3 configuration for large files
documents:
filesystem: 'oneup_flysystem.s3_filesystem'
base_uri: 'https://cdn.example.com'
path: '/documents'| Option | Type | Required | Description |
|---|---|---|---|
filesystem |
string | β | The Flysystem filesystem service ID |
base_uri |
string | β | The base URL for accessing uploaded files |
path |
string | β | The subdirectory path within the filesystem |
Simply add the upload option to any FileType field:
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\FormBuilderInterface;
class ProductType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name')
->add('image', FileType::class, [
'upload' => 'default', // Use the 'default' configuration
'required' => false,
])
;
}
}That's it! When the form is submitted:
- The file is uploaded to your configured filesystem
- The URL is automatically stored in the
$imageproperty - No additional controller code needed!
Control how files are named when uploaded:
$builder->add('avatar', FileType::class, [
'upload' => 'avatars',
'filename' => function (UploadedFile $file, User $user): string {
return sprintf(
'user_%d_%s.%s',
$user->getId(),
bin2hex(random_bytes(8)),
$file->guessClientExtension() ?? 'bin'
);
},
]);By default, the previous file is deleted when uploading a new one. Disable this behavior:
$builder->add('document', FileType::class, [
'upload' => 'documents',
'delete_previous' => false, // Keep old files
]);| Option | Type | Default | Description |
|---|---|---|---|
upload |
string | - | The configuration name to use |
filename |
callable | auto | Custom filename generator |
delete_previous |
bool | true |
Delete previous file on update |
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class Product
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
private string $name = '';
#[ORM\Column(length: 500, nullable: true)]
private ?string $image = null;
// Getters and setters...
public function getImage(): ?string
{
return $this->image;
}
public function setImage(?string $image): static
{
$this->image = $image;
return $this;
}
}namespace App\Form;
use App\Entity\Product;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\Image;
class ProductType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name', TextType::class, [
'label' => 'Product Name',
])
->add('image', FileType::class, [
'upload' => 'default',
'required' => false,
'label' => 'Product Image',
'constraints' => [
new Image([
'maxSize' => '5M',
'mimeTypes' => ['image/jpeg', 'image/png', 'image/webp'],
]),
],
])
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Product::class,
]);
}
}namespace App\Controller;
use App\Entity\Product;
use App\Form\ProductType;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class ProductController extends AbstractController
{
#[Route('/product/new', name: 'product_new')]
public function new(Request $request, EntityManagerInterface $em): Response
{
$product = new Product();
$form = $this->createForm(ProductType::class, $product);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em->persist($product);
$em->flush();
// $product->getImage() now contains the URL!
// e.g., "https://example.com/uploads/files/product_a1b2c3d4.jpg"
return $this->redirectToRoute('product_show', ['id' => $product->getId()]);
}
return $this->render('product/new.html.twig', [
'form' => $form,
]);
}
}The bundle provides a form theme that displays the current image:
{# templates/product/new.html.twig #}
{% extends 'base.html.twig' %}
{% block body %}
<h1>New Product</h1>
{{ form_start(form) }}
{{ form_row(form.name) }}
{{ form_row(form.image) }} {# Shows current image if exists #}
<button type="submit">Save</button>
{{ form_end(form) }}
{% endblock %}Override the default template to customize how uploaded files are displayed:
{# templates/form/uploaded_file.html.twig #}
{% extends '@UploadedFileType/form.html.twig' %}
{% block file_widget %}
{{ parent() }}
{% if url is defined and url is not null %}
<div class="uploaded-file-preview">
{% if url matches '/\\.(jpg|jpeg|png|gif|webp)$/i' %}
<img src="{{ url }}" alt="Preview" class="preview-image">
{% else %}
<a href="{{ url }}" target="_blank">View current file</a>
{% endif %}
</div>
{% endif %}
{% endblock %}Register your custom theme:
# config/packages/twig.yaml
twig:
form_themes:
- 'form/uploaded_file.html.twig'use Tiloweb\UploadedFileTypeBundle\UploadedFileTypeService;
class MyService
{
public function __construct(
private UploadedFileTypeService $uploadService,
) {}
public function uploadFile(UploadedFile $file): string
{
return $this->uploadService->upload(
filename: 'custom-name.pdf',
uploadedFile: $file,
configuration: 'documents'
);
}
public function deleteFile(string $url): bool
{
return $this->uploadService->delete($url, 'documents');
}
public function fileExists(string $url): bool
{
return $this->uploadService->exists($url, 'documents');
}
}# config/packages/oneup_flysystem.yaml
oneup_flysystem:
adapters:
s3_adapter:
awss3v3:
client: Aws\S3\S3Client
bucket: '%env(AWS_S3_BUCKET)%'
prefix: uploads
filesystems:
s3_filesystem:
adapter: s3_adapter
# config/packages/uploaded_file_type.yaml
uploaded_file_type:
configurations:
s3:
filesystem: 'oneup_flysystem.s3_filesystem'
base_uri: 'https://%env(AWS_S3_BUCKET)%.s3.%env(AWS_REGION)%.amazonaws.com'
path: '/uploads'oneup_flysystem:
adapters:
gcs_adapter:
googlecloudstorage:
client: Google\Cloud\Storage\StorageClient
bucket: '%env(GCS_BUCKET)%'
filesystems:
gcs_filesystem:
adapter: gcs_adapter
uploaded_file_type:
configurations:
gcs:
filesystem: 'oneup_flysystem.gcs_filesystem'
base_uri: 'https://storage.googleapis.com/%env(GCS_BUCKET)%'
path: '/uploads'# Run tests
composer test
# Run static analysis
composer phpstan
# Fix coding standards
composer cs-fixContributions are welcome! Please see CONTRIBUTING.md for details.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
This bundle is released under the MIT License.
- Thibault HENRY - Creator
- All Contributors