skip to content

Uploaded Files to Disks

$request->file() returns an UploadedFile you check with isValid() and save with store() or storeAs() under a hashed name. Interviewers ask why the client's name and extension are untrusted.

on this pageshow

explore

questions

5

In a Laravel controller, how do you read an uploaded profile photo and save it to a disk with store()?

level: juniorimportance: must knowfreq 68%

answer

  1. UploadedFile from $request->file()
  2. directory only, name is generated
  3. returns the path relative to the disk root
  4. second argument picks the disk
  5. FILESYSTEM_DISK defaults to local

basics

~10 s

$request->file('photo') returns an Illuminate\Http\UploadedFile; calling ->store('avatars') writes it to the default disk under a random 40-character name with a content-based extension and returns the relative path, or false if the write fails.

solid answer

~40 s

`$request->file('photo')` (or `$request->photo`) returns an `Illuminate\Http\UploadedFile`, or `null` when no file was sent, and `hasFile('photo')` tells you whether a usable upload is there. `->store('avatars')` takes a **directory**, not a filename: it generates a name with `hashName()` (40 random characters plus the extension guessed from the file's contents), streams the file onto the default disk and returns the path relative to that disk's root, such as `avatars/Xy3....jpg`, which you save on the user row. Pass a disk as the second argument, `store('avatars', 'public')`, or an options array. The skeleton's default is `FILESYSTEM_DISK=local`, rooted at `storage/app/private`. With the skeleton's `'throw' => false`, a failed write returns `false` instead of throwing.

code

php · 22 lines
php
<?php

use Illuminate\Http\Request;

public function updatePhoto(Request $request)
{
    // validation of type and size runs first (not shown)

    if (! $request->hasFile('photo')) {
        return back()->withErrors(['photo' => 'Choose a photo to upload.']);
    }

    $path = $request->file('photo')->store('avatars', 'public'); // e.g. avatars/Qm9...x2.jpg

    if ($path === false) {
        return back()->withErrors(['photo' => 'Could not save the photo.']);
    }

    $request->user()->update(['photo_path' => $path]);

    return back();
}

go deeper

for a junior

Recall the chain: file('photo') gives an UploadedFile, store('avatars') saves it under a random name and returns the path you keep in the database.

for a middle

Explain hashName(), the disk argument, the relative return value, and why the skeleton's local disk under storage/app/private is not web-accessible.

for a senior

Treat the return value as fallible, configure throw on disks where silent failure is unacceptable, and keep validation strictly before storage.

for a principal

Decide where user media lives, local disk or object storage, and how paths stored in the database survive a change of disk.

## From multipart request to UploadedFile When a browser submits a form with `enctype="multipart/form-data"` and an `<input type="file" name="photo">`, PHP writes the file to a temporary location and Laravel wraps each entry in an **`Illuminate\Http\UploadedFile`**. That class extends Symfony's `UploadedFile`, which in turn extends PHP's `SplFileInfo`, and adds Laravel's storage helpers. On a dating app's "edit profile" screen, the controller reads the photo like this: - `$request->file('photo')` returns the `UploadedFile`, or `null` when the user chose no file. Dot paths work, so `file('photos.0')` reaches the first of several. - `$request->photo` does the same through the request's dynamic properties. - `$request->hasFile('photo')` returns `true` only when there is a file object with a real temporary path, so it is the quick "did a usable upload arrive?" check. - `$request->allFiles()` returns every uploaded file keyed by input name. ## What store() does `store($path = '', $options = [])` is a thin wrapper: 1. It computes a file name with **`hashName()`**: 40 random characters from `Str::random(40)` plus a dot and the extension from `guessExtension()`, which inspects the file's contents rather than trusting the client. 2. It calls `storeAs($path, $thatName, $options)`. 3. `storeAs()` resolves the disk from the filesystem manager and calls the disk's `putFileAs()`, which opens the temporary file as a **stream** and writes it, so large photos are not loaded into memory. 4. It returns the stored path, `trim($path.'/'.$name, '/')`, relative to the disk root, or `false` if the write failed. | Call | Directory | File name | Disk | |---|---|---|---| | `store('avatars')` | `avatars` | random 40 chars + guessed extension | default disk | | `store('avatars', 'public')` | `avatars` | random | `public` | | `store('avatars', ['disk' => 's3'])` | `avatars` | random | `s3` | | `storeAs('avatars', "{$user->id}.jpg")` | `avatars` | the name you pass | default disk | The second argument may be a string (the disk name) or an options array; a string is turned into `['disk' => ...]`. ## Where the file lands in a fresh Laravel 13 app The skeleton's `config/filesystems.php` reads `FILESYSTEM_DISK` with a default of `local`, and the `local` disk's root is `storage_path('app/private')`. So `store('avatars')` in a new app writes to `storage/app/private/avatars/...`, a directory that is **not** under `public/` and is not directly reachable by URL. Profile photos that browsers should load are usually written to the `public` disk (rooted at `storage/app/public`) or a cloud disk. Configuring disks and the public symlink is a separate topic; for the upload itself, the point is to pass the disk you mean. ## Saving the path and handling failure Store the **returned path**, not an absolute filesystem path and not the client's file name: - the path stays valid if you move the app, because the disk root is configuration; - the random name avoids collisions between two users who both upload `IMG_0001.jpg`; - the name leaks nothing the user typed. Every disk in the skeleton sets `'throw' => false` and `'report' => false`, so a failed write returns `false` silently. Check the result, or set `'throw' => true` on the disk so failures raise an exception you will notice: ```php $path = $request->file('photo')->store('avatars', 'public'); if ($path === false) { return back()->withErrors(['photo' => 'Could not save the photo.']); } ``` ## Before you call store() Storing is not validating. The size, type and dimensions rules for an image belong in validation, which should run first so that a PDF renamed to `.jpg`, or a 40 MB file, never reaches the disk. Deleting the user's previous photo, generating thumbnails and serving the file are follow-up steps with their own APIs. ## Common mistakes interviewers listen for 1. **Passing a file name to `store()`.** `store('avatars/me.jpg')` treats the whole string as a directory and creates `avatars/me.jpg/<random>.jpg`. 2. **Saving `getClientOriginalName()` as the stored name.** It is client-controlled and collides across users; `store()` avoids it by design. 3. **Assuming `$request->input('photo')` returns the file.** `input()` reads fields only; files come from `file()`, `allFiles()` or `all()`. 4. **Forgetting `enctype="multipart/form-data"`** on the form. Without it the browser sends only the file name as text, and `file('photo')` is `null`. 5. **Storing the absolute path.** Moving the app or switching the disk root then breaks every saved reference.

  • Why does store() take a directory rather than a full file path?
    Because it names the file for you with `hashName()`: 40 random characters plus an extension guessed from the file's contents. Passing `'avatars/me.jpg'` would create a directory called `me.jpg` with a random file inside. When you need a specific name, call `storeAs('avatars', 'me.jpg')` instead.
  • Where does store('avatars') write in a fresh Laravel 13 app, and can a browser load that file?
    The skeleton's default disk is `local` (`FILESYSTEM_DISK=local`), rooted at `storage/app/private`, so the file lands in `storage/app/private/avatars`. That directory is outside `public/`, so there is no direct URL to it. Photos meant for browsers usually go to the `public` disk or a cloud disk.

saying these in an interview costs you the question

  • store('avatars/me.jpg') saves the file as me.jpg
  • store() returns the absolute path on the server
  • store() keeps the client's original file name
  • A failed store() always throws an exception
  • The default disk in a new app is public
open as a page

In Laravel's UploadedFile, why prefer hashName() and extension() over getClientOriginalName() and clientExtension() when saving a photo?

level: middleimportance: must knowfreq 55%

basics

~20 s

getClientOriginalName(), getClientOriginalExtension() and clientExtension() come from what the client sent and can be forged; hashName() is a random 40-character name and extension() is guessed from the file's actual contents, so they reflect the server's view, not the client's claim.

open as a page

In Laravel, what is the difference between $request->hasFile('photo') and $request->file('photo')->isValid() for an upload?

level: middleimportance: should knowfreq 40%

basics

~20 s

hasFile() asks whether the request holds a file object with a real temporary path; isValid() asks whether that particular upload finished with no error and came through PHP's upload mechanism. For multiple files, hasFile() is true if any one qualifies.

open as a page

In Laravel, what does storePublicly() change compared with store() on an UploadedFile, and what does it not change?

level: middleimportance: should knowfreq 32%

basics

~20 s

storePublicly() is store() with the visibility option forced to public: same generated name, same disk rules. It changes permissions or object access on the disk, but it does not make a file on a private local disk reachable by URL.

open as a page

A Laravel dating app rejects large profile photos, sometimes with a 413 PostTooLargeException and sometimes with 'The photo failed to upload.'; why two symptoms?

level: seniorimportance: should knowfreq 45%

basics

~20 s

A body over post_max_size is stopped by the global ValidatePostSize middleware with a 413 PostTooLargeException. A file over upload_max_filesize but inside post_max_size reaches the controller as an invalid UploadedFile, and validation reports it as failed to upload.

open as a page