Pracuję nad fajnym projektem i nad funkcją, która przetwarza przychodzące pliki i przesyła je do pamięci masowej w chmurze. Pomyślałem, że partie Laravel będą do tego idealne, co było! Postanowiłem połączyć moc partii Laravel z wydarzeniami, Laravel Echo i Livewire, aby pokazać postępy w czasie rzeczywistym moim użytkownikom i nie zapomnieć o konfetti do świętowania 🎉 .
W tym artykule pokażę ci krok po kroku, jak to zrobić, abyś mógł zdmuchnąć swoich klientów (konfetti).
Czym jest Laravel Livewire?
Livewire to pełny framework dla Laravel autorstwa Caleba Porzio, który sprawia, że budowanie dynamicznych interfejsów jest bardzo łatwe bez pisania ani jednej linii Javascript, co jest całkiem niesamowite, ponieważ możesz stworzyć uczucie SPA, ponownie bez konieczności pisania Javascript. Jak wspomniano na stronie Livewire, najlepszym sposobem na zrozumienie tego jest spojrzenie na kod, więc zacznijmy!
Instalowanie Laravel, Livewire i Tailwind CSS
Zamierzam użyć czystej instalacji Laravel 8, ale możesz oczywiście śledzić również jeden z istniejących projektów.
Pobierzmy i zainstalujmy Laravel, Livewire i Tailwind CSS dla tych, którzy chcą zacząć od zera.
Skupię się na partiach Laravel i postępach w czasie rzeczywistym z Livewire. Jeśli jesteś zupełnie nowy w Laravel, Livewire lub Tailwind CSS, wszystkie te produkty mają obszerną i dobrze napisaną dokumentację, która pomoże Ci zacząć.
# Install Laravel
laravel new laravel-livewire-job-batches
# Require 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
Następnie musimy wprowadzić jeszcze kilka poprawek, aby Tailwind CSS działał. Zaktualizujmy nasz webpack.mix.js
pierwszy.
// webpack.mix.js
mix.js("resources/js/app.js", "public/js")
.postCss("resources/css/app.css", "public/css", [
+ require("tailwindcss"),
]);
Otwórz ./resources/css/app.css
i dodaj dyrektywy CSS Tailwind:
@tailwind base;
@tailwind components;
@tailwind utilities;
Następnie chcemy się upewnić, że dołączamy nasz CSS do naszego pliku blade i uwzględniamy również dyrektywy Livewire. Więc otwórz ./resources/views/welcome.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">
<title>File Transfer</title>
<link href="{{ asset('css/app.css') }}" rel="stylesheet">
@livewireStyles
</head>
<body>
@livewireScripts
<script src="{{ asset('js/app.js') }}" type="text/javascript"></script>
</body>
</html>
To powinno załatwić sprawę. Uruchomnpm run dev
, aby upewnić się, że mamy skompilowany plik CSS.
Tailwind UI Jestem wielkim fanem Tailwind UI
I'm a big fan of Tailwind UI Jestem wielkim fanem Tailwind UI because it looks fantastic; it saves me many hours designing and writing markup, which I frankly don't enjoy as much as writing PHP. In this article, you will see screenshots with Tailwind UI Jestem wielkim fanem Tailwind UI components while we progress. The HTML markup in this article, however, will not include any Tailwind UI Jestem wielkim fanem Tailwind UI components. I've made the following sample application in a couple of minutes:
wysokiego poziomu Chcemy osiągnąć następujące cele:Utwórz komponent Livewire Zacznijmy
od utworzenia naszego komponentu Livewire:
- As a user, we want to upload one or more images.
- If a user provides an invalid file, we want to show an error message.
- We want to show a preview of the selected images which are valid.
- When the user submits their images successfully, the form should reset.
- When the user submits their images, our application should create a transfer object consisting of multiple transfer file objects.
- An TransferCreated event should occur with a listener, which will generate the batch and store the batch ID on the transfer object to track the progress.
- Create a read-only model for the
job_batches
table so we can eager-load our batches. - Livewire should update the table in real-time to show the transfer status or progress and storage usage.
- Fire the confetti cannon
php artisan livewire:make ManageTransfers
COMPONENT CREATED 🤙
CLASS: app/Http/Livewire/ManageTransfers.php
VIEW: resources/views/livewire/manage-transfers.blade.php
Przenieśmy nasz kod HTML do manage-transfers.blade.php
pliku.
<div>
<table>
<thead>
<tr>
<th> </th>
<th>Status</th>
<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>X</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 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</td>
<td>
<div class="flex h-2 overflow-hidden rounded bg-gray-50">
<div style="transform: scale({{ 50 / 100 }}, 1)"
class="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" name="files" type="file" multiple>
</div>
<div>
Files
</div>
<div>
<img src="#" alt="">
<img src="#" alt="">
<img src="#" alt="">
</div>
<div>
<button type="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 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z"></path>
</svg>
</button>
</div>
</div>
</div>
Dodaj również dyrektywę @livewire, aby welcome.blade.php
komponent się pojawił.
<body>
@livewire('manage-transfers')
Przesyłanie plików za pomocą Livewire Livewire
sprawiło, że przesyłanie plików było dziecinnie proste! To dość szalone, jak łatwo jest to uruchomić.
Livewire wyśle żądanie POST do /livewire/upload-file
punktu końcowego i zwróci tymczasowe nazwy plików za kulisami. Klient przeglądarki Livewire wyśle żądanie POST do komponentu Livewire, który zwróci kod HTML i zaktualizuje model DOM, aby wyświetlić podglądy obrazów.
Zaczniemy od zdefiniowania modelu dla naszego elementu przesyłania plików. W tym przypadku przejdźmy do pendingFiles
tego, ponieważ ten model będzie zawierał wszystkie pliki wybrane przez użytkownika, ale nie są jeszcze przetwarzane.
<input id="files" wire:model="pendingFiles" type="file" multiple>
Następnie przejdź do powiązanej klasy Livewire dla tego komponentu (app / HTTP / Livewire / ManageTransfers.php) i połącz to (gra słów przeznaczona).
<?php
namespace App\Http\Livewire;
use Livewire\Component;
use Livewire\WithFileUploads;
class ManageTransfers extends Component
{
use WithFileUploads;
public $pendingFiles = [];
public function render()
{
return view('livewire.manage-transfers');
}
}
Aby pracować z przesyłaniem plików, musisz uwzględnić cechę "Livewire\WithFileUploads". Ta cecha będzie składać się z niezbędnych metod przetwarzania plików przesłanych za pośrednictwem tego komponentu.
Wskazówka: Uważaj podczas wpisywania właściwości publicznych w komponentach Livewire. Livewire w niektórych przypadkach zgłosi wyjątek, jeśli właściwość nie jest ciągiem, tablicą, drażliwą wartością, ponieważ nie wie, co robić. Na przykład:
// Using array as typed property
public array $pendingFiles = [];
// Will throw a TypeError when you try to upload a file
TypeError Cannot assign string to property App\Http\Livewire\ManageTransfers::$pendingFiles of type array
Powrót do Livewire Magic. Dodaliśmy kilka linijek kodu, a nasze przesyłanie już działa. Aby to sprawdzić, szybko dodajmy tymczasowe obrazy podglądu:
<div>
@forelse($pendingFiles as $pendingFile)
<img src="{{ $pendingFile->temporaryUrl() }}"
alt="">
@empty
<p>No files selected</p>
@endforelse
</div>
Teraz spróbuj, wybierz kilka obrazów i obejrzyj magię. Szalone, prawda? Jak to działa? Cóż, Livewire przetwarza wszystkie wybrane pliki i umieszcza te pliki w prywatnym katalogu tymczasowym.
Livewire śledzi wszystkie przesłane przez nas pliki. Właściwość $pendingFiles
zwróci tablicę Livewire\TemporaryUploadedFile
obiektów. Metoda temporaryUrl
zwróci podpisaną trasę, aby udostępnić przesłany plik. Ze względów bezpieczeństwa ten tymczasowy adres URL będzie działał tylko w przypadku zatwierdzonych rozszerzeń plików. Więc jeśli prześlesz plik zip, to nie zadziała.
Jeśli chcesz zmienić to domyślne zachowanie, możesz dostosować plik konfiguracyjnylivewire.php
. Musisz uruchomić następujące polecenie, aby opublikować plik konfiguracyjny:Jeśli otworzysz ten plik, możesz przewinąć w dół do sekcji "Livewire Temporary File Uploads Endpoint Configuration" i dostosować konfigurację do własnych upodobań:
php artisan livewire:publish --config
'temporary_file_upload' => [
'disk' => null,
'rules' => null,
'directory' => null,
'middleware' => null,
'preview_mimes' => [
'png', 'gif', 'bmp', 'svg', 'wav', 'mp4',
'mov', 'avi', 'wmv', 'mp3', 'm4a',
'jpg', 'jpeg', 'mpga', 'webp', 'wma',
],
'max_upload_time' => 5,
],
Proszę wziąć pod uwagę, że chociaż Livewire jest dostarczany z pewnymi rozsądnymi ustawieniami domyślnymi, takimi jak limit wysyłania 12 MB i przepustnica, możliwe jest, że ktoś zapełni miejsce na dysku, jeśli ta osoba chce. Więc dodatkowe zabezpieczenia nie zaszkodzią. Można na przykład ograniczyć liczbę plików tymczasowych na użytkownika.
Sprawdzanie poprawności plików za pomocą Livewire Domyślnie Livewire
implementuje required
, file
i max:12288
reguły dla wszelkich tymczasowych przesyłanych plików. Aby dodać naszą walidację, możemy podać nasze reguły do validate
metody.
Stwórzmy publiczną metodę o nazwie initiateTransfer()
i niektóre z naszych własnych reguł sprawdzania poprawności (plik musi być obrazem i maksymalnym rozmiarem 5 MB), podłącz to do przycisku, aby użytkownik mógł przesłać wybrane pliki do przesłania.
public function initiateTransfer()
{
$this->validate([
'pendingFiles.*' => ['image', 'max:5120']
]);
}
<div>
<button wire:click="initiateTransfer" type="button">
Do some magic
</button>
</div>
Jeśli na przykład prześlesz zbyt duży obraz, nie zobaczysz jeszcze błędów. Dodajmy więc to do naszego widoku:
<div>
@error('pendingFiles.*')
{{ $message }}
@enderror
</div>
Jeśli na przykład prześlę plik o rozmiarze 70 MB, natychmiast się nie powiedzie, ponieważ nie przejdzie początkowej reguły walidacji (maks. 12 MB) zdefiniowanej w konfiguracji livewire.php.
Jeśli prześlemy obraz o rozmiarze 6,7 MB, zobaczysz tymczasowy podgląd, ale bez błędu. Błąd pojawi się dopiero po wykonaniu walidacji, więc jeśli klikniesz przycisk "Zrób trochę magii", powinieneś zobaczyć błąd informujący, że plik nie może przekroczyć 5 MB.
Wymowne modele i migracje
baz danych Aby śledzić status naszej partii, musimy dołączyć naszą partię do czegoś. Na przykład można dołączyć te pliki do dokumentu, projektu; Nazwij to. Możesz również pokazać całą zawartość tabeli "job_batches", ale nie sądzę, że jest to prawdopodobny przypadek użycia.
Tabela partii zadań
Zanim będziemy mogli wysyłać partie, musimy uruchomić polecenie rzemieślnicze, aby wygenerować tabelę bazy danych, której Laravel użyje do utrwalenia danych związanych z naszą partią. Uruchom więc następujące polecenie:Tabela job_batches
zawiera następujące informacje:
php artisan queue:batches-table
Migration created successfully!
- Nazwa Twojej pracy.
- Łączna liczba zadań podanej partii ma.
- Łączna liczba oczekujących zadań oczekujących na przetworzenie przez pracownika kolejki.
- Łączna liczba zadań, które nie zostały przetworzone przez pracownika kolejki.
- Identyfikatory nieudanych zadań, jest to odwołanie do
failed_jobs
tabeli. - Wszystkie opcje zdefiniowane na przykład
then
,catch
lubfinally
. - Sygnatura czasowa anulowania partii.
- Sygnatura czasowa utworzenia partii.
- Sygnatura czasowa, gdy pracownik kolejki zakończy przetwarzanie wszystkich zadań dla danej partii.
Niektóre z tych danych wykorzystamy w przyszłym kroku, aby na przykład pokazać postęp konkretnej partii.
Model użytkownika
Chcę zademonstrować, jak nadawać prywatnie, aby tylko zalogowany użytkownik widział zadania transferu i postęp w czasie rzeczywistym. Laravel jest już dostarczany z modelem użytkownika, więc jedyne, co musisz zrobić, to otworzyć DatabaseSeeder
i odkomentować fabrykę użytkownika.
Rusztowanie, interfejs użytkownika do uwierzytelniania itp. jest poza zakresem tego artykułu. Mimo to chciałbym podzielić się alternatywą podczas pracy lokalnej i szybko zalogować się na dowolne konto bez podawania żadnych danych uwierzytelniających.
Otwórz i routes/web.php
dodaj następujące elementy:
Route::get('dev-login', function () {
abort_unless(app()->environment('local'), 403);
auth()->loginUsingId(App\User::first());
return redirect()->to('/');
});
Będzie działać tylko lokalnie ze względu na abort_unless
funkcję, która wygeneruje błąd 403, jeśli środowisko nie jest równe local
. Używam pomocnika auth()
do logowania się do pierwszego użytkownika w bazie danych.
Przesyłanie i model plików
Użyjemy modelu Eloquent do śledzenia wszystkich plików. Jak wspomniano wcześniej, możesz zobaczyć to jako projekt (lub e-mail lub tweet z jednym lub więcej załącznikami) i chcesz przesłać i powiązane określone pliki do tego projektu.
Stwórzmy więc model i migrację zarówno dla transferu, jak i pliku.
php artisan make: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
Dla naszej transfers
tabeli musimy tylko dodać jedno dodatkowe pole, którym jest batch_id
:Tabela transfer_files
będzie zawierać ścieżkę pliku i rozmiar pliku:
Schema::create('transfers', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id');
$table->foreignUuid('batch_id')->nullable();
$table->timestamps();
});
Schema::create('transfer_files', function (Blueprint $table) {
$table->id();
$table->foreignId('transfer_id');
$table->string('disk');
$table->string('path');
$table->unsignedInteger('size');
$table->timestamps();
});
Skoro tu jesteśmy, skonfigurujmy relację między nimi i zdefiniujmy pola do wypełnienia.
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class 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 string[]
*/
protected $fillable = [
'disk',
'path',
'size'
];
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'disk' => 'string',
'path' => 'string',
'size' => 'integer'
];
public function transfer(): BelongsTo
{
return $this->belongsTo(Transfer::class);
}
}
Utrwalanie danych w bazie danych
Uruchomphp artisan migrate
, aby wykonać wszystkie migracje, jeśli jeszcze tego nie zrobiono. Możemy teraz przechowywać oczekujące pliki, które przeszły weryfikację.
public function initiateTransfer()
{
$this->validate([
'pendingFiles.*' => ['image', 'max:5120'],
]);
// This code will not execute if the validation fails
$transfer = auth()->user()->transfers()->create();
$transfer->files()->saveMany(
collect($this->pendingFiles)
->map(function (TemporaryUploadedFile $pendingFile) {
return new TransferFile([
'disk' => $pendingFile->disk,
'path' => $pendingFile->getRealPath(),
'size' => $pendingFile->getSize(),
]);
})
);
}
Po wybraniu czterech obrazów i kliknięciu magicznego przycisku zobaczysz jeden nowy wpis bazy danych w tabeli i cztery wpisy w transfers
transfer_files
tabeli.
Teraz zresetujmy $pendingFiles
właściwość do pustej tablicy, aby zresetować formularz i na koniec zdarzenie (utworzymy tę klasę w następnym kroku).
public function initiateTransfer()
{
$this->validate([
'pendingFiles.*' => ['image', 'max:5120'],
]);
$transfer = auth()->user()->transfers()->create();
$transfer->files()->saveMany(
// ...
);
$this->pendingFiles = [];
LocalTransferCreated::dispatch($transfer);
}
Zdarzenie Dispatch LocalTransferCreated
Teraz mamy już zapisane wszystkie nasze dane, możemy wysłać zdarzenie, którego będziemy mogli nasłuchiwać w przyszłych krokach. Utwórzmy zdarzenie, do którego odwołujemy się w initiateTransfer
powyższej metodzie:
php artisan make:event LocalTransferCreated
Event created successfully.
Ostatecznie chcemy aktualizować naszą tabelę za każdym razem, LocalTransferCreated
gdy zdarzenie jest wywoływane. Aby to zrobić, użyjemy Laravel Echo. Porozmawiamy o tym w przyszłości, ale skoro tu jesteśmy, upewnijmy się, że nasze wydarzenie implementuje ShouldBroadcast
interfejs, aby Laravel wiedział, że chcemy transmitować wydarzenie.
<?php
namespace App\Events;
use App\Models\Transfer;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class LocalTransferCreated implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(private Transfer $transfer)
{
}
public function getTransfer(): Transfer
{
return $this->transfer;
}
public function broadcastOn()
{
return new PrivateChannel('channel-name');
}
}
Utwórz i przypisz detektor Stwórzmy nowego detektora
, który będzie nasłuchiwał LocalTransferCreated
zdarzenia i wysyłał naszą partię.
php artisan make:listener CreateTransferBatch
Listener created successfully.
Następnie dodaj mapowanie do naszegoEventServiceProvider
, aby poinstruować Laravel, aby zadzwonił do nasCreateTransferBatch
, gdy LocalTransferCreated
zdarzenie zostanie wysłane:
<?php
namespace App\Providers;
use App\Events\LocalTransferCreated;
use App\Listeners\CreateTransferBatch;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
class EventServiceProvider extends ServiceProvider
{
/**
* The event listener mappings for the application.
*
* @var array
*/
protected $listen = [
LocalTransferCreated::class => [
CreateTransferBatch::class
]
];
}
Utwórz nasze zadanie transferu Zanim będziemy mogli utworzyć naszą partię, musimy mieć zadanie
do przekazania do naszej partii. Stwórzmy jeden:
php artisan make:job TransferLocalFileToCloud
Job created successfully.
Teraz jest to stosunkowo prosta praca. Pobieramy plik lokalny, przesyłamy go do naszego magazynu w chmurze, aktualizujemy rekord bazy danych, zmieniając dysk na "s3", a ścieżkę do wygenerowanej ścieżki zawierającej unikalną nazwę pliku zwróconą przez metodę "put". Na koniec sprzątamy, usuwając plik z naszej lokalnej pamięci masowej.
Aby Twoje zadanie było możliwe do wykonania partii, musisz dodać cechę "Batchable"; W przeciwnym razie otrzymasz następujący wyjątek:
Call to undefined method App\Jobs\TransferLocalFileToCloud::withBatchId()
W zależności od dostawcy pamięci masowej w chmurze może być konieczne wymaganie adaptera, np. Ponieważ chcemy aktualizować interfejs w czasie rzeczywistym, musimy wysyłać zdarzenie za każdym razem, gdy plik jest przesyłany.composer require "league/flysystem-aws-s3-v3 ~1.0"
<?php
namespace App\Jobs;
use App\Models\TransferFile;
use Illuminate\Bus\Batchable;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Http\File;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Storage;
class TransferLocalFileToCloud implements ShouldQueue
{
use Batchable, Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(private TransferFile $file)
{
}
public function handle()
{
$cloudPath = Storage::disk('s3')->put('images', new File($localPath = $this->file->path));
$this->file->update([
'disk' => 's3',
'path' => $cloudPath,
]);
Storage::delete(explode('/app/', $localPath)[1]);
// Dispatch event
}
}
Utwórzmy nowe zdarzenie:Weźmy model transferu jako parametr zdarzenia i upewnijmy się,
php artisan make:event FileTransferredToCloud
Event created successfully.
że zdarzenie implementuje interfejs ShouldBroadcastNow. Chcemy natychmiast rozgłaszać to zdarzenie, w przeciwnym razie skończy się ono na dole zaległości kolejki, powodując wyzwolenie tego zdarzenia, gdy cała partia jest już zakończona. Biorąc pod uwagę, że chcemy pokazać postępy w czasie rzeczywistym, potrzebujemy tej transmisji, aby odbywała się w czasie rzeczywistym.
<?php
namespace App\Events;
use App\Models\TransferFile;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class FileTransferredToCloud implements ShouldBroadcastNow
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(private TransferFile $file)
{
}
public function broadcastOn()
{
return new PrivateChannel('channel-name');
}
}
Teraz wyślij zdarzenie z naszego zadania:
public function handle()
{
// ...
FileTransferredToCloud::dispatch($this->file);
}
Utwórz partię zadań Czas utworzyć naszą partię
zadań wewnątrz naszego TransferLocalFileToCloud
CreateTransferBatch
detektora. Możemy użyć mapInto
metody zbierania, aby szybko wygenerować zadanie dla każdego pliku.
<?php
namespace App\Listeners;
use App\Events\LocalTransferCreated;
class CreateTransferBatch
{
public function handle(LocalTransferCreated $event)
{
$jobs = $event->getTransfer()->files->mapInto(TransferLocalFileToCloud::class);
}
}
Teraz możemy użyć fasadyBatch
, aby wysłać wszystkie te zadania za jednym razem.
public function handle(LocalTransferCreated $event)
{
$jobs = $event->getTransfer()->files->mapInto(TransferLocalFileToCloud::class);
$batch = Bus::batch($jobs)->dispatch();
}
Zapraszam do wypróbowania! Partia powinna przenieść pliki do magazynu w chmurze. Możesz to sprawdzić, patrząc na swoją job_batches
tabelę. Tutaj powinieneś zobaczyć nowy wpis i liczbę przetworzonych zadań.
Zapisywanie i konfigurowanie naszej partii Cały sens naszej partii
polega na tym, że chcemy pokazać postęp naszym użytkownikom. Zacznijmy więc od dołączenia identyfikatora partii do naszego modelu transferu.
public function handle(LocalTransferCreated $event)
{
$jobs = $event->getTransfer()->files->mapInto(TransferLocalFileToCloud::class);
$batch = Bus::batch($jobs)->dispatch();
$event->getTransfer()->update([
'batch_id' => $batch->id
]);
}
Następnie chcemy uruchomić zdarzenie, gdy wszystkie nasze zadania zostaną przetworzone. Fasada Batch
zapewnia kilka metod, których można użyć, aby to osiągnąć.
$batch = Bus::batch($jobs)
->then(function (Batch $batch) {
// All jobs completed successfully
})->catch(function (Batch $batch, Throwable $e) {
// First batch job failure detected
})->finally(function (Batch $batch) {
// The batch has finished executing
})->dispatch();
W naszym przypadku użyjemy tylko tej finally
metody do odpalenia TransferCompleted
zdarzenia. Najpierw stwórzmy to wydarzenie.
php artisan make:event TransferCompleted
Event created successfully.
To zdarzenie zaakceptuje Transfer
model jako parametr i zaimplementuje interfejs, ShouldBroadcast
tak jak zrobiliśmy to wcześniej.
<?php
namespace App\Events;
use App\Models\Transfer;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class TransferCompleted implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(private Transfer $transfer)
{
}
public function broadcastOn()
{
return new PrivateChannel('channel-name');
}
}
Następnie dodamy go do zamknięciafinally
.
class CreateTransferBatch
{
public function handle(LocalTransferCreated $event)
{
$transfer = $event->getTransfer();
$jobs = $transfer->files->mapInto(TransferLocalFileToCloud::class);
$batch = Bus::batch($jobs)
->finally(function () use ($transfer) {
TransferCompleted::dispatch($transfer);
})->dispatch();
$event->getTransfer()->update([
'batch_id' => $batch->id
]);
}
}
Przypisałem również zmienną do modelu transferu, aby przekazać model zamiast całego zdarzenia. Laravel serializuje zamknięcie i zapisze je w options
kolumnie job_batches
tabeli.
Jeśli chcesz, możesz dać całemu przepływowi kolejną szansę i użyć php artisan queue work
do wyświetlenia przetworzonych zadań.
Całkiem fajne, prawda?
Lista naszych transferów Możemy teraz uczynić naszą tabelę statyczną dynamiczną, wymieniając wszystkie transfery
. Wróć więc do swojej klasy komponentów Livewire i przekaż kolekcję Eloquent zawierającą nasze modele Transfer do naszego widoku.
public function render()
{
return view('livewire.manage-transfers', [
'transfers' => auth()->user()->transfers
]);
}
Otwórzmy nasze manage-transfers.blade.php
i dodajmy do @forelse
naszej tabeli.
<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 have no transfers. Create a batch on the right 👉🏻
</td>
</tr>
@endforelse
</tbody>
Musimy pokazać nasz status zadania i obliczyć łączny rozmiar wszystkich plików przesyłania. Teraz Laravel jest dostarczany z metodą wyszukiwania partii według ich identyfikatora: Bus::findBatch($batchId);
. Ta metoda użyje "DatabaseBatchRepository" za kulisami, aby pobrać dane z bazy danych i przekształcić dane w \Illuminate\Bus\Batch
obiekt.
Metoda findBatch
działa idealnie, ale chciałbym użyć Eloquent zamiast tego do chętnego ładowania wszystkich partii, wykonywania zakresów zapytań itp. Stwórzmy więc gotowy model tylko dla naszych partii.
Laravel jest odpowiedzialny za utrzymanie integralności danych naszej job_batches
tabeli i chcę, aby tak pozostało, dlatego lubię, aby model był tylko do odczytu.
Packagist na ratunek. Jeśli wykonasz szybkie wyszukiwanie tylko do odczytu, zobaczysz następujący pakiet. Ten pakiet wprowadzi cechę, dzięki której modele będą tylko do odczytu. Więc zainstalujmy go:Next, chcemy również utworzyć nasz model:
composer require michaelachrisco/readonly
php artisan make:model JobBatch
Zacznijmy od dodania cechy tylko do odczytu do naszego modelu, więc nasz model zgłosi wyjątek, jeśli wywołasz metodę taką jak create, save, delete itp.
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use MichaelAChrisco\ReadOnly\ReadOnlyTrait;
class JobBatch extends Model
{
use ReadOnlyTrait;
}
Jeśli spojrzymy na job_batches
tabelę, zobaczysz, że każda partia ma UUID zamiast przyrostowej liczby całkowitej, a także nie ma znacznika updated_at
czasu.
class JobBatch extends Model
{
use ReadOnlyTrait;
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'job_batches';
/**
* The "type" of the primary key ID.
*
* @var string
*/
protected $keyType = 'string';
/**
* Indicates if the IDs are auto-incrementing.
*
* @var bool
*/
public $incrementing = false;
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = false;
}
Aby to nieco ułatwić, możemy rzutować właściwości modelu, dzięki czemu otrzymujemy obiekt, gdy robimy coś takiego $jobBatch->finished_at
lub kolekcję, gdy robimy
class JobBatch extends Model
{
// ....
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'options' => 'collection',
'failed_jobs' => 'integer',
'created_at' => 'datetime',
'cancelled_at' => 'datetime',
'finished_at' => 'datetime',
];
}
$jobBatch->options
:Następnie chcemy odtworzyć kilka metod, które zwykle otrzymujemy Carbon
z Illuminate\Bus\Batch
obiektu:
- processedJobs: Pobierz łączną liczbę zadań, które zostały przetworzone przez partię do tej pory.
- postęp: Pobierz procent zadań, które zostały przetworzone (z zakresu od 0 do 100).
- gotowy: Sprawdź, czy partia zakończyła wykonywanie.
- hasFailues: Określ, czy partia ma błędy zadań.
- Anulowane: Sprawdź, czy coś anulowało partię.
Możemy powielić większość z tych metod w naszym modelu i odpowiednio dostosować kod, aby uzyskać dane z naszego modelu:
class JobBatch extends Model
{
// ...
/**
* Get 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 percentage of jobs that have been processed (between 0-100).
*
* @return int
*/
public function progress(): int
{
return $this->total_jobs > 0 ? round(($this->processedJobs() / $this->total_jobs) * 100) : 0;
}
/**
* Determine if the batch has pending jobs
*
* @return bool
*/
public function hasPendingJobs(): bool
{
return $this->pending_jobs > 0;
}
/**
* Determine if the batch has finished executing.
*
* @return bool
*/
public function finished(): bool
{
return !is_null($this->finished_at);
}
/**
* Determine if the batch has job failures.
*
* @return bool
*/
public function hasFailures(): bool
{
return $this->failed_jobs > 0;
}
/**
* Determine if all jobs failed.
*
* @return bool
*/
public function failed(): bool
{
return $this->failed_jobs === $this->total_jobs;
}
/**
* Determine if the batch has been canceled.
*
* @return bool
*/
public function cancelled(): bool
{
return !is_null($this->cancelled_at);
}
}
Jesteśmy prawie gotowi do chętnego ładowania naszych partii zadań. Jedyne, co pozostaje, to zdefiniowanie związku. Otwórzmy więc nasz Transfer
model i dodajmy relację:
class Transfer extends Model
{
// ...
public function jobBatch(): BelongsTo
{
return $this->belongsTo(JobBatch::class, 'batch_id');
}
}
Teraz możemy zaktualizować nasz kod wewnątrz naszego ManageTransfers
komponentu Livewire, aby chętnie ładować nasze partie.
class ManageTransfers extends Component
{
// ...
public function render()
{
return view('livewire.manage-transfers', [
'transfers' => auth()->user()->transfers()->with('jobBatch')->get(),
]);
}
}
Na koniec zaktualizujmy nasz kod HTML, aby odzwierciedlał różne stany zadań:
@forelse($transfers as $transfer)
<tr class="bg-white">
@if(is_null($transfer->jobBatch))
<td>
%
</td>
<td>
<div class="flex h-2 overflow-hidden rounded bg-gray-50">
<div style="transform: scale(0, 1)"
class="bg-indigo-500 transition-transform origin-left duration-200 ease-in-out w-full shadow-none flex flex-col"></div>
</div>
</td>
@elseif($transfer->jobBatch->hasPendingJobs())
<td>
%
</td>
<td>
<div class="flex h-2 overflow-hidden rounded bg-gray-50">
<div style="transform: scale({{ $transfer->jobBatch->progress() / 100 }}, 1)"
class="bg-indigo-500 transition-transform origin-left duration-200 ease-in-out w-full shadow-none flex flex-col"></div>
</div>
</td>
@elseif($transfer->jobBatch->finished() and $transfer->jobBatch->failed())
<td>
X
</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
Świetnie! Sprawy zaczynają nabierać kształtów. Zakończmy nasz widok tabeli, pokazując łączny rozmiar wszystkich plików. Możemy to zrobić, chętnie ładując pliki i sumując całkowity rozmiar:
return view('livewire.manage-transfers', [
'transfers' => auth()->user()->transfers()->with('jobBatch', 'files')->get(),
]);
// $transfer->files->sum('size');
Ale to załadowałoby wszystkie modele plików; chociaż działa, możemy to nieco zoptymalizować, pozwalając Eloquent obliczyć sumę za pomocą SQL.
return view('livewire.manage-transfers', [
'transfers' => auth()->user()->transfers()->with('jobBatch')->withSum('files', 'size')->get(),
]);
// $transfer->files_sum_size
Całkiem przydatny mały pomocnik, aby poprawić wydajność aplikacji. Teraz nasz widok jest gotowy i powinieneś mieć coś takiego:
Jeśli chcesz zobaczyć różne stany, możesz to sfałszować, dostosowując wpisy job_batches
. Na przykład ustaw kolumnę failed_jobs
lub pending_jobs
na 1.
Postęp w czasie rzeczywistym za pomocą Laravel Echo
Moment, na który wszyscy czekali, pokazujący postęp transferu i wyniki naszym użytkownikom w czasie rzeczywistym. Zamierzamy to zrobić, używając Laravel Echo i Livewire. Zanim przejdziemy dalej, zainstalujmy bibliotekę Laravel Echo Javascript.
npm install --save-dev laravel-echo pusher-js
Następnie możesz odkomentować następujący kod w pliku bootstrap.js
:
import Echo from 'laravel-echo';
window.Pusher = require('pusher-js');
window.Echo = new Echo({
broadcaster: 'pusher',
key: process.env.MIX_PUSHER_APP_KEY,
cluster: process.env.MIX_PUSHER_APP_CLUSTER,
forceTLS: true
});
W tym przykładzie użyję Pushera jako naszego nadawcy. Możesz założyć bezpłatne konto i uzyskać dane uwierzytelniające konta. Musisz dodać je do pliku .env
.
BROADCAST_DRIVER=pusher
PUSHER_APP_ID=10000
PUSHER_APP_KEY=h28f720sd6v5a02
PUSHER_APP_SECRET=jh8s02017cx0add
PUSHER_APP_CLUSTER=eu
Uruchom npm run dev
po zainstalowaniu pakietu npm i zdefiniowaniu zmiennych środowiskowych. Następnie musimy również wymagać konfiguracji Pusher SDK:
composer require pusher/pusher-php-server "~4.0"
Channel
Mamy trzy wydarzenia, które są transmitowane:
- LocalTransferCreated
- LocalTransferCreated
- TransferCompleted
Te wydarzenia są transmitowane na kanał prywatny. Zwróćmy nazwę kanału, która zawiera identyfikator użytkownika (właściciela) danego modelu transferu.
W większości przypadków należy użyć kanałów prywatnych, ale jeśli nie jest potrzebne żadne uwierzytelnianie, można zamiast tego zwrócić Illuminate\Broadcasting\Channel
obiekt.
class 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}");
}
}
Autoryzacja kanału
Laravel Echo poprosi o konkretny punkt końcowy, aby sprawdzić, czy masz dostęp do kanału prywatnego. Ten punkt końcowy nie istnieje domyślnie, ponieważ BroadcastServiceProvider
jest domyślnie wyłączony. Otwórz swoje app.php
i włącz App\Providers\BroadcastServiceProvider::class
Następnie możemy zdefiniować autoryzację w routes/channels.php
pliku.
Broadcast::channel('notifications.{channelUser}', function ($authUser, \App\Models\User $channelUser) {
return $authUser->id === $channelUser->id;
});
Zamknięcie ma dwie wartości; Pierwsza wartość to bieżący zalogowany użytkownik, druga wartość to broadcast user ID, która jest automatycznie rozpoznawana po wpisaniu podpowiedzi zamknięcia, tak jak w przypadku korzystania z tras.
Integracja Laravel Livewire z Laravel Echo
Możemy teraz skonfigurować nasz komponent Livewire i poinstruować go, aby odświeżył się, gdy otrzymamy nowe zdarzenie z Pushera. Livewire ma natywne wsparcie dla Laravel Echo, więc wymaga to tylko kilku linii kodu. ManageTransfers
Otwórz komponent i utwórz nową getListeners()
metodę.
public function getListeners()
{
return [];
}
Ta metoda powinna zwracać tablicę klucz-wartość, gdzie klucz jest zdarzeniem do nasłuchiwania, a wartość jest metodą wyzwalaną po wystąpieniu zdarzenia.
W naszym przypadku chcemy odświeżyć cały komponent. Fajną rzeczą w Livewire jest to, że wie, które elementy się zmieniły i aktualizuje DOM tylko dla tych elementów. Oznacza to, że animacje CSS będą działać, a nasz pasek postępu zostanie zaktualizowany z fajną animacją.
public function getListeners()
{
$userId = auth()->id();
return [
"echo-private:notifications.{$userId},FileTransferredToCloud" => '$refresh',
];
}
Jeśli korzystasz z Laravel Echo, musisz poprzedzić powiadomienie, aby Livewire wiedział, echo-private
że musi współpracować z Laravel Echo. Następnie podajemy mu naszą prywatną nazwę notifications.{$userId}
kanału, a następnie wydarzenie, którego chcemy słuchać, w tym przypadku . Nie mamy żadnej konkretnej metody, FileTransferredToCloud
którą chcemy uruchomić; chcemy tylko odświeżyć komponent, przekazując go $refresh
.
To wszystko! Idź i spróbuj i zobacz, jak dzieje się magia.
Bonus
za konfetti Czym jest postęp w czasie rzeczywistym bez konfetti? No właśnie, absolutnie nic! Dajmy mu podmuch konfetti, gdy skończy się z transferem.
Załóżmy nasze działo konfetti:Otwórz resources/app.js
i użyj tej Livewire.on()
funkcji, aby nasłuchiwać zdarzenia konfetti:
npm install --save canvas-confetti
import confetti from "canvas-confetti";
Livewire.on('confetti', () => {
confetti({
particleCount: 80,
spread: 200,
origin: {y: 0.6}
});
})
Następnie wróć do komponentu Livewire i zarejestruj nowego słuchacza.
public function getListeners()
{
$userId = auth()->id();
return [
"echo-private:notifications.{$userId},FileTransferredToCloud" => '$refresh',
"echo-private:notifications.{$userId},TransferCompleted" => 'fireConfettiCannon',
];
}
Następnie utwórz nową metodę o nazwie fireConfettiCannon
i użyj emit
pomocnika, aby wyemitować confetti
zdarzenie:
public function fireConfettiCannon()
{
$this->emit('confetti');
}
To jest okład! 🎉 Chciałbym usłyszeć, w jaki sposób użyłeś tej techniki, aby pokazać swoim klientom postęp w czasie rzeczywistym (i może trochę konfetti). Napisz do mnie tweeta @Philo01 i porozmawiaj! 🙌🏼
Wire Elements Pro - Profesjonalnie wykonane komponenty Livewire Piękne komponenty 🚀
wykonane z Livewire. Premiera wkrótce, pamiętaj, aby subskrybować.
Myślę o wydaniu kursu wideo premium w najbliższej przyszłości (jeśli wystarczająco dużo osób będzie zainteresowanych), w którym pokażę, jak zbudować rzeczywistą aplikację od początku do końca przy użyciu Laravel, Livewire i Tailwind CSS. Będę omawiał wszystko, od rejestracji domeny, konfiguracji serwera, pisania testów, co tylko chcesz. Zapisz się, jeśli jesteś zainteresowany i chcesz otrzymywać powiadomienia, a możesz również uzyskać dostęp za darmo, ponieważ będę rozdawał, gdy kurs się rozpocznie.