pentatrion/upload-bundle
Pentatrion UploadBundle is a Symfony bundle that simplifies file uploads with Dropzone-powered AJAX uploads, form integration, previews, and server-side handling for robust upload workflows.
pentatrion/upload-bundle is a Symfony bundle, not a Laravel package — so the first decision is whether you're actually in a Symfony app (or willing to lift the source into a Laravel service). It has 4 stars and 0 dependents, so treat it as a well-structured reference implementation you may need to fork rather than a battle-tested dependency.
Minimal path:
composer require pentatrion/upload-bundle
config/bundles.php and check config/packages/pentatrion_upload.yaml after bin/console config:dump-reference pentatrion_upload.Pentatrion\UploadBundle\Entity\File and TemporaryFile, then point config at them:
pentatrion_upload:
file_class: App\Entity\Attachment
temporary_file_class: App\Entity\TemporaryAttachment
storage_directory: '%kernel.project_dir%/var/uploads'
Where to look first (in this order): README.md → src/FileManager.php + src/FileManagerInterface.php (the entire public API) → src/DependencyInjection/Configuration.php (every config key) → src/Entity/File.php. The docs are thin; the source is small, so read it.
First use case: a single avatar/image upload on a User profile form using Symfony UX Dropzone. The bundle owns the HTTP endpoint that receives the file, writes it to disk, persists a File row, and returns JSON; your controller just receives the resulting file reference and links it to User.
For a Laravel brain: FileManagerInterface ≈ a service wrapping Storage::disk()->putFileAs(), and the File entity ≈ a Media model. There is no Storage facade, no streams abstraction, no S3 driver — it's local filesystem + Doctrine.
Inject the manager, not the entity. FileManagerInterface is the seam you'll mock in tests:
public function upload(Request $request, FileManagerInterface $fileManager): Response
{
$file = $fileManager->handleFile($request->files->get('file'), 'avatars/');
// $file is now a persisted File entity with a generated storage name
}
Temporary → permanent promotion. The typical flow is: JS uploads immediately to a temp area, the form submit posts a token/ID, and your controller promotes it:
handleTemporaryFile($temporaryFile) when the form validatesdeleteTemporaryFiles() on abandon, plus a scheduled cleanup (cron or a kernel.terminate listener)One-to-many galleries. Store File entities against a parent with OneToMany, and expose getFiles()-style payloads so the frontend Dropzone/FilePond controller can rehydrate the existing list on edit forms. Prefer an explicit ordering column over relying on insertion order.
Validation belongs outside the bundle. Enforce size/MIME with Symfony Validator constraints on a DTO wrapping the File, not with the dropzone config — client-side limits are advisory only.
Testing. Functional-test the upload endpoint with UploadedFile::fake()-equivalent (new UploadedFile($path, 'x.png', 'image/png', null, true)), asserting the JSON shape rather than internals; unit-test your own controller against a mocked FileManagerInterface.
Integration tip. Mirror the bundle's delete endpoint (DELETE on the file route) rather than deleting files directly from your controller, so DB rows and disk stay in sync.
composer.json, and vendor/fork the FileManager if you depend on it heavily. Budget an hour to read src/ before adopting.File — you'll collide with Symfony\Component\HttpFoundation\File\File in imports. Use Attachment/Media.Configuration.php whether storage_directory is resolved relative to the project dir or the CWD; the safest move is an absolute %kernel.project_dir%-prefixed value. Ensure the web server user can write there.post_max_size/upload_max_filesize in PHP — check PHP limits before blaming the bundle. Also confirm the route prefix is what the JS controller expects.FileManagerInterface to your own service in services.yaml to override behavior, and add metadata fields (alt text, ordering, tenant id) to your File subclass rather than editing the bundle.File entity subclass, treat schema changes as yours; don't expect upstream migrations to cover your added columns.How can I help you explore Laravel packages today?