[Upload] Add the UX Upload package - #3769
Open
smnandre wants to merge 1 commit into
Open
Conversation
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | ||
| with: | ||
| node-version-file: '.nvmrc' | ||
| cache: 'pnpm' |
Contributor
📊 Packages dist files size differenceThanks for the PR! Here is the difference in size of the packages dist files between the base branch and the PR.
|
||||||||||||||||||||||||||||||||||||||||||||||||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TL;DR;
From a small attachment to a large interrupted transfer, UX Upload keeps one Symfony Form contract: files upload before submit and survive validation errors without being selected or sent again.
Small files use one request, large files can resume, and Symfony receives a signed, lazy
CompletedUploadwhile the application owns permanent storage.UX Upload
Add one field and render it like any other Symfony Form field:
{{ form_row(form.attachment) }}The simple path stays simple. The same field automatically adds progress, validation persistence and resumable transfers when they are needed, without application JavaScript or a custom upload controller.
Objectives
UX Upload provides a modern upload foundation that feels native to Symfony and Symfony UX:
Features
Transport follows the effective uploader policy automatically:
flowchart LR A["Select a file"] ==> B{"size <= chunk_size?"} B ==> C["Yes: one multipart POST"] B ==> D["No: resumable chunks"] C ==> E["Temporary file + signed form value"] D ==> E E ==> F["Invalid form: re-render without re-upload"] E ==> G["Valid form: CompletedUpload"] G ==> H["Application storeOnce()"] E ==> I["Scheduled TTL cleanup"]CompletedUploadvalues, named uploader policies and values preserved across invalid form submissions;Out of Scope
Requirements
Required Dependencies
symfony/stimulus-bundle3.0 or later.Symfony Lock and MIME are required. Lock coordinates concurrent writes, retries and cleanup; MIME supports server-side content validation.
Optional Dependencies
The uploader, controller, Form type and local temporary storage work without any of the optional packages below. These dependencies are only needed for their corresponding integration:
symfony/twig-bundle ^7.4|^8.0symfony/console ^7.4|^8.0ux:upload:cleanupcommandsymfony/security-csrf ^7.4|^8.0symfony/security-bundle ^7.4|^8.0symfony/rate-limiter ^7.4|^8.0league/flysystem ^3.0symfony/ux-live-component ^2.20|^3.0ComponentWithUploadTraitand the LiveComponent bridgeUsage
Named uploaders keep transport policy on the server:
On a valid submission, the field contains a lazy
CompletedUpload. Its metadata is available without reading storage;openStream()is the explicit I/O boundary:$documentStorageandstoreOnce()are application code. The upload ID is an idempotency key: receiving the same valid request twice must not create two permanent files.Configuration
Global defaults apply to every uploader; named uploaders may narrow or override them:
chunk_sizeis both the direct-upload threshold and the part size for resumable uploads. Distributed deployments use Flysystem and must configure a shared lock store.integrity_algorithmselects the optional whole-file checksum attempted by the browser for files up to 64 MiB. The bundled transport independently sends a SHA-256 digest for every direct body or chunk.Integration
Symfony Forms and Twig
FileUploadTypeuses standard Symfony form rendering, validation errors and re-rendering. Itslayoutoption selectscompactordropzone, whileshow_previewindependently enables image previews in either layout. Collection entry indices are normalized in the signed field path, so removing or reordering an entry does not invalidate its upload or weaken isolation from other fields.The form theme owns the complete markup and exposes one composable contract made of ten blocks for the standard Form row, widget, picker, item visual, progress, actions, summary and client errors. Applications import it once with Twig
use, override one or several related blocks in the same application form theme, and can register that theme globally throughtwig.form_themes. Standard Symfony options render the label, help, row, errors and native file input attributes;widget_attrcustomizes the UX Upload container. Stimulus clones Twig<template>elements and updates state, nativehiddenanddisabledproperties, and accessibility attributes without constructing presentation markup. File-specific failures stay in their item; Symfony errors and general client feedback share the field-level presentation.Two standalone Baseline 2026 stylesheets provide optional compact and dropzone treatments. Both AssetMapper
autoimportentries are disabled by default, so the bundle remains compatible with an application's existing form theme and CSS.This PR deliberately introduces a Form type, not a Twig Component: the form is the integration, validation and submission boundary.
Live Components
ComponentWithUploadTraitcan assign a completed upload to an explicitly authorized nullable#[LiveProp].#[UploadTarget]is the allow-list, whilegetUpload()lazily resolves its signed token.The property stores the signed token rather than bytes or a path. The bridge uses LiveComponent's public JavaScript API and remains inert when the optional package is absent.
Implementation
Direct and chunked transfers converge on the same uploader pipeline, storage adapters, validation, events and final token format. The boundary is intentional: UX Upload solves browser transfer and temporary-file reliability while applications retain their own storage and transaction model.
The default direct transport uses XHR because browsers expose upload progress there, unlike
fetch(). Explicitly injectedfetchtransports remain supported for custom frontends, but direct progress then depends on an injected XHR factory. A direct network failure is never replayed automatically because the server may already have completed it; only an explicit413falls back to chunks.Metadata access does not contact storage. Only
openStream()may fetch bytes, which matters for remote backends and repeated form requests. Temporary files remain cleanup candidates, so applications copy the bytes they need and make that operation idempotent. Cleanup stays outside the form request because an immediate delete would make a repeated request fail.The whole-file checksum is optional and may be absent from
CompletedUpload. Applications that require a checksum for every file calculate it while copying into permanent storage or use a checksum supplied by their storage provider.The built-in Flysystem adapter accepts any configured
FilesystemOperator, including S3, Azure or GCS adapters.MultipartUploadStorageInterfacelets a custom server-side storage complete provider-native parts, but UX Upload deliberately does not implement presigned browser-to-cloud ingestion.The cleanup command intentionally exposes an age threshold rather than a generic batch limit. Very large remote inventories need provider pagination, lifecycle policies or an application-owned cleanup worker. Applications that increase file count, chunk size or parallelism also own the resulting browser, proxy and storage capacity.
Security
Upload endpoints accept untrusted browser input and write potentially large objects. The Bundle therefore applies security at every transition instead of relying on the form submission alone.
Requests and Authorization
File Validation
Storage and Concurrency
Tests
distverification and Composer security audit.Documentation
The documentation follows the user journey from the first field to production operation:
The detailed guide is authored as 19 publishable reStructuredText pages. It covers the first field, transport and storage concepts, customization, security, integrations and production operation.
index.rst,installation.rstform.rst,named-uploaders.rstarchitecture.rst,upload-lifecycle.rst,storage.rst,persisting-uploaded-files.rstjavascript.rst,customizing-upload-field.rst,events.rstvalidation.rst,security.rst,retry-and-resume.rstlive-component.rstproduction.rst,testing.rst,debugging.rstconfiguration.rstComing Next
Companion pull requests will address: