Skip to content

Validate the File constraint in the browser - #18

Merged
66Ton99 merged 1 commit into
mainfrom
feat/issue-116-file-constraint
Aug 24, 2026
Merged

66Ton99 merged 1 commit into
mainfrom
feat/issue-116-file-constraint

Conversation

@66Ton99

@66Ton99 66Ton99 commented Aug 22, 2026

Copy link
Copy Markdown

Fixes formapro/JsFormValidatorBundle#116.

What was broken

@Assert\File had no JavaScript counterpart. src/Resources/public/js/constraints/
contained no File.js and index.js never imported one, so
SvarohJsFormValidator.parseConstraints() — which instantiates window[className] —
silently dropped the constraint. NotBlank on a file input worked, but maxSize,
mimeTypes, extensions and filenameMaxLength were only reported after a full
upload round trip. The maintainer confirmed on the issue that it was not implemented.

Two further reasons it could not just work:

  • getSpecifiedElementTypeValue() read domNode.value for every input, which for a
    file input is only the fake path (C:\fakepath\avatar.png), never the files.
  • Symfony\Component\Validator\Constraints\File::$maxSize is a protected property
    behind __get(). JsModelAbstract::phpValueToJs() exports constraints with
    get_object_vars(), which sees public properties only, so the limit never reached the
    browser at all. Every other option (binaryFormat, mimeTypes, extensions,
    filenameMaxLength, filenameCountUnit, all the messages) is public and was already
    exported generically.

What changed

  • src/Resources/public/js/constraints/File.js (new). Checks what the File API can
    see, in Symfony's order and with Symfony's early returns: filename length →
    empty file → maxSize → extensions → mimeTypes. factorizeSizes() is ported from
    FileValidator (same k/M/Ki/Mi factors, same 2-decimal reduction, same
    bytes/kB/MB/KiB/MiB suffices), so maxSizeMessage renders
    {{ size }}/{{ limit }}/{{ suffix }} exactly as the server does.
    mimeTypesMessage, extensionsMessage, disallowEmptyMessage and the plural
    filenameTooLongMessage get their placeholders too. filenameCountUnit is honoured
    for bytes (UTF-8 length), codepoints and graphemes (Intl.Segmenter, falling
    back to code points).
  • index.js / globals.test.js — register the constraint the way the others are.
  • SvarohJsFormValidator.js — getSpecifiedElementTypeValue() now returns the
    files list of a file input via the new getFileListValue(). An empty selection is an
    empty array, so isValueEmty() keeps NotBlank working; without the File API it falls
    back to the previous string value.
  • src/Factory/JsFormValidatorFactory.php — parseConstraints() exports a File
    constraint as its public options plus the normalized maxSize, keeping
    get_class($item) as the key so the JavaScript constructor name is unchanged.
  • Docs — new src/Resources/doc/3_16.md plus a README index entry.

Deliberately not covered

  • Image. Its own options (maxWidth, minHeight, maxRatio, …) need the file
    decoded into an Image object, which is asynchronous. The library has an async path
    (the ajax queue used by UniqueEntity), but it is built around a request/response
    round trip rather than a load event, and wiring image decoding into it is a separate
    piece of work. Rather than half-implement both, Image is left server-side only and
    documented as such; an Image constraint is skipped in the browser exactly as before.
  • Mime type when the browser reports none. File.type is sniffed and can be empty or
    wrong. An empty type skips the mime check rather than rejecting the file, so the server
    keeps the last word.
  • extensions narrowing the accepted mime types. That needs the symfony/mime media
    type database; only the extension itself is checked client-side, which can only be more
    permissive than the server, never less.
  • filenameCharset, notFoundMessage, notReadableMessage and the upload*ErrorMessage
    family describe conditions that only exist server-side.

Tests

  • File.test.js — 36 cases: the four size/suffix factorizations (including the two
    reduction 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.Segmenter fallback,
    singular/plural filename message, multiple uploads, a real File object, and values
    that carry no file information. 100% statements / 96% branches on the new file.
  • SvarohJsFormValidator.test.js — a case for the file input value: the files list, the
    empty list still being an empty value for NotBlank, and the fake-path fallback.
  • JsFormValidatorFactoryTest::testFileConstraintExportsItsNormalizedMaxSize — asserts
    'maxSize':2097152 and 'binaryFormat':true reach the generated model for
    new Assert\File(maxSize: '2Mi', …). It errors on main because the constraint is
    still exported as an object without the option.

composer test 43 → 44 tests, 151 → 156 assertions. npx jest 34 → 35 suites,
347 → 385 tests. composer phpstan clean, git diff --check clean. No Cypress change:
the browser behaviour is fully covered by the Jest cases.

Note for the merger

src/Resources/doc/3_15.md is taken by two other open branches
(fix/issue-120-notblank-whitespace, fix/issue-148-constraint-property-path), so this
page is numbered 3_16.md / item 16 in the README index. It will need renumbering
depending on merge order.

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
66Ton99 force-pushed the feat/issue-116-file-constraint branch from 76c4de2 to a464e6a Compare August 24, 2026 19:35
@66Ton99
66Ton99 merged commit f87beef into main Aug 24, 2026
6 checks passed
@66Ton99
66Ton99 deleted the feat/issue-116-file-constraint branch August 26, 2026 06:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Question: how validate File?

1 participant