File Storage
By the end of this lesson you'll handle file uploads the safe way — validate them, store them where they can't bite you, serve them through a controller, and abstract storage so the same code runs on local disk or Amazon S3.
Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.
Part of the free Php course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
1️⃣ The Upload Form
A file upload starts in the browser with an HTML form. Two attributes are mandatory: method="post" (files are far too big for a URL) and enctype="multipart/form-data" (this tells the browser to send the raw file bytes, not just text). The name on the file input — here photo — becomes the key your PHP reads on the server.
2️⃣ Reading $_FILES
When the form submits, PHP collects the upload into the $_FILES superglobal — a built-in array PHP fills in for you. Each file input gives you an array with five keys. The crucial thing to learn here: only two of them come from PHP and can be trusted ( tmp_name , size , error ). The name and type come straight from the browser and a user can fake them.
tmp_name is a temporary file PHP created in /tmp ; it is deleted automatically when the script ends, so you must move it somewhere permanent if you want to keep it. An error of 0 (the constant UPLOAD_ERR_OK ) means the upload arrived cleanly.
3️⃣ Validate Everything
This is the most important section in the lesson. Never trust a single thing the browser tells you. Validate in three steps: confirm the upload succeeded, cap the real size (which PHP measured), and detect the true file type by reading the bytes with the finfo class — never $file["type"] . A whitelist of allowed types is far safer than a blacklist, because you can never list every dangerous extension.
Your turn. The check below is almost complete — fill in each ___ using the 👉 hint, then run it and compare with the Output panel.
4️⃣ Store It Safely
A validated file is still sitting in /tmp , about to be deleted. Move it with move_uploaded_file() — the only mover that first verifies the file truly came from an HTTP upload ( rename() and copy() don't, and that's exploitable). Two rules make storage safe: generate your own unguessable filename (never reuse the user's, which can contain ../ or collide), and store outside the web root so the file can't be requested or executed by URL.
Now build the safe filename yourself. Fill in the blanks so the name is 32 hex characters plus a dot and extension.
5️⃣ Serving Files Through PHP
Because your files live outside the web root, no one can link to them directly — which is exactly what you want. To let the right people see them, write a small controller : a PHP script that takes a file id, whitelists the characters it accepts (this single regex defeats path traversal), checks the visitor is allowed, then streams the bytes. Setting the Content-Type from the detected MIME stops the browser guessing.
6️⃣ Abstracting Storage (Local vs S3)
Local disk is fine for one server, but the moment you run two servers — or deploy to hosting that wipes the disk — uploads need to live in cloud object storage like Amazon S3 . The trick is to never hard-code which one. The Flysystem library gives every backend the same methods ( write , read , delete ), so you write your app once and switch from local to S3 by changing a single line — the adapter . That's an abstraction: one stable interface hiding many different implementations.
📋 Quick Reference — File Uploads & Storage
No code is filled in this time — just a brief and an outline. Write the function yourself, run it on onecompiler.com/php with the test data, then check your result against the expected output in the comments. This write-run-check loop is exactly what you'll do on every real upload handler.
Practice quiz
Which keys of $_FILES['photo'] come from PHP and can be trusted?
- name and type
- type and size only
- tmp_name, size, and error
- name, type, and tmp_name
Answer: tmp_name, size, and error. tmp_name, size, and error are set by PHP; name and type come straight from the browser and a user can fake them.
Why must you NOT trust $_FILES['photo']['type'] to validate a file's type?
- It is sent by the browser and can be faked, so a script renamed to dog.jpg can claim image/jpeg
- It is always empty
- PHP encrypts it
- It only works for images
Answer: It is sent by the browser and can be faked, so a script renamed to dog.jpg can claim image/jpeg. The browser-supplied type is untrusted; detect the real type from the file's bytes with the finfo class.
How do you detect the REAL MIME type of an uploaded file?
- Read the file extension from the name
- type
finfo reads the file's content, so a renamed script is caught regardless of its claimed type or extension.
What is the correct order to validate an upload?
- MIME, then size, then error
- error, then size, then real MIME
- name, then type, then size
- size, then name, then error
Answer: error, then size, then real MIME. First confirm error is UPLOAD_ERR_OK, then cap the size, then detect the true MIME with a whitelist.
Which function is the ONLY safe way to move an uploaded file out of /tmp?
- move_uploaded_file()
- rename()
- copy()
- file_put_contents()
Answer: move_uploaded_file(). move_uploaded_file() first confirms the file genuinely came from an HTTP upload; rename() and copy() do not and are exploitable.
How should you generate the stored filename for an upload?
- Reuse the user's original filename
- Use the current timestamp only
- Generate your own unguessable name, e.g. bin2hex(random_bytes(16)) plus a whitelisted extension
- Use the file's MIME type as the name
Answer: Generate your own unguessable name, e.g. bin2hex(random_bytes(16)) plus a whitelisted extension. Never reuse the user's name (it can contain ../ or collide). Build a safe, random, collision-proof name instead.
Why should uploads be stored OUTSIDE the web root (e.g. /var/www/storage, not /public)?
- To save disk space
- So files can't be requested or executed directly by URL — only a PHP controller you wrote can serve them
- Because PHP can't read files inside /public
- To make uploads load faster
Answer: So files can't be requested or executed directly by URL — only a PHP controller you wrote can serve them. Outside the web root, the only way to reach a file is through a controller that can check login and ownership first.
What is path traversal, and how does the serve controller defend against it?
- Sending too many requests; the controller rate-limits them
- Uploading a file that is too large; the controller checks the size
- Guessing a password; the controller hashes it
- Using '../' in an id to escape the storage folder; the controller whitelists the id with a strict regex
Answer: Using '../' in an id to escape the storage folder; the controller whitelists the id with a strict regex. A strict pattern like /^[a-f0-9]{32}\.(jpg|png|webp)$/ rejects any value containing '/' or '..' before a file is touched.
What does the Flysystem library give you?
- Automatic image resizing
- One identical API (write/read/delete) across backends, so you swap local disk for S3 with a one-line adapter change
- Built-in virus scanning of uploads
- Faster database queries
Answer: One identical API (write/read/delete) across backends, so you swap local disk for S3 with a one-line adapter change. Flysystem abstracts storage: the same app code runs against local disk or S3 by changing only the adapter — no lock-in.
What does an error value of UPLOAD_ERR_OK (0) on $_FILES indicate?
- The file is an image
- The file was rejected
- The upload arrived cleanly with no error
- The file exceeds the size limit
Answer: The upload arrived cleanly with no error. error === UPLOAD_ERR_OK (0) means the upload completed successfully; any other value signals a failure.