DEV Community

Cover image for Laravel Image Management with Cloudinary: A Step-by-Step Guide to Uploading, Updating, and Deleting Images
Issa Ahmed
Issa Ahmed

Posted on

Laravel Image Management with Cloudinary: A Step-by-Step Guide to Uploading, Updating, and Deleting Images

1. Introduction

Cloudinary stands as a cloud-based platform dedicated to the management of images and videos, offering a wide array of tools and services tailored for storing, optimizing, and efficiently delivering media assets. This platform presents developers with a robust solution, encompassing every facet of media management, from the seamless uploading and transformation of images and videos to their smooth delivery to end-users.

When considered within the Laravel framework, integrating Cloudinary into your application yields a multitude of benefits. Foremost, Cloudinary alleviates developers from the burdensome tasks associated with image management, including storage, resizing, and optimization. This liberates developers from the necessity of constructing and maintaining intricate image processing infrastructure.

In this tutorial, we'll walk through the process of building a form in Laravel 11 to add products to your application, complete with image upload functionality. This form will enable users to seamlessly upload product images along with other essential details.

Prerequisites

  • Basic understanding of Laravel framework.
  • Composer installed on your system.
  • Familiarity with HTML forms.

2. Setting Up Laravel Project

Firstly, we install Laravel via Composer.

composer create-project laravel/laravel product-form

For the database connection, we will be using the default sqlite connection where we will be uploading the file to a database GUI. In this case we are using Tableplus

DB_CONNECTION=sqlite
# DB_HOST=127.0.0.
# DB_PORT=3306
# DB_DATABASE=laravel
# DB_USERNAME=root
# DB_PASSWORD=
Enter fullscreen mode Exit fullscreen mode

3. Creating Database Migration

To generate a migrations file we use the artisan command:

php artisan make:migration create_products_table

We define the schema for product attributes.

Schema::create('products', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->text('description');
            $table->decimal('price',8,2);
            $table->string('image_url');
            $table->string('image_public_id');
            $table->timestamps();
        });
Enter fullscreen mode Exit fullscreen mode

The image_url column stores the URL or path of the uploaded image on Cloudinary. This URL serves as a direct link to access the image when rendering it in web pages or applications.

On the other hand, the image_public_id column stores the unique identifier assigned to the image by Cloudinary, known as the public ID. This public ID is used by Cloudinary to identify and manage the uploaded image within its system. We will delve into generating them a bit later on

Run the following artisan command to add this table to the database

php artisan migrate

4. Building the Model

We need to define a model, which will serve as an intermediary between the database and the controller.

php artisan make:model Product

After the file Product.php is generated, add the fillable property to allow mass assignment


class Product extends Model
{

    protected $fillable = ['name', 'description','price','image_url', 'image_public_id'];

}

Enter fullscreen mode Exit fullscreen mode

5. Generating the Controller

Next, we will generate a controller to handle product-related actions.

php artisan make:controller ProductController --resource

We add the --resource flag which creates the typical create, read, update, and delete ("CRUD") routes.

6. Integrating Cloudinary

Significantly, we have approached the most central part of our discussion: how to implement image storage using Cloudinary.

We start by installing the Cloudinary Laravel SDK package

composer require cloudinary-labs/cloudinary-laravel

Then we head over to Cloudinary's website and sign up to create a new account. We will note the API credentials. It should look similar to this:

Screenshot of Product Environment Credentials in the Cloudinary Dashboard

In the .env file we paste the values to the following lines

CLOUDINARY_URL=
CLOUDINARY_CLOUD_NAME=
CLOUDINARY_API_KEY=
CLOUDINARY_API_SECRET=
CLOUDINARY_SECURE_URL=true
Enter fullscreen mode Exit fullscreen mode

7. Implementing Logic in Controller

Firstly, we ensure we have the following namespaces imported in our ProductController class

use CloudinaryLabs\CloudinaryLaravel\Facades\Cloudinary;
use App\Models\Product;
Enter fullscreen mode Exit fullscreen mode

Display the index view


    public function index()
    {
        $products = Product::paginate(10);
        return view('index', compact('products'));
    }

Enter fullscreen mode Exit fullscreen mode

We pass paginated data from the Product model into the view.

To display the create form

public function create()
    {
        return view('create');
    }
Enter fullscreen mode Exit fullscreen mode

In the store method

   public function store(Request $request)
    {
        $validateRequest = $request->validate([
            'name' => ['required', 'max:255'],
            'image' => ['required', 'image', 'max:2048'],
            'price' => ['required', 'numeric'],
            'description' => 'required',
        ])
        ;
        $cloudinaryImage = $request->file('image')->storeOnCloudinary('products');
        $url = $cloudinaryImage->getSecurePath();
        $public_id = $cloudinaryImage->getPublicId();

        Product::create([
            'name' => $request->name,
            'description' => $request->description,
            'price' => $request->price,
            'image_url' => $url,
            'image_public_id' => $public_id,
        ]);

        return redirect()->route('products.index')->with('message', 'Created successfully');
    }
Enter fullscreen mode Exit fullscreen mode

After the validation, we have a variable $cloudinaryImage that is assigned to a file named image from the incoming HTTP request.

The ->storeOnCloudinary('products') method call uploads the file to Cloudinary. It stores the file in the specified Cloudinary folder named 'products'. The storeOnCloudinary method is provided by Laravel's Cloudinary package, allowing seamless integration with Cloudinary for file storage.

Thenceforth, the file is uploaded to Cloudinary and the line $cloudinaryImage->getSecurePath() retrieves the secure URL (HTTPS) for accessing the uploaded file. The getSecurePath() method returns the HTTPS URL for the uploaded file stored on Cloudinary. This URL can be used to display the uploaded image in the application's frontend.

In addition to the URL, Cloudinary assigns a unique public ID to each uploaded file. This line $cloudinaryImage->getPublicId(), retrieves the public ID assigned by Cloudinary to the uploaded image. The getPublicId() method returns this public ID, which can be useful for referencing the uploaded file within the Cloudinary platform, such as for updating or deleting the file

To display the edit form

public function edit(Product $product)
    {
        return view('edit', compact('product'));
    }
Enter fullscreen mode Exit fullscreen mode

In the update method

 public function update(Request $request, Product $product)
    {
        $validateRequest = $request->validate([
            'name' => ['sometimes','required', 'max:255'],
            'image' => ['sometimes','required', 'image', 'max:2048'],
            'price' => ['sometimes','required', 'numeric'],
            'description' => ['sometimes','required'],
        ]);

        if($request->hasFile('image')){
            Cloudinary::destroy($product->image_public_id);
            $cloudinaryImage = $request->file('image')->storeOnCloudinary('products');
            $url = $cloudinaryImage->getSecurePath();
            $public_id = $cloudinaryImage->getPublicId();

            $product->update([
                'image_url' => $url,
                'image_public_id' => $public_id,
            ]);

        }

        $product->update([
            'name' => $request->name,
            'description' => $request->description,
            'price' => $request->price
        ]);

        return redirect()->route('products.index')->with('message', 'Updated successfully');
    }

Enter fullscreen mode Exit fullscreen mode

In our validation, we add the rule sometimes to run validation checks against the fields only if they are present in the data being validated.

We then check if the request has an image file. If present, the Cloudinary::destroy($product->image_public_id) static method invocation calls the destroy method of the Cloudinary class, passing the public ID of the image as an argument. This will delete the image from Cloudinary based on its public ID.

The subsequent lines are those similar to the ones in the create method. We then update the model based on the new data.

In the destroy method

 public function destroy(Product $product)
    {
        Cloudinary::destroy($product->image_public_id);
        $product->delete();

        return redirect()->route('products.index')->with('message', 'Deleted Successfully');

    }
Enter fullscreen mode Exit fullscreen mode

The destroy method on the Cloudinary class works the same way as the its definition in the update method. It will delete the image from Cloudinary based on its public ID. We then delete the product from the database.

Here is the full logic of the ProductController

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use CloudinaryLabs\CloudinaryLaravel\Facades\Cloudinary;
use App\Models\Product;

class ProductController extends Controller
{
    public function index()
    {
        $products = Product::paginate(10);
        return view('index', compact('products'));
    }

    public function create()
    {
        return view('create');
    }

    public function store(Request $request)
    {
        $validateRequest = $request->validate([
            'name' => ['required', 'max:255'],
            'image' => ['required', 'image', 'max:2048'],
            'price' => ['required', 'numeric'],
            'description' => 'required',
        ]);
        $cloudinaryImage = $request->file('image')->storeOnCloudinary('products');
        $url = $cloudinaryImage->getSecurePath();
        $public_id = $cloudinaryImage->getPublicId();

        Product::create([
            'name' => $request->name,
            'description' => $request->description,
            'price' => $request->price,
            'image_url' => $url,
            'image_public_id' => $public_id,
        ]);

        return redirect()->route('products.index')->with('message', 'Created successfully');
    }

    public function edit(Product $product)
    {
        return view('edit', compact('product'));
    }

    public function update(Request $request, Product $product)
    {
        $validateRequest = $request->validate([
            'name' => ['sometimes', 'required', 'max:255'],
            'image' => ['sometimes', 'required', 'image', 'max:2048'],
            'price' => ['sometimes', 'required', 'numeric'],
            'description' => ['sometimes', 'required'],
        ]);

        if ($request->hasFile('image')) {
            Cloudinary::destroy($product->image_public_id);
            $cloudinaryImage = $request->file('image')->storeOnCloudinary('products');
            $url = $cloudinaryImage->getSecurePath();
            $public_id = $cloudinaryImage->getPublicId();

            $product->update([
                'image_url' => $url,
                'image_public_id' => $public_id,
            ]);
        }

        $product->update([
            'name' => $request->name,
            'description' => $request->description,
            'price' => $request->price,
        ]);

        return redirect()->route('products.index')->with('message', 'Updated successfully');
    }

    public function destroy(Product $product)
    {
        Cloudinary::destroy($product->image_public_id);
        $product->delete();

        return redirect()->route('products.index')->with('message', 'Deleted Successfully');
    }
}

Enter fullscreen mode Exit fullscreen mode

8. Defining the Routes and Views

We have already generated a resourceful controller, meaning we can define routes for a controller that handles CRUD operations using a single line of code

use App\Http\Controllers\ProductController;

Route::resource('/products', ProductController::class);
Enter fullscreen mode Exit fullscreen mode

Since we are making multiple views, we ought to create a folder called layouts which we will extend to all our views. Inside the layouts folder we will be creating the file main.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.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <meta name="csrf-token" content="{{ csrf_token() }}">

    {{-- Tailwind CSS play CDN --}}
    <script src="https://cdn.tailwindcss.com"></script>
    <title>Cloudinary Image Upload</title>
</head>
<body>
    <main class="px-20 py-10">
        @yield('content')
    </main>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

We have opted to use Tailwind CSS and included its CDN link, you can use any styling framework of your choice.

Now in the views directory, let's create three new views, and name them

  • index.blade.php
  • create.blade.php
  • edit.blade.php

These three files will be handling: listing all the products in a table, displaying a form for creating a new product and showing a form for editing and updating a product respectively.

Note the <x-cld-image></x-cld-image> directive that is present in all the views.

index.blade.php

@extends('layouts.main')
@section('content')
    <div>
        <div class="flex justify-between mb-2">
            <a href="{{ route('products.create') }}"
                class="text-sm uppercase p-4 my-4 bg-green-600 hover:bg-green-700 text-white  font-bold">Add new product</a>
        </div>

        {{-- Status message --}}
        @if (session('message'))
            <p class="bg-green-500 capitalize py-1 text-lg text-white mb-2 font-bold px-4">
                {{ session('message') }}</p>
        @endif

        <div class="overflow-x-auto relative shadow-md sm:rounded-lg">
            {{-- Pagination --}}
            <div class="mb-4">
                {{ $products->links() }}
            </div>
            <table class="w-full text-sm text-left text-gray-500">
                <thead class=" text-gray-200 uppercase bg-black">
                    <tr class="m-8">
                        <th scope="col" class="py-4 px-2">
                            Product name
                        </th>
                        <th scope="col" class="py-4 px-2">
                            Image
                        </th>

                        <th scope="col" class="py-4 px-2">
                            Description
                        </th>
                        <th scope="col" class="py-4 px-2">
                            Price
                        </th>

                        <th scope="col" class="py-4 px-2">
                            Action
                        </th>
                    </tr>
                </thead>
                <tbody>
                    @foreach ($products as $product)
                        <tr class="bg-white border-b">
                            <th scope="row" class="py-3 px-6 font-medium text-gray-900 whitespace-nowrap ">
                                {{ $product->name }}
                            </th>

                            <td class="py-3 px-6">
                                <x-cld-image public-id="{{ $product->image_public_id }}" alt="{{$product->name}}" width="80"
                                    height="80" class=""></x-cld-image>
                            </td>
                            <td class="py-3 px-6 text-ellipsis">
                                {{ $product->description }}
                            </td>
                            <td class="py-3 px-6">
                                {{ $product->price }}
                            </td>
                            <td class="py-3 px-6">
                                <div class=" flex space-x-5">
                                    <a href="{{ route('products.edit', $product->id) }}"
                                        class="py-1.5 px-3 uppercase text-white font-medium  bg-blue-500 hover:bg-blue-700">Edit</a>
                                    <form action="{{ route('products.destroy', $product->id) }}" method="POST"
                                        class="py-0.5 px-3 text-white font-medium text-lg bg-red-500 hover:bg-red-700"
                                        onsubmit="return confirm('Are you sure?')">
                                        @csrf
                                        @method('DELETE')
                                        <button type="submit" class=" uppercase">Delete</button>
                                    </form>
                                </div>
                            </td>
                        </tr>
                    @endforeach
                </tbody>
            </table>
        </div>
    </div>
@endsection
Enter fullscreen mode Exit fullscreen mode

create.blade.php

@extends('layouts.main')
@section('content')
    <div class="py-12">
        <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">

            <div class="mb-6">
                <a href="{{ route('products.index')}}"
                    class="px-10 py-2 text-white bg-blue-600 hover:bg-blue-400 uppercase">Go Back</a>
            </div>

            @if ($errors->any())
                <ul class="p-2 text-red-600 font-semibold list-disc">
                    @foreach ($errors->all() as $error)
                        <li>{{ $error }}</li>
                    @endforeach
                </ul>
            @endif

            <form action="{{ route('products.store') }}" method="POST" enctype="multipart/form-data">
                @csrf
                <div class="mb-6">
                    <label for="name" class="block mb-2 text-lg text-gray-600 font-bold">Product Name</label>
                    <input type="text"
                        class="bg-gray-50 border border-gray-300 text-gray-900 text-md rounded-lg focus:ring-black focus:border focus:border-black  block w-1/2 p-2.5"
                        name="name" required value="{{ old('name') }}">
                </div>

                <div class="mb-6">
                    <label for="description" class="block mb-2 text-lg text-gray-600 font-bold">Description</label>
                    <textarea name="description" id="" cols="30" rows="5"
                        class="bg-gray-50 border border-gray-300 text-gray-900 text-md rounded-lg focus:ring-gray-400  focus:border-gray-400  block w-1/2 p-2">{{ old('description') }}</textarea>
                </div>

                <div class="mb-6">

                    <label class="block mb-2 text-lg  text-gray-600 font-bold" for="images">Upload image</label>
                    <input
                        class="block w-1/2 p-2 text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 cursor-pointer"
                        type="file" name="image" required>
                </div>

                <div class="mb-6">
                    <label for="price" class="block mb-2 text-lg text-gray-600 font-bold">Price</label>
                    <input type="number" name="price"
                        class="block w-1/2 p-2 text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 focus:ring-gray-400  focus:border-gray-400"
                        required value="{{ old('price') }}">
                </div>

                <button type="submit"
                    class="my-8 w-48 h-12 bg-emerald-600 hover:bg-emerald-500 rounded-md text-lg text-white">Create
                    Product</button>
            </form>

        </div>
    </div>
@endsection


Enter fullscreen mode Exit fullscreen mode

edit.blade.php

@extends('layouts.main')
@section('content')
    <div class="py-12">

        <div class="mb-6">
            <a href="{{ route('products.index') }}" class="px-10 py-2 text-white bg-blue-600 hover:bg-blue-400 uppercase">Go
                Back</a>
        </div>

        @if ($errors->any())
            <ul class="p-2 text-red-600 font-semibold list-disc">
                @foreach ($errors->all() as $error)
                    <li>{{ $error }}</li>
                @endforeach
            </ul>
        @endif

        <form action="{{ route('products.update', $product->id) }}" method="POST" enctype="multipart/form-data">
            @csrf
            @method('PUT')

            <div class="mb-6">
                <label for="name" class="block mb-2 text-lg text-gray-600 font-bold">Product Name</label>
                <input type="text" id="base-input"
                    class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-black focus:border-black block w-1/2 p-2.5 "
                    name="name" value="{{ $product->name }}">
            </div>

            <div class="mb-6">
                <label for="description" class="block mb-2 text-lg text-gray-600 font-bold">Description</label>
                <textarea name="description" id="" cols="30" rows="5"
                    class="bg-gray-50 border border-gray-300 text-gray-900 text-md rounded-lg focus:ring-gray-400  focus:border-gray-400 block w-1/2 p-2">{{$product->description}}</textarea>
            </div>

            <label class="block mb-2 text-lg  text-gray-600 font-bold" for="image">Upload image</label>
            <div class="mb-24 w-44 h-48">
                <x-cld-image public-id="{{ $product->image_public_id }}" alt="{{$product->name}}"
                    class="w-full h-full object-cover rounded-xl"></x-cld-image>
            </div>
            <input class="block w-1/2 p-2 text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 cursor-pointer"
                type="file" name="image">

            <div class="mb-6">
                <label for="price" class="block mb-2 text-lg text-gray-600 font-bold">Price</label>
                <input type="number" name="price" min
                    class="block w-1/2 p-2 text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 focus:ring-gray-400  focus:border-gray-400"
                    required value="{{ $product->price}}">
            </div>

            <button type="submit"
                class="my-8 w-48 h-12 bg-emerald-600 hover:bg-emerald-500 rounded-md text-lg text-white">Update
                Product</button>
        </form>
    </div>
@endsection
Enter fullscreen mode Exit fullscreen mode

We used the blade directive
<x-cld-image public-id="{{ $product->image_public_id }}" height="80" width="80"></x-cld-image>
This blade directive ships with the package and we use it to display the image. The attribute public-id is specifying the public identifier of the image. We also specified a height and width attribute which is optional.
You can also define other attributes like crop, transformation, format, quality etc.

Alternatively you can opt to display images using the normal img tag. We use the image_url rather than image_public_id to retrieve an image and you would define it like so:

<img src="{{ $product_image->image_url }}">

Cloudinary also ships with other blade directives such as:

<x-cld-video public-id=""></x-cld-video>
To display videos

<x-cld-upload-button>
Upload Files
</x-cld-upload-button>
To display an upload button component.

At this point if you have followed the steps correctly you should have a working application. To view the project in the browser, run this command in the terminal:

php artisan serve

Then navigate to

http://127.0.0.1:8000/products

The Cloudinary Laravel SDK offers a lot more functionalities. In our next post, we will cover Media Management Via the Command Line. I highly recommend you visit the official Github repository for more details.

This tutorial was to show you an alternative way to storing your images when developing a Laravel application. If you have reached this far, thank you for taking the time to follow through the tutorial. If you have any suggestion or critique, please comment down below.

Top comments (0)