Uploading Files to S3 in PHP
Last updated December 02, 2022
Table of Contents
If your application needs to receive files uploaded by users, you need to make sure these uploads are stored in a central and durable location.
With Heroku’s ephemeral filesystem, any information written to a dyno’s filesystem will be lost when the dyno is restarted. Instead, Heroku recommends backing services. For file and media storage, Amazon’s Simple Storage Service (S3) is a great solution.
This article demonstrates how to set up a PHP application to use S3 for storing file uploads.
S3 setup
To follow this example, we’re assuming that you already have an Amazon Web Services (AWS) account set up, and you’re in possession of an AWS Access Key pair (access key ID and secret access key), see How do I Get Security Credentials? in the AWS documentation.
You’ll also need to create a new S3 bucket using your account, or re-use an existing one.
Read our Using AWS S3 to Store Static Assets and File Uploads guide for more information; it contains detailed background knowledge, step-by-step instructions for getting started with S3, and useful tips and tricks.
Application setup
If you haven’t created a Heroku application and Git repository yet, do so first:
$ mkdir heroku-s3-example
$ cd heroku-s3-example
$ git init
$ heroku create
Using the AWS SDK for PHP
The easiest way to install the AWS SDK for PHP is using Composer. The composer require
command is the easiest way (alternatively, you can add the aws/aws-sdk-php
package to composer.json
by hand):
$ composer require aws/aws-sdk-php:~3.0
If you haven’t already done so, now is a good time to add the vendor/
directory to your .gitignore
; you only want your composer.json
and composer.lock
, but not the vendor/
directory, under version control:
$ echo "vendor" >> .gitignore
Now you can commit everything:
$ git add composer.* .gitignore
$ git commit -m "use aws/aws-sdk-php"
Setting application config vars
Hard-coding database connection information, security credentials, or other runtime settings is not a good idea. Instead, you should store such information in an application’s environment and read it from there at runtime.
Any config var you set on your Heroku application will be available at runtime using getenv()
or in $_ENV
/$_SERVER
. We will use this mechanism to dynamically read the AWS security keys (key ID and secret key) and the S3 bucket name in our code; all you need to do is set these three bits information (the code below uses “aaa”, “bbb” and “ccc” placeholders) as config vars using the Heroku CLI:
$ heroku config:set AWS_ACCESS_KEY_ID=aaa AWS_SECRET_ACCESS_KEY=bbb S3_BUCKET=ccc
All that’s missing now is some code to handle a file upload!
Handling file uploads
Next, we’ll build a very simple script that accepts a file to upload in the browser, and stores it on S3 under the same name it had on the client’s computer.
This is a very simple example without proper validation and using verbatim file names from the client. Make sure you always validate the file’s name and contents e.g. using fileinfo, and generate a custom filename or use another method of ensuring you’re not overwriting an existing file. You may also wish to put file type and size limits in place.
Put the following into a file named index.php
:
<?php
require('vendor/autoload.php');
// this will simply read AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY from env vars
$s3 = new Aws\S3\S3Client([
'version' => '2006-03-01',
'region' => 'us-east-1',
]);
$bucket = getenv('S3_BUCKET')?: die('No "S3_BUCKET" config var in found in env!');
?>
<html>
<head><meta charset="UTF-8"></head>
<body>
<h1>S3 upload example</h1>
<?php
if($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_FILES['userfile']) && $_FILES['userfile']['error'] == UPLOAD_ERR_OK && is_uploaded_file($_FILES['userfile']['tmp_name'])) {
// FIXME: you should add more of your own validation here, e.g. using ext/fileinfo
try {
// FIXME: you should not use 'name' for the upload, since that's the original filename from the user's computer - generate a random filename that you then store in your database, or similar
$upload = $s3->upload($bucket, $_FILES['userfile']['name'], fopen($_FILES['userfile']['tmp_name'], 'rb'), 'public-read');
?>
<p>Upload <a href="<?=htmlspecialchars($upload->get('ObjectURL'))?>">successful</a> :)</p>
<?php } catch(Exception $e) { ?>
<p>Upload error :(</p>
<?php } } ?>
<h2>Upload a file</h2>
<form enctype="multipart/form-data" action="<?=$_SERVER['PHP_SELF']?>" method="POST">
<input name="userfile" type="file"><input type="submit" value="Upload">
</form>
</body>
</html>
The example sets the ACL for the uploaded file so it’s publicly readable. If you want different permissions, adjust the example accordingly. You can also use a bucket policy to define what the default permissions for uploaded files in a bucket should be.
You can now add, commit and push this to Heroku:
$ git add index.php
$ git commit -m "file upload test form"
$ git push heroku master
After the deploy has finished, you can run heroku open
or manually point your browser to your application to test it. Select a file from your computer (e.g. an image or text file) and upload it. If all went well, a success message will appear with a link to the uploaded file.