Laravel — P44: Form Single Image Upload (CMP)

Implementing Single Image Upload in Forms

The image upload. Instantly throws fear into the beginner developer. I’m not sure where the fear comes from but it’s one thing that developers fear. Laravel makes the process incredibly simple so we don’t have to stress about file uploads in general.

This is a continuation from our article discussing forms. Our form needs to have a file upload button, which it does (create.blade.php).

<div class="flex flex-col mb-4">
    <div class="mb-8">
        <label class="mb-2 font-bold text-lg text-gray-900 block" for="file">Select Image</label>
        <input type="file" name="file" id="file" class="mb-2 text-lg text-gray-900" />
    </div>
</div>

We simply need to process it. There is one more addition that we need to add to our form; we need to set the enctype.

<form action="/personalcars/" method="post" enctype="multipart/form-data">

This will ensure that we’re able to submit our form with file data.

Next, we need to validate the data. Inside of our $request->validate method, we need to add one additional array element. It should match our file name, which just also happens to be file.

$validated = $request->validate([
    'year' => 'required|integer',
    'make' => 'required|max:255',
    'model' => 'required|max:255',
    'is_manual' => 'required|boolean',
    'exterior_color' => 'required|max:255',
    'purchase_amount' => 'numeric',
    'current_value' => 'numeric',
    'sales_amount' => 'numeric|nullable',
    'date_purchased' => 'required|date',
    'date_sold' => 'date|nullable',
    'file' => 'image|mimes:jpg,jpeg,png,gif|max:2048|nullable',
]);

We’re specifying that the file is an image and that it can be in the following formats: jpg, jpeg, png, or gif. The max size is 2048kb.

The image can now be stored inside of our directory with the following command:

$image_path = $request->file('file')->store('file');

But where is it actually stored? The storage location will be in the app/storage/file/ directory. You can check to make sure that the file exists there.

The image gets a random name and the path is stored in our $image_path variable. If we dump the $image_path we’ll see that the path provided is file/hMBXe6EXgPNK9R2wAXiZPTUsY4I15Q65kTI01WIW.jpg.

If we want this to be in our storage/public folder, we can simply add a second argument to our store() method.

$image_path = $request->file('file')->store('file', 'public');

The file path is: images/FV9DGZnRQLDmWR61iyOirJKxRam51qKFsxM1flGS.jpg.

Let’s store this image in our images table and then link our Image to our PersonalCar.

$image = Image::create([
    'url' => $image_path,
    'alt' => $request->year . " " . $brand->name . " " . $model->name,
]);

This will add our $image_path into our images table.

Finally, after the car is inserted we need to link the PersonalCar to the Image. We do that with the attach() method.

$car->images()->attach($image);

The full code for store looks like this:

public function store(Request $request)
{

    $validated = $request->validate([
        'year' => 'required|integer',
        'make' => 'required|max:255',
        'model' => 'required|max:255',
        'is_manual' => 'required|boolean',
        'exterior_color' => 'required|max:255',
        'purchase_amount' => 'numeric',
        'current_value' => 'numeric',
        'sales_amount' => 'numeric|nullable',
        'date_purchased' => 'required|date',
        'date_sold' => 'date|nullable',
        'file' => 'image|mimes:jpg,jpeg,png,gif|max:2048|nullable',
    ]);

    $brand = PersonalCarBrand::firstOrCreate([
        'name' => $request->make,
    ], [
        'slug' => str_replace(" ", "-", strtolower($request->make))
    ]);

    $model = PersonalCarModel::firstOrCreate([
        'name' => $request->model,
    ], [
        'slug' => str_replace(" ", "-", strtolower($request->model))
    ]);

    $image_path = $request->file('file')->store('images', 'public');

    $image = Image::create([
        'url' => $image_path,
        'alt' => $request->year . " " . $brand->name . " " . $model->name,
    ]);

    $car = new PersonalCar;
    $car->year            = $request->year;
    $car->brand()->associate($brand);
    $car->model()->associate($model);
    $car->is_manual       = $request->is_manual;
    $car->exterior_color  = $request->exterior_color;
    $car->purchase_amount = $request->purchase_amount;
    $car->current_value   = $request->current_value;
    $car->sales_amount    = $request->sales_amount == 0 ? 0 : $request->sales_amount;
    $car->date_purchased  = $request->date_purchased;
    $car->date_sold       = $request->date_sold;

    $car->save();

    $car->images()->attach($image);

    return redirect()->to('/personalcars/')->with('status', 'Your car has been added.');
}

Let’s do a test run. I’m going to add a 2003 Corvette to my personal cars collection. After clicking Save I can see that the image is in our images folder and the file path is in our images table.

 

mysql> select * from images;
+----+---------------------------------------------------------------+---------------------+---------------------+---------------------+
| id | url                                                           | alt                 | created_at          | updated_at          |
+----+---------------------------------------------------------------+---------------------+---------------------+---------------------+
|  1 | https://pngimg.com/uploads/chevrolet/%D1%81hevrolet_PNG18.png | 2007 Chevy Corvette | 2023-01-22 13:15:11 | 2023-01-22 13:15:11 |
|  2 | https://pngimg.com/uploads/chevrolet/%D1%81hevrolet_PNG23.png | 2003 Chevy Corvette | 2023-01-22 13:29:50 | 2023-01-22 13:29:50 |
|  3 | https://pngimg.com/uploads/chevrolet/%D1%81hevrolet_PNG25.png | 2003 Chevy Corvette | 2023-01-22 13:30:08 | 2023-01-22 13:30:08 |
|  4 | https://pngimg.com/uploads/chevrolet/%D1%81hevrolet_PNG60.png | Test Image          | 2023-01-22 13:36:52 | 2023-01-22 13:36:52 |
|  5 | images/CHl8y3afKXuwgE8DZWt0D61vOwUEkB11CtSIl0lO.jpg           | 2003 Chevy Corvette | 2023-01-28 17:46:41 | 2023-01-28 17:46:41 |
|  6 | images/yrqW77FwXqfwmdLA3rH2FQFoavAXxuolz48s5LKu.jpg           | 2003 Chevy Corvette | 2023-01-28 17:47:02 | 2023-01-28 17:47:02 |
+----+---------------------------------------------------------------+---------------------+---------------------+---------------------+
6 rows in set (0.00 sec)

The link between the our PersonalCar and Image is also present in our image_personal_car pivot table.

mysql> select * from image_personal_car;
+-----------------+----------+
| personal_car_id | image_id |
+-----------------+----------+
|               1 |        1 |
|               1 |        3 |
|              18 |        6 |
+-----------------+----------+
3 rows in set (0.00 sec)

If we were to go to our show page for that particular id, we would see the hardcoded image there now.

 

We need to display our actual image. To display an image, we need the asset() helper method. It accepts the location. For us, it’ll be in storage/public/images. You do not specify the public directory since it’s just understood, but you do specify storage.

<img src="{{ asset( 'storage/' . $car->images[0]->url ) }}" class="w-full relative z-10" alt="">

The $car->images[0] grabs the first pivot association. Remember that this is a many-to-many association. For the first one, we get the url.

If we refresh now, we’ll get a blank screen. That’s because we haven’t created a symbolic link for our storage directory. The artisan command for that is:

# php artisan storage:link

   INFO  The [public/storage] link has been connected to [storage/app/public].  

Refreshing the show page, we get our image displayed.

That’s it. This is what the full code looks like now.

show.blade.php

<x-layouts.app title="{{ $title }}">
    <div class="flex bg-white mt-12" style="height:600px;">
        <div class="w-full max-w-6xl rounded bg-white shadow-xl p-10 lg:p-20 mx-auto text-gray-800 relative md:text-left">
            <div class="md:flex items-center -mx-10">
                <div class="w-full md:w-1/2 px-10 mb-10 md:mb-0">
                    <div class="relative">
                        <img src="{{ asset( 'storage/' . $car->images[0]->url ) }}" class="w-full relative z-10" alt="">
                        <div class="border-4 border-blue-200 absolute top-10 bottom-10 left-10 right-10 z-0"></div>
                    </div>
                </div>
                <div class="w-full md:w-1/2 px-10">
                    <div class="mb-10">
                        <h1 class="font-bold uppercase text-2xl mb-5">{{ $car->year . " " . $car->brand->name . " " . $car->model->name }}</h1>
                        <ul class="text-sm">
                            <li>Year: {{ $car->year }}</li>
                            <li>Make: {{ $car->brand->name }}</li>
                            <li>Make: {{ $car->model->name }}</li>
                            <li>Transmission: {{ $car->is_manual }}</li>
                            <li>Purchase Amount: {{ $car->purchase_amount }}</li>
                            <li>Current Value: {{ $car->current_value }}</li>
                            <li>Sales Amount: {{ $car->sales_amount }}</li>
                            <li>Date Purchased: {{ $car->date_purchased }}</li>
                            <li>Date Sold: {{ $car->date_sold }}</li>
                        </ul>
                    </div>
                    <div>
                        <div class="inline-block align-bottom mr-5">
                            <span class="font-bold text-5xl leading-none align-baseline">{{ $car->current_value }}</span>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <div class="flex w-full flex-wrap content-center justify-center p-10">
        <div class="grid grid-cols-4 gap-3">

            @foreach( $car->images as $image )
                <div class="w-80 bg-white p-3">
                    <img class="h-52 w-full object-cover" src="{{ $image->url }}" alt="{{ $image->alt }}" />
                </div>
            @endforeach

        </div>
    </div>
</x-layouts.app>

create.blade.php

<x-layouts.app title="{{ $title }}">
    <div class="flex bg-white mt-12">
        <div class="flex justify-center items-center w-full">
            <div class="w-1/2 bg-white rounded shadow-2xl p-8 m-4">
                <h1 class="block w-full text-center text-gray-800 text-2xl font-bold mb-6">Add New Car</h1>
                @if ($errors->any())
                    <ul>
                        @foreach ($errors->all() as $error)
                            <li>{{ $error }}</li>
                        @endforeach
                    </ul>
                @endif
                <form action="/personalcars/" method="post" enctype="multipart/form-data">
                    @csrf
                    <div class="flex flex-col mb-4">
                        <label class="mb-2 font-bold text-lg text-gray-900" for="year">Year</label>
                        <input class="border py-2 px-3 text-grey-800" type="number" name="year" id="year" min="1920" max="{{ date('Y') + 1 }}" placeholder="2003">
                    </div>
                    <div class="flex flex-col mb-4">
                        <label class="mb-2 font-bold text-lg text-gray-900" for="make">Make</label>
                        <input class="border py-2 px-3 text-grey-800" type="text" name="make" id="make" placeholder="Chevy">
                    </div>
                    <div class="flex flex-col mb-4">
                        <label class="mb-2 font-bold text-lg text-gray-900" for="model">Model</label>
                        <input class="border py-2 px-3 text-grey-800" type="text" name="model" id="model" placeholder="Corvette">
                    </div>
                    <div class="flex flex-col mb-4">
                        <label class="mb-2 font-bold text-lg text-gray-900" for="password">Transmission</label>
                        <div class="flex items-center mb-4">
                            <input id="is-manual-1" type="radio" name="is_manual" value="1" class="h-4 w-4 border-gray-300 focus:ring-2 focus:ring-blue-300" checked>
                            <label for="is-manual-1" class="text-sm font-medium text-gray-900 ml-2">
                                Manual
                            </label>
                        </div>
                        <div class="flex items-center mb-4">
                            <input id="is-manual-2" type="radio" name="is_manual" value="0" class="h-4 w-4 border-gray-300 focus:ring-2 focus:ring-blue-300">
                            <label for="is-manual-2" class="text-sm font-medium text-gray-900 ml-2">
                                Automatic
                            </label>
                        </div>
                    </div>
                    <div class="flex flex-col mb-4">
                        <label class="mb-2 font-bold text-lg text-gray-900" for="exterior_color">Exterior Color</label>
                        <input class="border py-2 px-3 text-grey-800" type="text" name="exterior_color" id="exterior_color" placeholder="Blue">
                    </div>
                    <div class="flex flex-col mb-4">
                        <label class="mb-2 font-bold text-lg text-gray-900" for="purchase_amount">Purchase Amount (in USD)</label>
                        <input class="border py-2 px-3 text-grey-800" type="text" name="purchase_amount" id="purchase_amount" placeholder="9532.57">
                    </div>
                    <div class="flex flex-col mb-4">
                        <label class="mb-2 font-bold text-lg text-gray-900" for="current_value">Current Value (in USD)</label>
                        <input class="border py-2 px-3 text-grey-800" type="text" name="current_value" id="current_value" placeholder="95532.57">
                    </div>
                    <div class="flex flex-col mb-4">
                        <label class="mb-2 font-bold text-lg text-gray-900" for="sales_amount">Sales Amount (in USD)</label>
                        <input class="border py-2 px-3 text-grey-800" type="text" name="sales_amount" id="sales_amount" placeholder="0.00">
                    </div>
                    <div class="flex flex-col mb-4">
                        <label class="mb-2 font-bold text-lg text-gray-900" for="date_purchased">Date Purchased</label>
                        <input class="border py-2 px-3 text-grey-800" type="date" name="date_purchased" id="date_purchased">
                    </div>
                    <div class="flex flex-col mb-4">
                        <label class="mb-2 font-bold text-lg text-gray-900" for="date_sold">Date Sold</label>
                        <input class="border py-2 px-3 text-grey-800" type="date" name="date_sold" id="date_sold">
                    </div>
                    <div class="flex flex-col mb-4">
                        <div class="mb-8">
                            <label class="mb-2 font-bold text-lg text-gray-900 block" for="file">Select Image</label>
                            <input type="file" name="file" id="file" class="mb-2 text-lg text-gray-900" />
                        </div>
                    </div>

                    <button class="block bg-green-400 hover:bg-green-600 text-white uppercase text-lg mx-auto p-4 rounded" type="submit">Save Car</button>
                </form>
            </div>
        </div>
    </div>
</x-layouts.app>

PersonalCarController.php

<?php

namespace App\Http\Controllers;

use App\Models\Image;
use App\Models\PersonalCar;
use App\Models\PersonalCarBrand;
use App\Models\PersonalCarModel;
use Faker\Provider\Person;
use Illuminate\Http\Request;

class PersonalCarController extends Controller
{
    /**
     * Display a listing of the resource.
     */
    public function index()
    {
        $cars = PersonalCar::with(['brand', 'model'])->orderBy('year', 'desc')->get();

        return view('personalcars/index', [
            'title' => 'Personal Cars',
            'cars' => $cars,
        ]);
    }

    /**
     * Show the form for creating a new resource.
     */
    public function create()
    {
        return view('personalcars/create', [
            'title' => 'Personal Cars'
        ]);
    }

    /**
     * Store a newly created resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     */
    public function store(Request $request)
    {

        $validated = $request->validate([
            'year' => 'required|integer',
            'make' => 'required|max:255',
            'model' => 'required|max:255',
            'is_manual' => 'required|boolean',
            'exterior_color' => 'required|max:255',
            'purchase_amount' => 'numeric',
            'current_value' => 'numeric',
            'sales_amount' => 'numeric|nullable',
            'date_purchased' => 'required|date',
            'date_sold' => 'date|nullable',
            'file' => 'image|mimes:jpg,jpeg,png,gif|max:2048|nullable',
        ]);

        $image_path = $request->file('file')->store('images', 'public');

        $brand = PersonalCarBrand::firstOrCreate([
            'name' => $request->make,
        ], [
            'slug' => str_replace(" ", "-", strtolower($request->make))
        ]);

        $model = PersonalCarModel::firstOrCreate([
            'name' => $request->model,
        ], [
            'slug' => str_replace(" ", "-", strtolower($request->model))
        ]);

        $image = Image::create([
            'url' => $image_path,
            'alt' => $request->year . " " . $brand->name . " " . $model->name,
        ]);

        $car = new PersonalCar;
        $car->year            = $request->year;
        $car->brand()->associate($brand);
        $car->model()->associate($model);
        $car->is_manual       = $request->is_manual;
        $car->exterior_color  = $request->exterior_color;
        $car->purchase_amount = $request->purchase_amount;
        $car->current_value   = $request->current_value;
        $car->sales_amount    = $request->sales_amount == 0 ? 0 : $request->sales_amount;
        $car->date_purchased  = $request->date_purchased;
        $car->date_sold       = $request->date_sold;

        $car->save();

        $car->images()->attach($image);

        return redirect()->to('/personalcars/')->with('status', 'Your car has been added.');
    }

    /**
     * Display the specified resource.
     *
     * @param  int  $id
     */
    public function show($id)
    {
        $car = PersonalCar::with(['brand', 'model', 'images'])->find($id);

        return view('personalcars/show', [
            'title' => $car->year . " " . $car->brand->name . " " . $car->model->name,
            'car' => $car,
        ]);
    }

    // ...
}

In the next article, we’ll cover editing our existing records.

https://github.com/dinocajic/youtube-laravel

Laravel Series

Continue your Laravel Learning.

Laravel — P43: Controller Form Validation (CMP)

Mastering Form Validation in Laravel Controllers

Laravel – P43: CMP – Form Validation

Learn how to implement form validation in Laravel controllers to ensure data integrity and improve user input handling in your applications.

Laravel — P44: Form Single Image Upload (CMP)

Implementing Single Image Upload in Forms

Laravel – P44: CMP – Form Single Image Upload

Learn how to implement single image uploads in Laravel forms. A step-by-step guide to handling and storing images effectively in your application.

How to Use Laravel’s Controller Edit/Update Methods Effectively

Laravel – P45: CMP – Controller Edit/Update

Learn how to effectively implement Edit and Update methods in Laravel controllers to manage data updates smoothly in your applications.

Leave a Reply