In this tutorial, you will learn how to handle file uploads with validation in Laravel.

Almost all applications have to do with file upload of some sort: a profile picture, a document for KYC verification, a document containing a CV, etc.

Hence, it is important to understand how to handle file uploads in Laravel.

In this tutorial, we are going to build a simple user profile form, where the user will need to fill in his details and these details will include submitting a profile picture.

At the end of this tutorial, you should be able to handle file uploads, handle file upload validation, and know what to do when the file doesn’t get uploaded successfully.

Handling File Uploads with validation in Laravel

To get started, Laravel needs to be installed on your system. If you have Laravel installed already, you can skip this step.

I would advise that you install Laravel globally. Especially if you plan on using it often. You can do so by running the following command on a terminal.

composer global require laravel/installer

Once that is complete, we can go ahead to create a new Laravel project for this tutorial.

laravel new profile-form

Running the above command will create a new Laravel project in the current directory called profile-form.

Serve the app by running the following:

cd profile-form
php artisan serve

Navigate to http://localhost:8000/ where you’ll find your app showing the default Laravel landing page.

Writing the Database Migration

We will need to create a users table that will store user details. Laravel ships with a migration for the users table which can be found at database\migrations\2014_10_12_000000_create_users_table.php. Modify that file to look like this:

<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateUsersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->string('photo');
            $table->rememberToken();
            $table->timestamps();
        });
    }
    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('users');
    }
}

Here, we have modified the default Laravel migration for the user’s table to include a field photo that will store the URL to the user’s photograph.

Modify the .env file to include the correct database details. Then run the migrations by running the following command:

php artisan migrate

Update the User model to reflect the addition of the photo field. Add a photo to the $fillable so it can be autofilled by Laravel when creating and updating a user.

// App\User.php

protected $fillable = [
    'name', 'email', 'password', 'photo'
];

Once that is done, we can proceed to build the front end of the profile form.

Building the Frontend

We will need to create an authentication system that will allow the user to register and login into the application.

Create a new file in resources/views called register.blade.php. Add the following to the newly created file.

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Register | Profile Form</title>
    <!-- Fonts -->
    <link href="https://fonts.googleapis.com/css?family=Nunito:200,600" rel="stylesheet">
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
    <!-- Styles -->
    <style>
      .form-box{
        display: flex;
        justify-content: center;
        flex-direction: column;
        align-items: center;
        margin-bottom: 10px
      }
    </style>
  </head>
  <body>
    <nav class="navbar navbar-expand-sm navbar-light bg-light">
      <a class="navbar-brand" href="#">Profile Form</a>
      <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
        <span class="navbar-toggler-icon"></span>
      </button>
      <div class="collapse navbar-collapse" id="navbarSupportedContent">
        <ul class="navbar-nav mr-auto">
          <li class="nav-item active">
            <a class="nav-link" href="#">Register <span class="sr-only">(current)</span></a>
          </li>
          <li class="nav-item">
            <a class="nav-link" href="#">Login</a>
          </li>
        </ul>
        <form class="form-inline my-2 my-lg-0">
          <input class="form-control mr-sm-2" type="search" placeholder="Search" aria-label="Search">
          <button class="btn btn-outline-success my-2 my-sm-0" type="submit">Search</button>
        </form>
      </div>
    </nav>
    <div class="container">
      <h1>Register</h1>
      <section class="form-box">
        <form action="#" class="col-md-5">
          <div class="form-group">
            <label for="name">Full Name</label>
            <input type="text" id="name" name="name"  class="form-control" required>
          </div>
          <div class="form-group">
            <label for="email">Email Address</label>
            <input type="email" name="email" id="email" class="form-control" required>
          </div>
          <div class="form-group">
            <label for="password">
              Password
            </label>
            <input type="password" name="password" id="password" class="form-control" required>
          </div>
          <div class="form-group">
            <label for="password_confirmation">Confirm Password</label>
            <input type="password" name="password_confirmation" id="password_confirmation" class="form-control" required>
          </div>
          <div class="form-group">
            <label for="photo">Attach a photograph</label>
            <input type="file" name="photo" id="photo" class="form-control-file" required>
          </div>
          <div class="form-group">
            <button type="submit" class="btn btn-outline-primary">Submit</button>
          </div>
        </form>
      </section>
    </div>
  </body>
</html>

We are using Bootstrap for styling, hence the need to add it to the top section of the page. We’ve made all the fields required here so the user won’t be able to submit the form without filling all the fields.

The field that is particularly important to us here is the file input field <input type="file" Update web.php to include the route to the register page

// web.php
...
Route::get('/register', function () {
    return view('register');
});

Going to http://localhost:8000/register will show the register page containing the fields that are typical of a registration form. 

File Uploads with validation in Laravel
File upload form

We need to specify the type of files that are supported. Since we want only image files, we need to specify in the input element that we want just image files. 

Modify the input:type="file" to accept only image files by adding the accept  attribute with a value of image/* to the input tag.

The photograph section should now be like this:

<div class="form-group">
  <label for="photo">Attach a photograph</label>
  <input type="file" name="photo" id="photo" accept="image/*" class="form-control-file">
</div>

While the form looks set to send images to the backend, it can not do that yet. We need to tell the form that it will need to send another type of form data to the backend, in this case, binary data.

We do this by adding the enctype attribute to the form element, and setting its value to multipart/form-data.

The opening form tag should look like this: 

<form action="#" class="col-md-5" enctype="multipart/form-data">

Next, we need to write the controller to handle the upload.

Writing the Controller

Our backend should be able to handle the validation, storing, and redirecting/authentication of the user.

Laravel ships with a RegisterController which can be found at app\Http\Controllers\Auth\RegisterController.php.

Validation

The validator method in the Register Controller is tasked with the responsibility of validating the fields. So let’s update it to reflect what we actually want.

Modify the validator method in RegisterController.php to validate all our fields.

protected function validator(array $data)
{
    return Validator::make($data, [
        'name' => ['required', 'string', 'max:255'],
        'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
        'password' => ['required', 'string', 'min:8', 'confirmed'],
        'photo' => ['required', 'image']
    ]);
}

The name field should be a required field, and a string, hence the validation rules. Similar goes for email and password.

The password field needs to be confirmed. In the front end, we added a field for password_confirmation. This is what the password field is compared with. Finally the photo field.

This field is required when registering. It is required because, in our database migration, we made it so. If we had set it to be nullable, then we wouldn’t need to set the validation to required.

Laravel also has a handy validation rule, image, which is used to validate image files (files with either of the following extensions: jpeg, png, bmp, gif, or SVG).

Some other validations that you might want to be carried out on files are 

  • Ensuring the file size is not greater or smaller than a specific amount. This rule can be added as max:500 or min:500 as required. It should be noted that the file size here is in kilobytes
  • Checking the dimensions of the image. This can be done by adding the following to the array of rules 'dimensions:min_width=100,min_height=200'.

Storing

The next step is storing the image. We will create a new function to handle this functionality.

There are several places where files can be stored in an application: they could be stored in an s3 bucket, google drive, or even in your application server.

Laravel comes with support for storing files in these locations. For simplicity, we will be storing the image file in the application.

Laravel Request class Illuminate\Http\Request provides support for file handling out of the box. The uploaded file can be stored via $request→file(<inputName>)→store(<folderToBeStored>). For our case, we could use: 

$request->file('photo')->store('profile')

This stores the file in the storage \app\profile folder. The file is saved with a random name, and the URL to the file is returned.

If we want to assign a custom name to the image as it is being stored, we need to use the storeAs method.

Using the storeAs method will look like

$request→file(<inputName>)→storeAs(<folderToBeStored>, <customName.fileExtension>)

The file extension is important, and laravel provides simple ways to get the file extension of the file that was uploaded, via the clientExtension method.

It can be gotten as shown below

$request->file('photo')->extension()

Using this method, we can save a user’s image based on the user’s name.

$fileName = $request->get('name') . '.' . $request->file('photo')->extension();        
$request->file('photo')->storeAs('profile', $fileName);

In our case, we can make do with the random names that Laravel saves in our file.

  • For your file to be accessed publicly, it needs to be stored in the storage/app/public folder.
  • Files in other folders will not be publicly available

Let’s create a function that handles the task of saving images to the storage.

Underneath the validator function, add the following

protected function storeImage(Request $request) {
  $path = $request->file('photo')->store('public/profile');
  return substr($path, strlen('public/'));
}

The storeImage function stores the image in the storage/app/public/profile folder and returns a URL to the storage location of the file.

Putting it all together

Since we are adding more things to be done, we need to override the default register method provided for us by Laravel.

Laravel provides a trait for registering users and can be found at Illuminate\Foundation\Auth\RegistersUsers.php.

Update the Register Controller to include the following:

protected function create(array $data)
{
    return User::create([
        'name' => $data['name'],
        'email' => $data['email'],
        'password' => Hash::make($data['password']),
        'photo' => $data['photo']
    ]);
}

public function register(Request $request)
{
    $this->validator($request->all())->validate();
    
    $imageUrl = $this->storeImage($request);
    
    $data = $request->all();
    $data['photo'] = $imageUrl;
    $user = $this->create($data);
    
    $this->guard()->login($user);
    
    return $this->registered($request, $user)
                    ?: redirect($this->redirectPath());
}

As seen above, the create function does the work of creating a user based on the data that is sent to it.

The register function is where everything happens. The validation is first carried out before any other thing.

If the validation is passed, we store the image. After that is done, we create the user by passing all the data that’s needed in an array, $data.

If everything happens successfully, we authenticate the user and redirect to the home page.

Showing the Uploaded Image

The home page is where we will display all the user details. First, let’s create a route and controller for it.

On the terminal, run the code to generate the HomeController.

php artisan make:controller HomeController

Go to the newly created HomeController, and add the following methods to the class.

<?php
namespace App\Http\Controllers;

use Illuminate\Support\Facades\Auth;

class HomeController extends Controller
{
    function __construct()
    {
        $this->middleware('auth');
    }
    public function show()
    {
        return view('home')->with('user', Auth::user());
    }
}

In the constructor, we define that we are using the auth middleware. What this middleware does is that it allows only authorized users to access functions defined in the class.

Hence, the user can only come here if he is authenticated. If the user is unauthenticated, he is redirected to the login page.

The show function handles the displaying of the home page. We are passing the authenticated user to the view as well. To learn more about this, check the laravel documentation.

Update the routes file to contain the home route.

// web.php
...
Route::get('/home', 'HomeController@show')->name('home');

Finally, create a home.blade.php file in the resources/views directory.

Add the following to home.blade.php:

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Home | Profile Form</title>
    <!-- Fonts -->
    <link href="https://fonts.googleapis.com/css?family=Nunito:200,600" rel="stylesheet">
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
    <style>
      body {
        display: flex;
        flex-direction: column;
        justify-content: center;
        align-items: center;
        height: 500px;
      }
      .card {
        display: flex;
        flex-direction: column;
        justify-content: center;
        align-items: center;
        width: 300px;
        padding: 50px 0;
      }
      .card-img-top {
        width: 200px;
        height: 200px;
        border-radius: 50%;
      }
    </style>
  </head>
  <body>
    <div class="card">
      <img src="https://via.placeholder.com/150" class="card-img-top" alt="...">
      <div class="card-body">
        <h5 class="card-title">{{$user->name}}</h5>
        <p class="card-text">{{$user->email}}</p>
        <a class="btn btn-warning">Logout</a>
      </div>
    </div>
  </body>
</html>

In the view file, we have access to the user data that was passed from the controller.

We are currently using a placeholder image to show the user’s photo because the photo is not currently available to us.

We need to create a symbolic link from public/storage to storage/app/public folder to  Laravel makes this simple by providing the artisan command:

php artisan storage:link

After doing that, update the image tag to show the profile image.

<img src="{{asset('storage/'.$user->photo)}}" class="card-img-top" alt="...">

Congrats!!! You have successfully learned how to handle File Uploads with validation in Laravel.

Conclusion

Almost all applications require storing files in one way or another. Following the above guide, you should be able to handle file uploads with validation in Laravel.