Upload Progress Finishes While Request Is Still Pending

So I'yard working on a cool projection, and I was working on a feature that processes incoming files and uploads them to deject storage. I idea Laravel batches would be ideal for this, which it was! I decided to combine the power of Laravel batches with events, Laravel Echo, and Livewire to testify existent-time progress to my users, and not to forget, some confetti to celebrate 🎉.

So in this article, I'yard going to show you footstep-by-step how to do this and then you can accident your customers away (with confetti).

What is Laravel Livewire?

Livewire is a full-stack framework for Laravel past Caleb Porzio that makes building dynamic  interfaces super easy without writing a unmarried line of Javascript, which is pretty awesome considering y'all tin can create a SPA like feeling, again without having to write any Javascript. As mentioned on the Livewire website, the best way to understand it is to expect at the code, so let's get started!

Installing Laravel, Livewire, and Tailwind CSS

I'm going to use a make clean install of Laravel 8, only y'all tin can, of course, follow along with one of your existing projects also.

Let's download and install Laravel, Livewire, and Tailwind CSS for those who want to start from scratch.

I volition be focusing on Laravel batches and real-time progress with Livewire. If you are entirely new to Laravel, Livewire, or Tailwind CSS, all of these products have all-encompassing and well-written documentation to help yous become started.

          # Install Laravel laravel new laravel-livewire-job-batches  # Crave Livewire composer require livewire/livewire  # Install Tailwind CSS npm install -D tailwindcss@latest postcss@latest autoprefixer@latest  # Create Tailwind CSS configuration file npx tailwindcss init                  

Next, we need to make a few more tweaks for Tailwind CSS to work. Let'due south update our webpack.mix.js first.

                      // webpack.mix.js   mix.js("resources/js/app.js", "public/js")     .postCss("resources/css/app.css", "public/css", [ +     require("tailwindcss"),     ]);                  

Open ./resources/css/app.css and add together the Tailwind CSS directives:

          @tailwind base of operations; @tailwind components; @tailwind utilities;                  

Next, nosotros desire to make sure we include our CSS inside our bract file and include the Livewire directives as well. And so open ./resources/views/welcome.bract.php

          <!DOCTYPE html> <html lang="{{ str_replace('_', '-', app()->getLocale()) }}"> <head>     <meta charset="utf-8">     <meta name="viewport" content="width=device-width, initial-scale=one">      <title>File Transfer</championship>      <link href="{{ asset('css/app.css') }}" rel="stylesheet">      @livewireStyles </head> <body>  @livewireScripts <script src="{{ asset('js/app.js') }}" type="text/javascript"></script> </body> </html>        

That should do the trick. Run npm run dev to make sure we accept a compiled CSS file.

Tailwind UI

I'm a large fan of Tailwind UI because it looks fantastic; it saves me many hours designing and writing markup, which I frankly don't enjoy every bit much as writing PHP. In this commodity, yous will see screenshots with Tailwind UI components while we progress. The HTML markup in this article, however, will non include whatsoever Tailwind UI components. I've made the following sample awarding in a couple of minutes:

Loftier-level overview

Nosotros want to achieve the following:

  1. As a user, we desire to upload ane or more than images.
  2. If a user provides an invalid file, we want to show an error message.
  3. We want to show a preview of the selected images which are valid.
  4. When the user submits their images successfully, the form should reset.
  5. When the user submits their images, our application should create a transfer object consisting of multiple transfer file objects.
  6. An TransferCreated event should occur with a listener, which will generate the batch and store the batch ID on the transfer object to runway the progress.
  7. Create a read-just model for the job_batches table so nosotros can eager-load our batches.
  8. Livewire should update the table in real-time to show the transfer status or progress and storage usage.
  9. Fire the confetti cannon

Create a Livewire Component

Allow'south kick off by creating our Livewire component:

          php artisan livewire:make ManageTransfers  COMPONENT CREATED  🤙  CLASS: app/Http/Livewire/ManageTransfers.php VIEW:  resource/views/livewire/manage-transfers.blade.php                  

Let'due south move our HTML into the manage-transfers.bract.php file.

          <div>     <table>         <thead>         <tr>             <thursday>&nbsp;</th>             <th>Status</thursday>             <th>Batch ID</th>             <th>Storage</th>         </tr>         </thead>         <tbody>         <tr>             <td>√</td>             <td>Uploaded</td>             <td>d9cbb5a7-ea12-42b4-9fb3-3e5a7f10631f</td>             <td>2MB</td>         </tr>         <tr>             <td>10</td>             <td>Finished with errors</td>             <td>0d669854-fb2c-480f-ae04-8572ec695242</td>             <td>0MB</td>         </tr>         <tr>             <td>!!</td>             <td>Failed</td>             <td>e176a925-8534-446f-a1f6-3fc2e06fcb0f</td>             <td>0MB</td>         </tr>         <tr>             <td>                 <svg                         xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">                     <circle                             stroke-width="4"></circle>                     <path                             d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 v.291A7.962 seven.962 0 014 12H0c0 3.042 i.135 5.824 3 7.938l3-2.647z"></path>                 </svg>             </td>             <td>                 <div grade="flex h-2 overflow-hidden rounded bg-grayness-50">                     <div style="transform: scale({{ fifty / 100 }}, ane)"                          course="bg-indigo-500 transition-transform origin-left duration-200 ease-in-out w-full shadow-none flex flex-col"></div>                 </div>             </td>             <td>                 296fc64e-af31-401d-9895-3d18ce02931c             </td>             <td>                 0MB             </td>         </tr>         </tbody>     </table>       <div>         <h3>Create Batch</h3>         <p>Select the files you want to upload.</p>          <div>             <input id="files" proper name="files" type="file" multiple>         </div>          <div>             Files         </div>           <div>             <img src="#" alt="">             <img src="#" alt="">             <img src="#" alt="">         </div>          <div>             <push button type="push button">                 Do some magic                  <svg                         xmlns="http://www.w3.org/2000/svg">                     <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"                           d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 vi.857L21 12l-five.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z"></path>                 </svg>             </button>         </div>     </div> </div>        

Also, add together the @livewire directive to welcome.blade.php and then the component will show upwards.

          <torso> @livewire('manage-transfers')        

Uploading files with Livewire

Livewire made uploading files a cakewalk! It'southward pretty crazy how elementary it is to go this to work.

Livewire file upload flow

Livewire will make a Mail request to the /livewire/upload-file endpoint and render the temporary filenames behind the scenes. The Livewire browser client will make a Postal service request to the Livewire component, which returns the HTML and updates the DOM to show the epitome previews.

We will get-go by defining a model for our file upload chemical element. In this case, let'south get with pendingFiles as this model will concord all the files a user selects simply are non nonetheless processed.

          <input id="files" wire:model="pendingFiles" type="file" multiple>        

Next, head over to the associated Livewire grade for this component (app/Http/Livewire/ManageTransfers.php) and wire this up (pun intended).

          <?php  namespace App\Http\Livewire;  use Livewire\Component; use Livewire\WithFileUploads;  form ManageTransfers extends Component {     use WithFileUploads;          public $pendingFiles = [];      public function render()     {         return view('livewire.manage-transfers');     } }        

To work with file uploads, you need to include the `Livewire\WithFileUploads` trait. This trait will consist of the necessary methods to process files uploaded via this component.

Tip: Sentry out when typed public properties in Livewire components. Livewire will, in some cases, throw an exception if the property is not a string, array, irritable value since information technology doesn't know what to practise. For example:

          // Using assortment equally typed property public array $pendingFiles = [];  // Volition throw a TypeError when you attempt to upload a file TypeError Cannot assign string to property App\Http\Livewire\ManageTransfers::$pendingFiles of type array        

Dorsum to the Livewire Magic. We've added a couple of lines of code, and our upload already works. To verify this, let's quickly add some temporary preview images:

          <div>     @forelse($pendingFiles equally $pendingFile)     <img src="{{ $pendingFile->temporaryUrl() }}"          alt="">     @empty     <p>No files selected</p>     @endforelse </div>        
Upload preview images

Now requite it a try, select a couple of images and watch the magic. Crazy right? And then how does this piece of work? Well, Livewire process all the selected files and place these files within a individual temporary directory.

Livewire keeps rail of all our uploads. The $pendingFiles property will return an array of Livewire\TemporaryUploadedFile objects. The temporaryUrl method volition return a signed road to make the uploaded file available. For security purposes, this temporary URL will only work for approved file extensions. So if you would upload a naught file, this will not work.

If y'all want to alter this default behavior, you can adjust the livewire.php configuration file. Yous demand to run the post-obit command to publish the configuration file:

          php artisan livewire:publish --config        

If y'all open this file, you tin scroll down to the 'Livewire Temporary File Uploads Endpoint Configuration' section and tweak the configuration to your liking:

          'temporary_file_upload' => [     'disk' => aught,             'rules' => null,            'directory' => goose egg,        'middleware' => null,       'preview_mimes' => [            'png', 'gif', 'bmp', 'svg', 'wav', 'mp4',         'mov', 'avi', 'wmv', 'mp3', 'm4a',         'jpg', 'jpeg', 'mpga', 'webp', 'wma',     ],     'max_upload_time' => 5,  ],        

Please consider that although Livewire ships with some sensible defaults like a 12MB upload limit and a throttle, information technology is possible for someone to make full your disk space if this person wants to. Then some additional security wouldn't hurt. You could, for example, limit the number of temporary files per user.

File validation with Livewire

Past default, Livewire implements the required, file, and max:12288 rules for whatever temporary file uploads. To add our validation, we can provide our rules to the validate method.

Let'south create a public method named initiateTransfer() and some of our own validation rules (file must be an image and a max size of 5MB), wire this upwardly to the push button so a user tin can submit the selected files for transfer.

          public function initiateTransfer() {     $this->validate([         'pendingFiles.*' => ['paradigm', 'max:5120']     ]); }                  
          <div>     <button wire:click="initiateTransfer" blazon="push">         Do some magic     </push> </div>        

If you lot upload a besides large image for example, y'all won't see errors just still. So let's add this to our view:

          <div>     @mistake('pendingFiles.*')     {{ $bulletin }}     @enderror </div>        

If I would upload a file that is 70MB, for case, information technology will fail immediately considering information technology does not pass the initial validation rule (12MB max) defined in the livewire.php configuration.

Validation error

If we would upload a half dozen.7MB image, you will run across the temporary preview only no mistake. The error will only show after the validation executes, and then if you click the 'Do some magic' button, you should see the error stating the file cannot exceed 5MB.

Eloquent models and database migrations

To rail the condition of our batch, we demand to attach our batch to something. For instance, yous might desire to attach these files to a document, projection; yous proper noun information technology. Y'all could also show the unabridged contents of the `job_batches` table, but I don't think that is a probable use case.

Job Batches Tabular array
Before we tin acceleration batches, nosotros need to run an artisan command to generate the database table that Laravel will use to persist data related to our batch. So run the following command:

          php artisan queue:batches-table Migration created successfully!        

The job_batches table contains the following Information:

  • The name of your job.
  • The total amount of jobs given batch has.
  • The full amount of pending jobs waiting to be processed by the queue worker.
  • The total amount of jobs that failed to exist candy past the queue worker.
  • The ID'southward of the failed jobs, this is a reference to the failed_jobs table.
  • Any options defined like then, take hold of or finally.
  • The timestamp when the batch was canceled.
  • The timestamp when the batch was created.
  • The timestamp when the queue worker finished processing all the jobs for a given batch.

We will employ some of this information in a future step to, for case, bear witness the progress of a item batch.

User model
I desire to demonstrate how to broadcast privately, so only the logged-in user volition see the transfer jobs and the existent-time progress. Laravel already ships with a user model, then the only thing you lot demand to do is open the DatabaseSeeder and uncomment the User manufacturing plant.

Scaffolding the UI for authentication etc. is out of scope for this commodity. However, I'd like to share an alternative when working locally and quickly login into whatever account without providing any credentials.

Open up your routes/web.php and add the following:

          Route::get('dev-login', function () {     abort_unless(app()->environment('local'), 403);      auth()->loginUsingId(App\User::first());      render redirect()->to('/'); });        

This volition only work locally due to the abort_unless role which will throw a 403 mistake if the environs is not equal to local. I'chiliad using the auth() helper to sign in to the first user in the database.

Transfer and File Model
We volition utilize an Eloquent model to rail all the files. As previously mentioned, yous could see this as a project (or email or tweet with one or more attachments), and y'all want to upload and associated specific files to this project.

So allow's create a model and migration for both the transfer and the file.

          php artisan brand:model Transfer --migration  Model created successfully. Created Migration: 2021_02_04_174530_create_transfers_table  php artisan make:model TransferFile --migration Model created successfully. Created Migration: 2021_02_04_174548_create_transfer_files_table        

For our transfers tabular array, nosotros only need to add together 1 additional field, which is the batch_id:

          Schema::create('transfers', role (Blueprint $tabular array) {     $table->id();     $tabular array->foreignId('user_id');     $table->foreignUuid('batch_id')->nullable();     $tabular array->timestamps(); });        

The transfer_files table will comprise the file path and the file size:

          Schema::create('transfer_files', role (Blueprint $table) {     $table->id();     $tabular array->foreignId('transfer_id');     $table->cord('disk');     $table->cord('path');     $tabular array->unsignedInteger('size');     $table->timestamps(); });        

While we are here, let'due south set up the relationship between them and define the fillable fields.

          <?php  namespace App\Models;  utilise Illuminate\Database\Eloquent\Model; apply Illuminate\Database\Eloquent\Relations\HasMany;  course Transfer extends Model {     /**      * The attributes that are mass assignable.      *      * @var string[]      */     protected $fillable = [         'batch_id',     ];          public function files(): HasMany     {         return $this->hasMany(TransferFile::class);     } }        
          <?php  namespace App\Models;  use Illuminate\Database\Eloquent\Model;  class TransferFile extends Model {     /**      * The attributes that are mass assignable.      *      * @var cord[]      */     protected $fillable = [         'disk',         'path',         'size'     ];      /**      * The attributes that should exist cast.      *      * @var assortment      */     protected $casts = [         'disk' => 'string',         'path' => 'cord',         'size' => 'integer'     ];      public office transfer(): BelongsTo     {         return $this->belongsTo(Transfer::class);     } }        

Persisting data to the database

Run php artisan migrate to execute all the migrations if y'all haven't already. We can now store the awaiting files that have passed validation.

          public function initiateTransfer() {     $this->validate([         'pendingFiles.*' => ['image', 'max:5120'],     ]);         // This code will non execute if the validation fails     $transfer = auth()->user()->transfers()->create();     $transfer->files()->saveMany(         collect($this->pendingFiles)             ->map(role (TemporaryUploadedFile $pendingFile) {                 render new TransferFile([                     'disk' => $pendingFile->disk,                     'path' => $pendingFile->getRealPath(),                     'size' => $pendingFile->getSize(),                 ]);             })     ); }        

When y'all select four images and click the magic button, you lot will see one new database entry in the transfers table and four entries in the transfer_files table.

Now let's reset the $pendingFiles belongings to an empty array to reset the form and finally consequence (we will create this class in the next footstep).

          public function initiateTransfer() {     $this->validate([         'pendingFiles.*' => ['image', 'max:5120'],     ]);      $transfer = auth()->user()->transfers()->create();     $transfer->files()->saveMany(       // ...     );      $this->pendingFiles = [];      LocalTransferCreated::acceleration($transfer); }        

Dispatch LocalTransferCreated issue

Now we accept all our data stored, we tin can dispatch an event that we tin listen for in futurity steps. Permit's create the event we referenced in the initiateTransfer method above:

                      php artisan brand:event LocalTransferCreated Result created successfully.        

We eventually want to update our table every fourth dimension the LocalTransferCreated consequence is dispatched. To exercise this, we volition employ Laravel Echo. We will talk most it in a hereafter footstep, but since nosotros are hither, let's make sure our event implements the ShouldBroadcast interface, so Laravel knows we desire to broadcast the effect.

          <?php  namespace App\Events;  utilize App\Models\Transfer; employ Illuminate\Dissemination\InteractsWithSockets; utilize Illuminate\Dissemination\PrivateChannel; use Illuminate\Contracts\Dissemination\ShouldBroadcast; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels;  class LocalTransferCreated implements ShouldBroadcast {     utilise Dispatchable, InteractsWithSockets, SerializesModels;      public function __construct(private Transfer $transfer)     {     }      public role getTransfer(): Transfer     {         return $this->transfer;     }          public function broadcastOn()     {         return new PrivateChannel('channel-name');     } }        

Create and assign listener

Permit'due south create a new listener that will listen to the LocalTransferCreated upshot and dispatch our batch.

          php artisan brand:listener CreateTransferBatch Listener created successfully.        

Next, add together the mapping to our EventServiceProvider to instruct Laravel to call our CreateTransferBatch when the LocalTransferCreated event is dispatched:

          <?php  namespace App\Providers;  use App\Events\LocalTransferCreated; apply App\Listeners\CreateTransferBatch; employ Illuminate\Foundation\Support\Providers\EventServiceProvider every bit ServiceProvider;  class EventServiceProvider extends ServiceProvider {     /**      * The consequence listener mappings for the application.      *      * @var array      */     protected $listen = [         LocalTransferCreated::class => [             CreateTransferBatch::class         ]     ]; }        

Create our transfer job

Before we can create our batch, we need to have a job to pass to our batch. And then let'southward create one:

          php artisan make:task TransferLocalFileToCloud Job created successfully.        

At present, this is a relatively uncomplicated job. We accept the local file, upload it to our cloud storage, update the database record by irresolute the deejay to `s3`, and the path to generated path containing a unique filename returned by the `put` method. Finally, we clean up by removing the file from our local storage.

To make your chore batchable, you demand to add the `Batchable` trait; otherwise, you will go the following exception:

          Call to undefined method App\Jobs\TransferLocalFileToCloud::withBatchId()        

Depending on your cloud storage provider, you may need to require the adapter, e.thousand. composer require "league/flysystem-aws-s3-v3 ~ane.0"

          <?php  namespace App\Jobs;  use App\Models\TransferFile; use Illuminate\Bus\Batchable; employ Illuminate\Motorbus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Http\File; apply Illuminate\Queue\InteractsWithQueue; utilise Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\Storage;  grade TransferLocalFileToCloud implements ShouldQueue {     utilize Batchable, Dispatchable, InteractsWithQueue, Queueable, SerializesModels;      public part __construct(private TransferFile $file)     {     }      public part handle()     {         $cloudPath = Storage::disk('s3')->put('images', new File($localPath = $this->file->path));          $this->file->update([             'deejay' => 's3',             'path' => $cloudPath,         ]);          Storage::delete(explode('/app/', $localPath)[one]);             // Dispatch consequence     } }        

Since nosotros want to update the interface in existent-time, nosotros need to dispatch an result every time a file is transferred. Let'southward create a new event:

          php artisan make:event FileTransferredToCloud  Event created successfully.        

Allow's take the Transfer model as our event parameter and brand sure the event implements the ShouldBroadcastNow interface. Nosotros desire to broadcast this event direct away, or else information technology will end up at the bottom of the queue backlog, causing this effect to trigger when the unabridged batch is already done. Given we want to bear witness the progress in real-time, nosotros need this circulate to happen in existent-fourth dimension.

          <?php  namespace App\Events;  use App\Models\TransferFile; utilise Illuminate\Broadcasting\InteractsWithSockets; use Illuminate\Dissemination\PrivateChannel; use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels;  form FileTransferredToCloud implements ShouldBroadcastNow {     use Dispatchable, InteractsWithSockets, SerializesModels;      public function __construct(private TransferFile $file)     {     }      public office broadcastOn()     {         return new PrivateChannel('aqueduct-name');     } }        

Now acceleration the event from our TransferLocalFileToCloud job:

          public role handle() {     // ...      FileTransferredToCloud::dispatch($this->file); }        

Create job batch

Fourth dimension to create our task batch inside our CreateTransferBatch listener. We can use the mapInto collection method to generate a job for each file quickly.

          <?php  namespace App\Listeners;  apply App\Events\LocalTransferCreated;  class CreateTransferBatch {     public function handle(LocalTransferCreated $event)     {         $jobs = $outcome->getTransfer()->files->mapInto(TransferLocalFileToCloud::class);     } }        

Now we can utilize the Batch facade to dispatch all these jobs in a single get.

          public role handle(LocalTransferCreated $effect) {     $jobs = $consequence->getTransfer()->files->mapInto(TransferLocalFileToCloud::grade);     $batch = Bus::batch($jobs)->acceleration(); }        

Feel free to requite it a try! The batch should transfer your files to your cloud storage. You can verify this by taking a await at your job_batches table. Hither yous should see a new entry and the number of jobs candy.

Saving & configuring our batch

The whole point of our batch is that we desire to show the progress to our users. So let'south start by attaching the batch ID to our Transfer model.

          public role handle(LocalTransferCreated $event) {     $jobs = $event->getTransfer()->files->mapInto(TransferLocalFileToCloud::form);     $batch = Bus::batch($jobs)->dispatch();      $upshot->getTransfer()->update([         'batch_id' => $batch->id     ]); }        

Next, we want to fire an consequence when all of our jobs are processed. The Batch facade provides a couple of methods you lot can use to achieve this.

          $batch = Autobus::batch($jobs)     ->and so(function (Batch $batch) {         // All jobs completed successfully     })->take hold of(function (Batch $batch, Throwable $e) {         // First batch job failure detected     })->finally(office (Batch $batch) {         // The batch has finished executing     })->acceleration();        

In our instance, we will only utilise the finally method to burn the TransferCompleted event. So let's create this event commencement.

          php artisan make:event TransferCompleted Event created successfully.                  

This event will accept the Transfer model as the parameter and implements the ShouldBroadcast interface every bit nosotros did before.

          <?php  namespace App\Events;  use App\Models\Transfer; utilise Illuminate\Broadcasting\InteractsWithSockets; use Illuminate\Dissemination\PrivateChannel; use Illuminate\Contracts\Broadcasting\ShouldBroadcast; utilise Illuminate\Foundation\Events\Dispatchable; employ Illuminate\Queue\SerializesModels;  class TransferCompleted implements ShouldBroadcast {     use Dispatchable, InteractsWithSockets, SerializesModels;          public function __construct(private Transfer $transfer)     {     }          public office broadcastOn()     {         return new PrivateChannel('channel-name');     } }        

Adjacent, nosotros volition add it to the finally closure.

          class CreateTransferBatch {     public function handle(LocalTransferCreated $event)     {         $transfer = $event->getTransfer();         $jobs = $transfer->files->mapInto(TransferLocalFileToCloud::course);          $batch = Autobus::batch($jobs)             ->finally(function () use ($transfer) {                 TransferCompleted::dispatch($transfer);             })->dispatch();          $upshot->getTransfer()->update([             'batch_id' => $batch->id         ]);     } }        

I've also assigned a variable to the transfer model to laissez passer the model instead of the entire upshot. Laravel will serialize the closure and store it in the options column of the job_batches table.

If you want, y'all can give the unabridged flow another become and use php artisan queue work to run into your processed jobs.

Pretty absurd, correct?

Listing our transfers

We can now make our static table dynamic by listing all the transfers. And then head back to your Livewire component grade and laissez passer an Eloquent collection containing our Transfer models to our view.

          public function render() {     render view('livewire.manage-transfers', [         'transfers' => auth()->user()->transfers     ]); }        

Allow'due south open our manage-transfers.blade.php and add together a @forelse to our tabular array.

          <tbody> @forelse($transfers as $transfer) <tr>     <td>         {{-- status icon --}}     </td>     <td>         {{-- status text --}}     </td>     <td>         {{ $transfer->batch_id }}     </td>     <td>         {{-- combined file size of transfer files --}}     </td> </tr> @empty <tr>     <td colspan="4">         You accept no transfers. Create a batch on the right 👉🏻     </td> </tr> @endforelse </tbody>        

Nosotros need to show our job condition and calculate the combined size of all the transfer files. Now Laravel ships with a method to lookup batches by their ID: Passenger vehicle::findBatch($batchId);. This method volition use the `DatabaseBatchRepository` behind the scenes to fetch the information from the database and transform the data into a \Illuminate\Charabanc\Batch object.

The findBatch method works perfectly, but I would love to use Eloquent instead to eager load all the batches, perform query scopes, etc. And then let's create a ready-only model for our batches.

Laravel is responsible for maintaining the data integrity of our job_batches table, and I want to proceed it that fashion, so that's why I like the model to be read-only.

Packagist to the rescue. If you do a quick search for read-but, you will see the post-obit packet prove upwards. This package will innovate a trait to make models read-only. So let'due south install it:

          composer require michaelachrisco/readonly        

Adjacent, we also desire to create our model:

          php artisan make:model JobBatch        

Permit'south showtime by adding the read-only trait to our model, and then our model volition throw an exception if yous call a method like create, salvage, delete, etc.

          <?php  namespace App\Models;  use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; apply MichaelAChrisco\ReadOnly\ReadOnlyTrait;  grade JobBatch extends Model {     use ReadOnlyTrait; }        

If we have a quick await at the job_batches table, you tin come across each batch has a UUID instead of an incrementing integer, and it too does not accept the updated_at timestamp. Then let'south update our model:

          form JobBatch extends Model {     use ReadOnlyTrait;      /**      * The table associated with the model.      *      * @var cord      */     protected $table = 'job_batches';      /**      * The "type" of the primary key ID.      *      * @var string      */     protected $keyType = 'string';      /**      * Indicates if the IDs are motorcar-incrementing.      *      * @var bool      */     public $incrementing = false;      /**      * Indicates if the model should be timestamped.      *      * @var bool      */     public $timestamps = false; }        

To make things a bit easier, we can cast the model backdrop, then we go a Carbon object when nosotros do something similar $jobBatch->finished_at or a drove when we exercise $jobBatch->options:

          course JobBatch extends Model {     // ....      /**      * The attributes that should exist cast.      *      * @var assortment      */     protected $casts = [         'options'      => 'collection',         'failed_jobs'  => 'integer',         'created_at'   => 'datetime',         'cancelled_at' => 'datetime',         'finished_at'  => 'datetime',     ]; }        

Next, we desire to copy some methods that we would typically become from theIlluminate\Bus\Batch object:

  • processedJobs: Get the full number of jobs that have been processed by the batch thus far.
  • progress: Get the percent of jobs that accept been candy (between 0-100).
  • finished: Determine if the batch has finished executing.
  • hasFailues: Determine if the batch has job failures.
  • canceled: Determine if anything canceled the batch.

We tin duplicate most of these methods into our model and suit the lawmaking accordingly to get to data from our model:

          class JobBatch extends Model {     // ...      /**      * Go the total number of jobs that have been processed by the batch thus far.      *      * @return int      */     public function processedJobs()     {         return $this->total_jobs - $this->pending_jobs;     }      /**      * Get the per centum of jobs that have been processed (between 0-100).      *      * @render int      */     public role progress(): int     {         return $this->total_jobs > 0 ? round(($this->processedJobs() / $this->total_jobs) * 100) : 0;     }      /**      * Determine if the batch has pending jobs      *      * @render bool      */     public function hasPendingJobs(): bool     {         return $this->pending_jobs > 0;     }      /**      * Determine if the batch has finished executing.      *      * @return bool      */     public role finished(): bool     {         return !is_null($this->finished_at);     }      /**      * Decide if the batch has job failures.      *      * @return bool      */     public role hasFailures(): bool     {         return $this->failed_jobs > 0;     }      /**      * Determine if all jobs failed.      *      * @return bool      */     public function failed(): bool     {         render $this->failed_jobs === $this->total_jobs;     }      /**      * Determine if the batch has been canceled.      *      * @render bool      */     public function cancelled(): bool     {         return !is_null($this->cancelled_at);     } }        

We are almost ready to eager-load our job batches. The only thing that remains is defining the relationship. So permit's open up our Transfer model and add the relationship:

          class Transfer extends Model {     // ...      public role jobBatch(): BelongsTo     {         return $this->belongsTo(JobBatch::grade, 'batch_id');     } }        

Now we can update our code inside our ManageTransfers Livewire component to eager-load our batches.

          class ManageTransfers extends Component {     // ...      public function render()     {         return view('livewire.manage-transfers', [             'transfers' => auth()->user()->transfers()->with('jobBatch')->become(),         ]);     } }        

Finally, permit's update our HTML to reflect the unlike task states:

          @forelse($transfers as $transfer) <tr form="bg-white">      @if(is_null($transfer->jobBatch))      <td>         %     </td>     <td>         <div grade="flex h-2 overflow-hidden rounded bg-grey-l">             <div way="transform: scale(0, 1)"                  class="bg-indigo-500 transition-transform origin-left duration-200 ease-in-out w-total shadow-none flex flex-col"></div>         </div>     </td>     @elseif($transfer->jobBatch->hasPendingJobs())     <td>         %     </td>     <td>         <div grade="flex h-2 overflow-subconscious rounded bg-gray-50">             <div mode="transform: scale({{ $transfer->jobBatch->progress() / 100 }}, 1)"                  class="bg-indigo-500 transition-transform origin-left duration-200 ease-in-out due west-full shadow-none flex flex-col"></div>         </div>     </td>      @elseif($transfer->jobBatch->finished() and $transfer->jobBatch->failed())      <td>         Ten     </td>     <td>         Failed     </td>      @elseif($transfer->jobBatch->finished() and $transfer->jobBatch->hasFailures())      <td>         !!     </td>     <td>         Finished with errors     </td>      @elseif($transfer->jobBatch->finished())      <td>         √     </td>     <td>         Uploaded     </td>     @endif     <td>         {{ $transfer->batch_id }}     </td>     <td>         {{-- combined file size of transfer files --}}     </td> </tr> @empty <tr>     <td colspan="4">         You have no transfers. Create a batch on the right 👉🏻     </td> </tr> @endforelse        

Not bad! Things are starting to take shape. Let's finish our tabular array view by showing the combined size of all the files. We could do this by eager-loading the files and summing upward the total size:

          return view('livewire.manage-transfers', [     'transfers' => auth()->user()->transfers()->with('jobBatch', 'files')->get(), ]);  // $transfer->files->sum('size');        

But that would load all the file models; although information technology works, we can optimize this a bit by letting Eloquent calculate the sum via SQL.

          render view('livewire.manage-transfers', [     'transfers' => auth()->user()->transfers()->with('jobBatch')->withSum('files', 'size')->get(), ]);  // $transfer->files_sum_size        

Quite a handy niggling helper to improve the operation of your application.    At present our view is ready, and you should have something like this:

If you want to encounter the different states, y'all can fake this by adjusting the job_batches entries. For example, set the failed_jobs or pending_jobs column to i.

Real-time progress using Laravel Echo

The moment anybody has been waiting for, showing transfer progress and results to our users in real-time. We are going to make this happen by using Laravel Echo and Livewire. Then before nosotros move on, allow's install the Laravel Echo Javascript library.

          npm install --salvage-dev laravel-echo pusher-js        

Next, you can uncomment the following code in your bootstrap.js file:

          import Repeat from 'laravel-echo';  window.Pusher = require('pusher-js');  window.Echo = new Echo({     broadcaster: 'pusher',     fundamental: process.env.MIX_PUSHER_APP_KEY,     cluster: process.env.MIX_PUSHER_APP_CLUSTER,     forceTLS: true });        

In this example, I will use Pusher equally our broadcaster. You can sign-upwardly for a costless business relationship and get your account credentials. Yous need to add these in your .env file.

          BROADCAST_DRIVER=pusher PUSHER_APP_ID=10000 PUSHER_APP_KEY=h28f720sd6v5a02 PUSHER_APP_SECRET=jh8s02017cx0add PUSHER_APP_CLUSTER=eu        

Run npm run dev once you take the npm package installed and the surround variables defined. Next, we as well need to require the Pusher SDK:

          composer require pusher/pusher-php-server "~4.0"        

Aqueduct configuration
We accept three events that are broadcasting:

  1. LocalTransferCreated
  2. LocalTransferCreated
  3. TransferCompleted

These events are broadcasting to a private aqueduct. Let's return a channel name that includes the user id (possessor) of a given transfer model.

In near cases, yous would use private channels, but if you lot don't demand any hallmark, you can return an Illuminate\Broadcasting\Channel object instead.

          course FileTransferredToCloud implements ShouldBroadcastNow {     // ...      public function broadcastOn()     {         return new PrivateChannel("notifications.{$this->file->transfer->user_id}");     } }   class LocalTransferCreated implements ShouldBroadcast {     // ...      public function broadcastOn()     {         return new PrivateChannel("notifications.{$this->transfer->user_id}");     } }  class TransferCompleted implements ShouldBroadcast {     // ...     public function broadcastOn()     {         return new PrivateChannel("notifications.{$this->transfer->user_id}");     } }        

Channel dominance
Laravel Repeat will request a particular endpoint to verify if you take access to a private aqueduct. This endpoint doesn't exist by default considering the BroadcastServiceProvider is disabled by default. Open up your app.php and enable App\Providers\BroadcastServiceProvider::class

Next we can ascertain the authorization in the routes/channels.php file.

          Broadcast::channel('notifications.{channelUser}', function ($authUser, \App\Models\User $channelUser) {     return $authUser->id === $channelUser->id; });        

The closure has two values; the first value is the electric current logged in user, the 2d value is the broadcast user id, which is automatically resolved when you type-hint the closure only similar when you use routes.

Integrating Laravel Livewire with Laravel Echo
We can now configure our Livewire component and instruct it to refresh itself when we receive a new event from Pusher. Livewire has native support for Laravel Repeat, so this only requires a couple of lines of code. Open the ManageTransfers component and create a new getListeners() method.

          public function getListeners() {     return []; }        

This method should return a key-value array with the key beingness the consequence to listen to and the value being the method to trigger when the effect occurs.

In our case, we want to refresh the unabridged component. The cool thing about Livewire is that information technology knows which elements have inverse and only updates the DOM for those elements. This means CSS animations will work, and nosotros volition see our progress bar update with a cool blitheness.

          public part getListeners() {     $userId = auth()->id();      render [         "echo-private:notifications.{$userId},FileTransferredToCloud" => '$refresh',     ]; }        

If you use Laravel Repeat, yous need to prefix your notification with echo-private then Livewire knows information technology needs to piece of work together with Laravel Repeat. Next, nosotros provide information technology our private channel name notifications.{$userId} followed past the event we want to listen to, in this case, FileTransferredToCloud.  We don't have whatever specific method we desire to trigger; we just want to refresh the component by passing it $refresh.

That's information technology! Get and give it a try and watch the magic happen.

Confetti bonus

What'due south existent-time progress without some confetti? Exactly, admittedly nothing! Let's give it a blast of confetti when information technology's washed with the transfer.

Let's install our confetti cannon:

          npm install --save sheet-confetti        

Open resources/app.js and use the Livewire.on() role to mind for a confetti event:

          import confetti from "canvas-confetti";  Livewire.on('confetti', () => {     confetti({         particleCount: 80,         spread: 200,         origin: {y: 0.half dozen}     }); })        

Next, head dorsum to the Livewire component and register a new listener.

          public function getListeners() {     $userId = auth()->id();      render [         "echo-private:notifications.{$userId},FileTransferredToCloud" => '$refresh',         "echo-private:notifications.{$userId},TransferCompleted" => 'fireConfettiCannon',     ]; }        

Next, create a new method chosen fireConfettiCannon and use the emit helper to emit the confetti result:

          public role fireConfettiCannon() {     $this->emit('confetti'); }        

That's a wrap! 🎉 I'd love to hear how y'all have used this technique to show real-time progress (and maybe some confetti) to your customers. Drop me a tweet @Philo01 and talk! 🙌🏼


Manage multiple Larave Horizon instances with Observer

All your favorite Laravel Horizon features (and a few new ones) are packed into a single desktop application. A must-take productivity booster for every Laravel programmer.


Premium Series: Build a real-world application with Laravel, Livewire & Tailwind

I'm thinking most releasing a premium video course in the most future (if plenty people are interested) where I volition show you how to build a real-world awarding from start to finish using Laravel, Livewire, and Tailwind CSS. I will be covering everything, from registering a domain, setting upwards a server, writing tests, you proper noun it. Subscribe if y'all are interested and want to exist notified, and yous might likewise get access for free as I volition exist doing a give away when the course launches.

coopersuchaked.blogspot.com

Source: https://philo.dev/laravel-batches-and-real-time-progress-with-livewire/

0 Response to "Upload Progress Finishes While Request Is Still Pending"

Post a Comment

Iklan Atas Artikel

Iklan Tengah Artikel 1

Iklan Tengah Artikel 2

Iklan Bawah Artikel