How I Handle Image Uploads in Laravel: Easily Switching Between Public Disk and Direct Public Folder
When working with image uploads in Laravel, you might encounter scenarios where you want to upload images either to Laravel's public storage disk or directly to the public folder. This article will guide you through setting up both options and making your application flexible enough to switch between the two.
Why Choose Between Public Disk and Direct Public Folder Uploads?
Laravel’s public disk, stored in storage/app/public and linked to public/storage, provides a secure and configurable way to manage public assets. Direct uploads to the public folder offer simplicity and immediate access, although they can require extra permissions management. The reason behind this setup is that some hosting providers lack terminal or symlink support, which can limit options. While direct uploads to public aren’t generally recommended (using php artisan storage:link is always better), this approach adds flexibility for limited environments. Additionally, creating an image upload service improves consistency, simplifies maintenance, and eliminates the need to manage individual controller methods—saving time, which appeals to my "efficient" (lazy) side! ??
Step 1: Configuring Disks in Laravel
In the config/filesystems.php file, we define the public and public_uploads disks:
Here's how to configure these disks:
// config/filesystems.php
'disks' => [
'public' => [
'driver' => 'local',
'root' => storage_path('app/public'),
'url' => env('APP_URL').'/storage',
'visibility' => 'public',
],
'public_uploads' => [
'driver' => 'local',
'root' => public_path('uploads'),
'url' => env('APP_URL').'/uploads',
'visibility' => 'public',
],
// other disks...
],
Step 2: Creating an Enum for Disk Options
Enums help keep our code clean by centralizing the disk names, so if we need to add more options later, we only update them in one place.
// app/Enums/EnumFileSystemDisk.php
namespace App\Enums;
enum EnumFileSystemDisk: string
{
case PUBLIC = 'public';
case PUBLIC_UPLOADS = 'public_uploads';
}
Step 3: Setting Up Image Path Helpers
With a helper function, we can dynamically generate image paths based on the disk specified in .env. Here's an example:
// app/Helpers/ImagePathHelper.php
use App\Enums\EnumFileSystemDisk;
use Illuminate\Support\Facades\Storage;
if (!function_exists('getUserImageProfilePath')) {
function getUserImageProfilePath($user)
{
$disk = env('FILESYSTEM_DISK');
$placeholderUrl = 'https://via.placeholder.com/150';
if ($disk === EnumFileSystemDisk::PUBLIC->value) {
if ($user->image_profile && Storage::disk('public')->exists($user->image_profile)) {
return asset('storage/' . $user->image_profile);
}
} elseif ($disk === EnumFileSystemDisk::PUBLIC_UPLOADS->value) {
$filePath = $user->image_profile;
if ($user->image_profile && file_exists(public_path($filePath))) {
return asset($filePath);
}
}
return $placeholderUrl;
}
}
This function checks the current disk setting and returns the correct URL path for the user's profile image or a placeholder if the image isn't available.
Step 4: Creating an Image Management Service
The ImageManagementService handles image uploads, deletions, and switching between the two disk options. It also manages existing files, preventing duplication.
// app/Services/ImageManagementService.php
namespace App\Services;
use App\Enums\EnumFileSystemDisk;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\File;
class ImageManagementService
{
public function uploadImage(UploadedFile $file, array $options = [])
{
$currentImagePath = $options['currentImagePath'] ?? null;
$disk = $options['disk'] ?? EnumFileSystemDisk::PUBLIC->value;
$folder = $options['folder'] ?? null;
if ($disk === EnumFileSystemDisk::PUBLIC->value) {
if ($currentImagePath && Storage::disk('public')->exists($currentImagePath)) {
Storage::disk('public')->delete($currentImagePath);
}
$imagePath = $file->store($folder, 'public');
return $imagePath;
} elseif ($disk === EnumFileSystemDisk::PUBLIC_UPLOADS->value) {
$directory = public_path($folder);
if (!File::exists($directory)) {
File::makeDirectory($directory, 0755, true);
}
$fileName = time() . '.' . $file->extension();
if ($currentImagePath && File::exists(public_path($currentImagePath))) {
File::delete(public_path($currentImagePath));
}
$file->move($directory, $fileName);
return $folder . '/' . $fileName;
}
return null;
}
public function destroyImage($currentImagePath, $disk = EnumFileSystemDisk::PUBLIC->value)
{
if ($disk === EnumFileSystemDisk::PUBLIC->value) {
if ($currentImagePath && Storage::disk('public')->exists($currentImagePath)) {
Storage::disk('public')->delete($currentImagePath);
return true;
}
} elseif ($disk === EnumFileSystemDisk::PUBLIC_UPLOADS->value) {
if ($currentImagePath && File::exists(public_path($currentImagePath))) {
File::delete(public_path($currentImagePath));
return true;
}
}
return false;
}
}
Step 5: Implementing the Controller for Profile Picture Updates
The controller leverages ImageManagementService to manage user profile image uploads, deletion of old images, and setting the new image path.
public function update(Request $request, ImageManagementService $imageManagementService)
{
$request->validate([
'image_profile' => 'image|mimes:jpeg,png,jpg,gif|max:2048',
'name' => 'required|string|max:255',
'username' => 'required|string|max:255',
'email' => 'required|email|unique:users,email,' . Auth::id(),
]);
$user = Auth::user();
$currentImagePath = $user->image_profile;
if ($request->hasFile('image_profile')) {
$file = $request->file('image_profile');
$imagePath = $imageManagementService->uploadImage($file, [
'currentImagePath' => $currentImagePath,
'disk' => env('FILESYSTEM_DISK'),
'folder' => 'uploads/user_profiles'
]);
$user->image_profile = $imagePath;
}
$user->name = $request->name;
$user->username = $request->username;
$user->email = $request->email;
$user->save();
activity('profile_management')
->causedBy(Auth::user())
->log('Updated profile information.');
return redirect()->route('profile.index')->with('success', 'Profile updated successfully');
}
领英推荐
if ($request->hasFile('image_profile')) {
$file = $request->file('image_profile');
$imagePath = $imageManagementService->uploadImage($file, [
'currentImagePath' => $currentImagePath,
'disk' => env('FILESYSTEM_DISK'),
'folder' => 'uploads/user_profiles'
]);
$user->image_profile = $imagePath;
}
In the code snippet above, when a user uploads a new profile image, the process involves retrieving the uploaded file from the request and passing it to the ImageManagementService to manage the upload. This service is provided with essential details, including the file to upload, the current image path to delete (if it exists), and configuration options like the disk type and folder (such as the public disk or public/uploads). Once the image is successfully uploaded, the user->image_profile attribute is updated with the new file path, ensuring the user’s profile is always linked to their latest image.
To streamline your code and reduce the number of parameters in the update method, you can use dependency injection in the controller’s constructor. This way, ImageManagementService becomes a class property, allowing you to access it throughout the controller without passing it as a parameter.
Here’s how you can refactor it:
Add Dependency Injection in the Constructor
By injecting ImageManagementService in the constructor, you create a single point of access for the service.
protected $imageManagementService;
public function __construct(ImageManagementService $imageManagementService)
{
$this->imageManagementService = $imageManagementService;
}
Now, $this->imageManagementService is available to any method in the controller.
Refactor the update Method
Since ImageManagementService is now a class property, you can remove it from the method signature of update and update the call within the method to use $this->imageManagementService.
Updated update method:
public function update(Request $request)
{
.....
// Handle new profile image upload if present
if ($request->hasFile('image_profile')) {
$file = $request->file('image_profile');
$imagePath = $this->imageManagementService->uploadImage($file, [
'currentImagePath' => $currentImagePath,
'disk' => env('FILESYSTEM_DISK'),
'folder' => 'uploads/user_profiles'
]);
$user->image_profile = $imagePath;
}
.......
}
In the Blade template, you can simply call the helper function to display the user’s profile image, as shown below:
<div class="col-3 d-flex justify-content-center">
<img src="{{ getUserImageProfilePath(Auth::user()) }}" class="rounded-circle" style="width: 200px; height: 200px; object-fit: cover;">
</div>
This snippet calls getUserImageProfilePath with the authenticated user, dynamically fetching the profile image URL.
Conclusion
This setup allows you to control image uploads in Laravel dynamically, switching between the public disk and direct uploads to the public/uploads folder as needed. The flexible configuration means you can easily adapt to project requirements and improve image storage management.
Full Code
You can view the complete code implementation in my latest project, a portfolio and blogging application built with Laravel 10. Check it out here
Image Cover Credit
Photo by Luca Bravo on Unsplash