w3resource

Laravel (5.7) File Storage

File uploads is one the most commonly used features on the web. From uploading avatars to family pictures to sending documents via email, we can't do without files on the web. Handling of files is another thing Laravel has simplified in its ecosystem. Before we get started, we’ll need a few things. First, let’s create a Laravel project.

composer create-project --prefer-dist laravel/laravel files

Where files are the name of our project. After installing the app, we'll need a few packages installed, so, let's get them out of the way. You should note that these packages are only necessary if you intend to save images to Amazon's s3 or manipulate images like cropping, filters etc.

composer require league/flysystem-aws-s3-v3:~1.0 intervention/image:~2.4

After installing the dependencies, the final one is Mailtrap. Mailtrap is a fake SMTP server for development teams to test, view and share emails sent from the development and staging environments without spamming real customers. So head over to Mailtrap and create a new inbox for testing.

Then, in welcome.blade.php update the head tag to:

<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>File uploads</title>
<style>
  * {
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
        "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji",
        "Segoe UI Emoji", "Segoe UI Symbol";
  }
</style>
Modify the body contents to:
<form action="/process" enctype="multipart/form-data" method="POST">
    <p>
        <label for="photo">
            <input type="file" name="photo" id="photo">
        </label>
    </p>
    <button>Upload</button>
    {{ csrf_field() }}
</form>

For the file upload form, the enctype="multipart/form-data" and method="POST" are extremely important as the browser will know how to properly format the request. {{ csrf_field() }} is Laravel specific and will generate a hidden input field with a token that Laravel can use to verify the form submission is legit.

If the CSRF token does not exist on the page, Laravel will show "The page has expired due to inactivity" page.

Laravel The page has expired due to inactivity

Now that we have our dependencies out of the way, let's get started.

Understanding How Laravel Handles Files

Development as we know it in 2018 is growing fast, and in most cases there are many solutions to one problem. Take file hosting for example, now we have so many options to store files, the sheer number of solutions ranging from self hosted to FTP to cloud storage to GFS and many others.

Since Laravel is a framework that encourages flexibility, it has a native way to handle the many file structures. Be it local, Amazon's s3, Google's Cloud, Laravel has you covered.

Laravel's solution to this problem is to call them disks. Makes sense, any file storage system you can think of can be labeled as a disk in Laravel. To this regard, Laravel comes with native support for some providers (disks). We have:

local, public, s3, rackspace, FTP etc. All this is possible because of Flysystem.

If you open config/filesystems.php you'll see the available disks and their respected configuration.

File Uploads in Laravel

From the introduction section above, we have a form with a file input ready to be processed. We can see that the form is pointed to /process. In routes/web.php, we define a new POST /process route.

use Illuminate\Http\Request;

Route::post('process', function (Request $request) {
    $path = $request->file('photo')->store('photos');

    dd($path);
});

What the above code does is grab the photo field from the request and save it to the photos folder. dd() is a Laravel function that kills the running script and dumps the argument to the page. For me, the file was saved to "photos/3hcX8yrOs2NYhpadt4Eacq4TFtpVYUCw6VTRJhfn.png". To find this file on the file system, navigate to storage/app and you'll find the uploaded file.

If you don't like the default naming pattern provided by Laravel, you can provide yours using the store As method.

Route::post('process', function (Request $request) {
    // cache the file
    $file = $request->file('photo');

    // generate a new filename. getClientOriginalExtension() for the file extension
    $filename = 'profile-photo-' . time() . '.' . $file->getClientOriginalExtension();

    // save to storage/app/photos as the new $filename
    $path = $file->storeAs('photos', $filename);

    dd($path);
});

After running the above code, I got "photos/profile-photo-1517311378.png".

Difference Between Local and Public Disks

In config/filesystems.php you can see the disks local and public defined. By default, Laravel uses the local disk configuration. The major difference between local and public disk is that local is private and cannot be accessed from the browser while public can be accessed from the browser.

Since the public disk is in storage/app/public and Laravel's server root is in public you need to link storage/app/public to Laravel's public folder. We can do that with our trusty artisan by running php artisan storage:link.

Uploading Multiple Files

Since Laravel doesn't provide a function to upload multiple files, we need to do that ourselves. It's not much different from what we've been doing so far, we just need a loop.

First, let's update our file upload input to accept multiple files.

<input type="file" name="photos[]" id="photo" multiple>

When we try to process this $request->file('photos'), it's now an array of UploadedFile instances so we need to loop through the array and save each file.

Route::post('process', function (Request $request) {
    $photos = $request->file('photos');
    $paths  = [];

    foreach ($photos as $photo) {
        $extension = $photo->getClientOriginalExtension();
        $filename  = 'profile-photo-' . time() . '.' . $extension;
        $paths[]   = $photo->storeAs('photos', $filename);
    }

    dd($paths);
});

After running this, I got the following array, since I uploaded a GIF and a PNG:

array:2 [?
  0 => "photos/profile-photo-1517315875.gif"
  1 => "photos/profile-photo-1517315875.png"
]

Validating File Uploads

Validation for file uploads is extremely important. Apart from preventing users from uploading the wrong file types, it's also for security. Let me give an example regarding security. There's a PHP configuration option cgi.fix_pathinfo=1. What this does is when it encounters a file like

https://site.com/images/evil.jpg/nonexistent.php, PHP will assume nonexistent.php is a PHP file and it will try to run it. When it discovers that nonexistent.php doesn't exist, PHP will be like "I need to fix this ASAP" and try to execute evil.jpg (a PHP file disguised as a JPEG). Because evil.jpg wasn't validated when it was uploaded, a hacker now has a script they can freely run live on your server… Not… good.

To validate files in Laravel, there are so many ways, but let’s stick to controller validation.

Route::post('process', function (Request $request) {
    // validate the uploaded file
    $validation = $request->validate([
        'photo' => 'required|file|image|mimes:jpeg,png,gif,webp|max:2048'
        // for multiple file uploads
        // 'photo.*' => 'required|file|image|mimes:jpeg,png,gif,webp|max:2048'
    ]);
    $file      = $validation['photo']; // get the validated file
    $extension = $file->getClientOriginalExtension();
    $filename  = 'profile-photo-' . time() . '.' . $extension;
    $path      = $file->storeAs('photos', $filename);

    dd($path);
});

For the above snippet, we told Laravel to make sure the field with a name of the photo is required, a successfully uploaded file, it's an image, it has one of the defined mime types, and it's a max of 2048 kilobytes ~~ 2 megabytes.

Now, when a malicious user uploads a disguised file, the file will fail validation and if for some weird reason you leave cgi.fix_pathinfo on, this is not a means by which you can get PWNED!!!

If you head over to Laravel's validation page you'll see a whole bunch of validation rules.

Moving Files to the Cloud

Okay, your site is now an adult, it has many visitors and you decide it's time to move to the cloud. Or maybe from the beginning, you decided your files will live on separate server. The good news is Laravel comes with support for many cloud providers, but, for this tutorial, let's stick with Amazon.

Earlier we installed league/flysystem-aws-s3-v3 through composer. Laravel will automatically look for it if you choose to use Amazon S3 or throw an exception.

To upload files to the cloud, just use:

$request->file('photo')->store('photos', 's3');

For multiple file uploads:

foreach ($photos as $photo) {
    $extension = $photo->getClientOriginalExtension();
    $filename  = 'profile-photo-' . time() . '.' . $extension;
    $paths[]   = $photo->storeAs('photos', $filename, 's3');
}

Note: you'll have to configure your Amazon s3 credentials in config/filesystems.php**.**

Sending Files as Email Attachments

Before we do this, let's quickly configure our mail environment. In .env file you will see this section

MAIL_DRIVER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null

We need a username and password which we can get at Mailtrap.io. Mailtrap is really good for testing emails during development as you don't have to crowd your email with spam. You can also share inboxes with team members or create separate inboxes.

First, create an account and login:

  1. Create a new inbox
  2. Click to open the inbox
  3. Copy username and password under SMTP section

After copying credentials, we can modify .env to:

MAIL_DRIVER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=8a1d546090493b
MAIL_PASSWORD=328dd2af5aefc3
MAIL_ENCRYPTION=null

Don't bother using mine, I deleted it.

Create your mailable

php artisan make:mail FileDownloaded

Then, edit its build method and change it to:

public function build()
{
    return $this->from('[email protected]')
        ->view('emails.files_downloaded')
        ->attach(storage_path('app/file.txt'), [
            'as' => 'secret.txt'
        ]);
}

As you can see from the method above, we pass the absolute file path to the attach() method and pass an optional array where we can change the name of the attachment or even add custom headers. Next we need to create our email view.

Create a new view file in resources/views/emails/files_downloaded.blade.php and place the content below.

<h1>Only you can stop forest fires</h1>
<p>Lorem, ipsum dolor sit amet consectetur adipisicing elit. Labore at reiciendis consequatur, ea culpa molestiae ad minima est quibusdam ducimus laboriosam dolorem, quasi sequi! Atque dolore ullam nisi accusantium.Tenetur!</p>

Now, in routes/web.php we can create a new route and trigger a mail when we visit it.

use App\Mail\FileDownloaded;
Route::get('mail', function () {
    $email = '[email protected]';

    Mail::to($email)->send(new FileDownloaded);

    dd('done');
});

If you head over to Mailtrap, you should see this.

Laravel Mailtrap

Storage Facade for When Files Already Exist

In an application, it's not every time we process files through uploads. Sometimes, we decide to defer cloud file uploads till a certain user action is complete. Other times we have some files on disk before switching to a cloud provider.

For times like this, Laravel provides a convenient Storage facade. For those who don't know, facades in Laravel are class aliases. So instead of doing something like

Symfony\File\Whatever\Long\Namespace\UploadedFile, we can do Storage instead.

Choosing a disk to upload file. If no disk is specified, Laravel looks in config/filesystems.php and use the default disk.

Storage::disk('local')->exists('file.txt');

use default cloud provider

// Storage::disk('cloud')->exists('file.txt'); will not work so do:
Storage::cloud()->exists('file.txt');

Create a new file with contents

Storage::put('file.txt', 'Contents');

Prepend to file

Storage::prepend('file.txt', 'Prepended Text');

Append to file

Storage::append('file.txt', 'Prepended Text');

Get file contents

Storage::get('file.txt')

Check if file exists

Storage::exists('file.txt')

Force file download

Storage::download('file.txt', $name, $headers); // $name and $headers are optional

Generate publicly accessible URL

Storage::url('file.txt');

Generate a temporary public URL (i.e files that won't exists after a set time). This will only work for cloud providers as Laravel doesn't yet know how to handle generation of temporary URLs for local disk.

Storage::temporaryUrl('file.txt', now()->addMinutes(10));

Get file size

Storage::size('file.txt');

Last modified date

Storage::lastModified('file.txt')

Copy files

Storage::copy('file.txt', 'shared/file.txt');

Move files

Storage::move('file.txt', 'secret/file.txt');

Delete files

Storage::delete('file.txt');
// to delete multiple files
Storage::delete(['file1.txt', 'file2.txt']);

Manipulating files

Resizing images, adding filters etc. This is where Laravel needs external help. Adding this feature natively to Laravel will only bloat the application since not installs need it. We need a package called intervention/image. We already installed this package, but for reference.

composer require intervention/image

Since Laravel can automatically detect packages, we don't need to register anything.

To resize an image

$image = Image::make(storage_path('app/public/profile.jpg'))->resize(300, 200);

Don't forget directories

Laravel also provides handy helpers to work with directories. They are all based on PHP iterators so they'll provide the utmost performance.

To get all files:

Storage::files

To get all files in a directory including files in sub-folders

Storage::allFiles($directory_name);

To get all directories within a directory

Storage::directories($directory_name);

To get all directories within a directory including files in sub-directories

Storage::allDirectories($directory_name);

Make a directory

Storage::makeDirectory($directory_name);

Delete a directory

Storage::deleteDirectory($directory_name);

Previous: Laravel (5.7) Events
Next: Laravel (5.7) Mail



Follow us on Facebook and Twitter for latest update.