How I Handle Image Uploads in Laravel: Easily Switching Between Public Disk and Direct Public Folder

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:

  • public disk stores files under storage/app/public and is accessible via public/storage.
  • public_uploads disk stores files directly under the public/uploads directory.

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

要查看或添加评论,请登录

社区洞察

其他会员也浏览了