Validate the File constraint in the browser - #18
Merged
Merged
Conversation
The File constraint had no JavaScript counterpart, so maxSize, mimeTypes, extensions and filenameMaxLength were silently skipped and the user only learned about them after a full upload round trip. Add SymfonyComponentValidatorConstraintsFile, checking everything the File API exposes: the size, the sniffed mime type (wildcards included), the extension and the length of the file name. Sizes are factorized the way FileValidator factorizes them so the messages read the same as the server-side ones. Read the "files" list of a file input instead of its fake path, so the constraint receives File objects and NotBlank still sees an empty list as an empty value. Export the "maxSize" option from the factory: it is a protected property behind a magic getter, so the generic public property export of the model never saw it. Image is not covered: its own options need the image decoded first, which is asynchronous in the browser. Fixes formapro#116
66Ton99
force-pushed
the
feat/issue-116-file-constraint
branch
from
August 24, 2026 19:35
76c4de2 to
a464e6a
Compare
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.
Fixes formapro/JsFormValidatorBundle#116.
What was broken
@Assert\Filehad no JavaScript counterpart.src/Resources/public/js/constraints/contained no
File.jsandindex.jsnever imported one, soSvarohJsFormValidator.parseConstraints()— which instantiateswindow[className]—silently dropped the constraint.
NotBlankon a file input worked, butmaxSize,mimeTypes,extensionsandfilenameMaxLengthwere only reported after a fullupload round trip. The maintainer confirmed on the issue that it was not implemented.
Two further reasons it could not just work:
getSpecifiedElementTypeValue()readdomNode.valuefor every input, which for afile input is only the fake path (
C:\fakepath\avatar.png), never the files.Symfony\Component\Validator\Constraints\File::$maxSizeis a protected propertybehind
__get().JsModelAbstract::phpValueToJs()exports constraints withget_object_vars(), which sees public properties only, so the limit never reached thebrowser at all. Every other option (
binaryFormat,mimeTypes,extensions,filenameMaxLength,filenameCountUnit, all the messages) is public and was alreadyexported generically.
What changed
src/Resources/public/js/constraints/File.js(new). Checks what the File API cansee, in Symfony's order and with Symfony's early returns: filename length →
empty file →
maxSize→extensions→mimeTypes.factorizeSizes()is ported fromFileValidator(samek/M/Ki/Mifactors, same 2-decimal reduction, samebytes/kB/MB/KiB/MiBsuffices), somaxSizeMessagerenders{{ size }}/{{ limit }}/{{ suffix }}exactly as the server does.mimeTypesMessage,extensionsMessage,disallowEmptyMessageand the pluralfilenameTooLongMessageget their placeholders too.filenameCountUnitis honouredfor
bytes(UTF-8 length),codepointsandgraphemes(Intl.Segmenter, fallingback to code points).
index.js/globals.test.js— register the constraint the way the others are.SvarohJsFormValidator.js—getSpecifiedElementTypeValue()now returns thefileslist of a file input via the newgetFileListValue(). An empty selection is anempty array, so
isValueEmty()keepsNotBlankworking; without the File API it fallsback to the previous string value.
src/Factory/JsFormValidatorFactory.php—parseConstraints()exports aFileconstraint as its public options plus the normalized
maxSize, keepingget_class($item)as the key so the JavaScript constructor name is unchanged.src/Resources/doc/3_16.mdplus a README index entry.Deliberately not covered
Image. Its own options (maxWidth,minHeight,maxRatio, …) need the filedecoded into an
Imageobject, which is asynchronous. The library has an async path(the ajax queue used by
UniqueEntity), but it is built around a request/responseround trip rather than a load event, and wiring image decoding into it is a separate
piece of work. Rather than half-implement both,
Imageis left server-side only anddocumented as such; an
Imageconstraint is skipped in the browser exactly as before.File.typeis sniffed and can be empty orwrong. An empty type skips the mime check rather than rejecting the file, so the server
keeps the last word.
extensionsnarrowing the accepted mime types. That needs thesymfony/mimemediatype database; only the extension itself is checked client-side, which can only be more
permissive than the server, never less.
filenameCharset,notFoundMessage,notReadableMessageand theupload*ErrorMessagefamily describe conditions that only exist server-side.
Tests
File.test.js— 36 cases: the four size/suffix factorizations (including the tworeduction loops), empty file, mime types with wildcards and with a string option,
unsniffed type, extensions as a list and as a mime map, the extension and mime error
reported together, the three filename count units and the
Intl.Segmenterfallback,singular/plural filename message, multiple uploads, a real
Fileobject, and valuesthat carry no file information. 100% statements / 96% branches on the new file.
SvarohJsFormValidator.test.js— a case for the file input value: thefileslist, theempty list still being an empty value for
NotBlank, and the fake-path fallback.JsFormValidatorFactoryTest::testFileConstraintExportsItsNormalizedMaxSize— asserts'maxSize':2097152and'binaryFormat':truereach the generated model fornew Assert\File(maxSize: '2Mi', …). It errors onmainbecause the constraint isstill exported as an object without the option.
composer test43 → 44 tests, 151 → 156 assertions.npx jest34 → 35 suites,347 → 385 tests.
composer phpstanclean,git diff --checkclean. No Cypress change:the browser behaviour is fully covered by the Jest cases.
Note for the merger
src/Resources/doc/3_15.mdis taken by two other open branches(
fix/issue-120-notblank-whitespace,fix/issue-148-constraint-property-path), so thispage is numbered
3_16.md/ item 16 in the README index. It will need renumberingdepending on merge order.