Sniff the type from the bytes, never the filename or Content-Type; store outside the document root and serve through a controller; and add the path to the nginx list or it will 404 only in production.

Use $request->file('field') for one and $request->files('field') for a multiple picker — the second always returns a list, whether one file arrived or forty.

The rules, all learned the hard way:

  • Sniff the type from the file's own bytes. The filename and the Content-Type are both supplied by whoever is uploading.
  • Re-encode images rather than storing what arrived. A file that parses as a JPEG can carry anything in the parts a decoder skips.
  • Never accept SVG without a sanitiser. It is a document that can carry script, served from your own origin.
  • Check pixel dimensions before decoding. A 30,000 × 30,000 PNG is a few hundred kilobytes and needs gigabytes to decode.
  • Store under content/, outside the document root, and serve through a controller.

🚨 Add your path to tools/deploy/nginx-paths-served-by-php.conf. Uploads served through PHP still have addresses ending in .png, and a typical nginx rule serves those from disk without reaching the application. It works perfectly locally and 404s in production.

Str::slug(), Html::toText() and the CoverStore / AvatarStore / ImageStore classes are worth reading before writing your own.

Sign in to say whether this helped.