DEV Community

Nad Lambino
Nad Lambino

Posted on

Handling File Uploads in Laravel, Automatically.

Managing your file uploads in Laravel is really easy... or is it? Actually, it is really easy, and you can do this in so many ways which you might or might not like.

Today, I will introduce you to my first Laravel package which I have created to handle the file uploads for your models, automatically.

Laravel Uploadable takes care of the files from your request and upload them for your model. Below is the detailed documentation to get you started.

Table of Contents

Installation

You can install the package via composer:

composer require nadlambino/uploadable
Enter fullscreen mode Exit fullscreen mode

Publish and run the migrations with:

php artisan vendor:publish --tag="uploadable-migrations"
php artisan migrate
Enter fullscreen mode Exit fullscreen mode

IMPORTANT

You can add more fields to the uploads table according to your needs, but the existing fields should remain.

Optionally, you can publish the Upload model using

php artisan vendor:publish --tag="uploadable-model"
Enter fullscreen mode Exit fullscreen mode

You can publish the config file with:

php artisan vendor:publish --tag="uploadable-config"
Enter fullscreen mode Exit fullscreen mode

Usage

Simply use the NadLambino\Uploadable\Concerns\Uploadable trait to your model that needs file uploads.

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use NadLambino\Uploadable\Concerns\Uploadable;

class Post extends Model
{
    use Uploadable;
}
Enter fullscreen mode Exit fullscreen mode

Now, everytime you create or update a post, it will automatically upload the files that are included in your request and it will save the details in uploads table.

Customizing the Rules and Messages

Files from the request should have the following request names:

Request name Use Case Rules
document Single document upload sometimes, file, mime
documents Multiple document uploads sometimes, file, mime
image Single image upload sometimes, image, mime
images Multiple image uploads sometimes, image, mime
video Single video upload sometimes, mime
videos Multiple video uploads sometimes, mime

You can add more fields or override the default ones by defining the protected uploadRules
method in your model.

protected function uploadRules(): array
{
    return [
        // Override the rules for `document` field
        'document' => ['required', 'file', 'mime:application/pdf'], 

        // Add a new field with it's own set of rules
        'avatar' => ['required', 'image', 'mime:png'] 
    ];
}
Enter fullscreen mode Exit fullscreen mode

To add or override the rules messages, you can define the protected uploadRuleMessages method in your model.

public function uploadRuleMessages(): array
{
    return [
        'document.required' => 'The file is required.',
        'document.mime' => 'The file must be a PDF file.',
        'avatar.required' => 'The avatar is required.',
        'avatar.mime' => 'The avatar must be a PNG file.'
    ];
}
Enter fullscreen mode Exit fullscreen mode

Customizing the file name and upload path

You can customize the file name and path by defining the public methods getUploadFilename and getUploadPath in your model.

public function getUploadFilename(UploadedFile $file): string
{
    return str_replace('.', '', microtime(true)).'-'.$file->hashName();
}

public function getUploadPath(UploadedFile $file): string
{
    return $this->getTable().DIRECTORY_SEPARATOR.$this->{$this->getKeyName()};
}
Enter fullscreen mode Exit fullscreen mode

IMPORTANT

Make sure that the file name is completely unique to avoid overriding existing files.

Uploading Files with Custom Options for the Storage

When you're uploading your files on cloud storage, oftentimes you want to provide options like visibility, cache control, and other metadata. To do so, you can define the getUploadStorageOptions in your uploadable model.

public function getUploadStorageOptions(): array
{
    return [
        'visibility' => 'public',
        'CacheControl' => 'max-age=315360000, no-transform, public'
    ];
}
Enter fullscreen mode Exit fullscreen mode

Manually processing file uploads

File upload happens when the uploadable model's created or updated event was fired.
If you're creating or updating an uploadable model quietly, you can call the createUploads or updateUploads method to manually process the file uploads.

public function update(Request $request, Post $post)
{
    $post->update($request->all());

    // If the post did not change, the `updated` event won't be fired.
    // So, we need to manually call the `updateUploads` method.
    if (! $post->wasChanged()) {
        $post->updateUploads();
    }
}
Enter fullscreen mode Exit fullscreen mode

IMPORTANT

Depending on your configuration, the createUploads will delete the uploadable model when the upload process fails, while updateUploads will update it to its original attributes.

Temporarily Disable the File Upload Process

You can temporarily disable the file uploads by calling the static method disableUpload.

public function update(Request $request, Post $post)
{
    // Temporarily disable the file uploads
    Post::disableUpload();

    $post->update($request->all());

    // Do more stuff here...

    // Manually process the uploads after everything you want to do.
    $post->updateUploads();
}
Enter fullscreen mode Exit fullscreen mode

Caveat

When you are trying to create or update multiple uploadable models, the default behavior of this package is that all of the files from the request will be uploaded and will be attached to all of these models. This is because these models are firing the created or updated event which triggers the upload process.

There are multiple ways to prevent this from happening such as:

  • Silently create or update the models that you don't want to have the files being uploaded and attached to them. By doing so, the created or updated event won't be fired which will not trigger the upload process.
  • Disable the upload process on the specific model by calling the disableUpload() method.
  • Disable the upload process from the NadLambino\Uploadable\Actions\Upload action itself. The NadLambino\Uploadable\Actions\Upload::disableFor() method can accept a string class name of a model, a model instance, or an array of each or both. See below example:
use NadLambino\Uploadable\Actions\Upload;

public function store(Request $request)
{
    // Disable the uploads for all of the instances of Post model during this request lifecycle
    Upload::disableFor(Post::class);

    // User will be created with the files being uploaded and attached to it
    User::create($request->validated());

    // Post will be created without the files being uploaded and attached to it
    Post::create(...);
}

// OR

public function update(Request $request, User $user)
{
     // Disable the uploads only for this specific user during this request lifecycle
    Upload::disableFor($user);

    // $user will be updated without the files being uploaded and attached to it
    $user->update($request->validated());

    $anotherUser = User::find(...);

    // $anotherUser will be updated with the files being uploaded and attached to it
    $anotherUser->update(...);
}
Enter fullscreen mode Exit fullscreen mode

Also, there is NadLambino\Uploadable\Actions\Upload::enableFor() method to let you enable the upload process for the given model. This will just remove the given model class or instance from the list of disabled models. Both of these methods will also work on queued uploads.

Uploading files on model update

By default, when you update an uploadable model, the files from the request will add up to the existing uploaded files. If you want to replace the existing files with the new ones, you can configure it in the uploadable.php config file.

'replace_previous_uploads' => true,
Enter fullscreen mode Exit fullscreen mode

Or alternatively, you can call the static method replacePreviousUploads before updating the model.

public function update(Request $request, Post $post)
{
    // Replace the previous uploads
    Post::replacePreviousUploads();

    $post->update($request->all());
}
Enter fullscreen mode Exit fullscreen mode

NOTE

The process of deleting the previous uploads will only happen when new files were successfully
uploaded.

Uploading files that are NOT from the request

If you wish to upload a file that is not from the request, you can do so by calling the uploadFrom method. This method can accept an instance or an array of \Illuminate\Http\UploadedFile or a string path of a file that is uploaded on your temporary_disk.

// DO
$post->uploadFrom(UploadedFile::fake()->image('avatar1.jpg'));

// OR
$post->uploadFrom([
    UploadedFile::fake()->image('avatar1.jpg'),
    UploadedFile::fake()->image('avatar2.jpg'),
]);

// OR
$fullpath = UploadedFile::fake()->image('avatar.jpg')->store('tmp', config('uploadable.temporary_disk', 'local'));

$post->uploadFrom($fullpath);

// OR
$post->uploadFrom([
    $fullpath1,
    $fullpath2
]);

// OR even a mixed of both
$post->uploadFrom([
    UploadedFile::fake()->image('avatar1.jpg'),
    $fullpath,
]);

$post->save();
Enter fullscreen mode Exit fullscreen mode

IMPORTANT

Make sure that you've already validated the files that you're passing here as it does not run any validation like it does when uploading from request.

Relation methods

There are already pre-defined relation method for specific upload type.

// Relation for all types of uploads
public function upload(): MorphOne { }

// Relation for all types of uploads
public function uploads(): MorphMany { }

// Relation for uploads where extension or type is in the accepted image mimes
public function image(): MorphOne { }

// Relation for uploads where extension or type is in the accepted image mimes
public function images(): MorphMany { }

// Relation for uploads where extension or type is in the accepted video mimes
public function video(): MorphOne { }

// Relation for uploads where extension or type is in the accepted video mimes
public function videos(): MorphMany { }

// Relation for uploads where extension or type is in the accepted document mimes
public function document(): MorphOne { }

// Relation for uploads where extension or type is in the accepted document mimes
public function documents(): MorphMany { }
Enter fullscreen mode Exit fullscreen mode

IMPORTANT

MorphOne relation method sets a limit of one in the query.

Lifecycle and Events

During the entire process of uploading your files, events are being fired in each step. This comes very helpful if you need to do something in between these steps or just for debugging purposes.

Event When It Is Fired What The Event Receives When Dispatched
NadLambino\Uploadable\Events\BeforeUpload::class Fired before the upload process starts Model $uploadable, array $files, UploadOptions $options
NadLambino\Uploadable\Events\StartUpload::class Fired when the upload process has started and its about to upload the first file in the list. This event may fired up multiple times depending on the number of files that is being uploaded Model $uploadable, string $filename, string $path
NadLambino\Uploadable\Events\AfterUpload::class Fired when the file was successfully uploaded and file information has been stored in the uploads table. This event may fired up multiple times depending on the number of files that is being uploaded Model $uploadable, Upload $upload
NadLambino\Uploadable\Events\CompleteUpload::class Fired when all of the files are uploaded and all of the necessary clean ups has been made Model $uploadable, Collection $uploads
NadLambino\Uploadable\Events\FailedUpload::class Fired when an exception was thrown while trying to upload a specific file. Throwable $exception, Model $uploadable

If you want to do something before the file information is stored to the uploads table, you can define the beforeSavingUpload public method in your model. This method will be called after the file is uploaded in the storage and before the file information is saved in the database.

public function beforeSavingUpload(Upload $upload, Model $model) : void
{
    $upload->additional_field = "some value";
}
Enter fullscreen mode Exit fullscreen mode

Alternatively, you can statically call the beforeSavingUploadUsing method and pass a closure.
The closure will receive the same parameters as the beforeSavingUpload method.
Just make sure that you call this method before creating or updating the uploadable model.
Also, beforeSavingUploadUsing has the higher precedence than the beforeSavingUpload allowing you to override it when needed.

Post::beforeSavingUploadUsing(function (Upload $upload, Post $model) use ($value) {
    $model->additional_field = $value;
});

$post->save();
Enter fullscreen mode Exit fullscreen mode

IMPORTANT

Remember, when you're on a queue, you are actually running your upload process in a different
application instance so you don't have access to the current application state like the request object.
Also, make sure that the closure and its dependencies you passed to the beforeSavingUploadUsing method are serializable.

Queueing

You can queue the file upload process by defining the queue name in the config.

'upload_on_queue' => null,
Enter fullscreen mode Exit fullscreen mode

Alternatively, you can also call the static method uploadOnQueue.

Post::uploadOnQueue('default');

$post->save();
Enter fullscreen mode Exit fullscreen mode

Top comments (0)