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=
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();
});
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'];
}
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:
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
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;
Display the index view
public function index()
{
$products = Product::paginate(10);
return view('index', compact('products'));
}
We pass paginated data from the Product model into the view.
To display the create form
public function create()
{
return view('create');
}
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');
}
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'));
}
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');
}
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');
}
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');
}
}
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);
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>
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
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
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
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>
To display an upload button component.
Upload Files
</x-cld-upload-button>
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:
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)