• Час читання ~24 хв
  • 05.07.2022

Тому я працюю над крутим проектом, і працював над функцією, яка обробляє вхідні файли та завантажує їх у хмарне сховище. Я думав, що партії Laravel ідеально підійдуть для цього, що це і було! Я вирішив поєднати потужність партій Laravel з подіями, Laravel Echo та Livewire, щоб показати своїм користувачам прогрес у режимі реального часу та не забути, деякі конфетті для святкування 🎉.

Тож у цій статті я покроково покажу вам, як це зробити, щоб ви могли вразити своїх клієнтів (за допомогою конфетті).

Що таке Laravel Livewire?

Livewire - це повноцінний фреймворк для Laravel від Калеба Порціо, який робить створення динамічних  інтерфейсів надзвичайно простим без написання жодного рядка Javascript, що досить приголомшливо, тому що ви можете створити SPA, як відчуття, знову ж таки, без необхідності писати будь-який Javascript. Як згадувалося на веб-сайті Livewire, найкращий спосіб зрозуміти це - подивитися на код, тож давайте почнемо!

Встановлення Laravel, Livewire та Tailwind CSS

Я збираюся використовувати чисту установку Laravel 8, але ви, звичайно, можете слідувати разом з одним із ваших існуючих проектів.

Давайте завантажимо та встановимо Laravel, Livewire та Tailwind CSS для тих, хто хоче почати з нуля.

Я зосереджуся на партіях Laravel та прогресі в режимі реального часу з Livewire. Якщо ви новачок у Laravel, Livewire або Tailwind CSS, усі ці продукти мають велику та добре написану документацію, яка допоможе вам розпочати роботу.

# 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

Далі нам потрібно зробити ще кілька налаштувань, щоб CSS Tailwind працював. Давайте оновимо наше webpack.mix.js перше.

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

Відкрийте ./resources/css/app.css та додайте директиви CSS Tailwind:

@tailwind base;
@tailwind components;
@tailwind utilities;

Далі ми хочемо переконатися, що ми включаємо наш CSS у файл леза, а також включаємо директиви Livewire. Отже, відкритий./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>

, що повинен зробити свою справу. Запустітьnpm run dev, щоб переконатися, що у нас є скомпільований файл CSS.

Інтерфейс користувача

I'm a big fan of Інтерфейс користувача 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 Інтерфейс користувача components while we progress. The HTML markup in this article, however, will not include any Інтерфейс користувача components. I've made the following sample application in a couple of minutes:

високого рівня Ми хочемо досягти наступного:Створіть компонент

Livewire Давайте почнемо зі створення нашого компонента Livewire:

  1. As a user, we want to upload one or more 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 track the progress.
  7. Create a read-only model for the job_batches table so we 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

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

Давайте перемістимо наш HTML у файл.manage-transfers.blade.php

<div>
    <table>
        <thead>
        <tr>
            <th>&nbsp;</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>

Крім того, додайте директиву @livewire, щоб welcome.blade.php компонент відображався.

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

Завантаження файлів за допомогою Livewire Livewire

зробило завантаження файлів легким! Це досить божевільно, наскільки просто змусити це працювати.

Livewire file upload flow

Livewire зробить запит POST до кінцевої /livewire/upload-file точки та поверне тимчасові імена файлів за лаштунками. Клієнт браузера Livewire зробить запит POST до компонента Livewire, який поверне HTML і оновить DOM, щоб показати попередній перегляд зображення.

Ми почнемо з визначення моделі для нашого елемента завантаження файлів. У цьому випадку давайте підемоpendingFiles, оскільки ця модель міститиме всі файли, вибрані користувачем, але ще не оброблені.

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

Далі перейдіть до пов'язаного класу Livewire для цього компонента (app/Http/Livewire/ManageTransfers.php) і з'єднайте його (каламбур

<?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');
    }
}

).Щоб працювати із завантаженням файлів, потрібно включити рису "Livewire\WithFileUploads". Ця риса буде складатися з необхідних методів обробки файлів, завантажених через цей компонент.

Порада: остерігайтеся введення загальнодоступних властивостей у компонентах Livewire. Livewire у деяких випадках робить виняток, якщо властивість не є рядком, масивом, дратівливим значенням, оскільки вона не знає, що робити. Наприклад:

// 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

Назад до магії Livewire. Ми додали пару рядків коду, і наше завантаження вже працює. Щоб переконатися в цьому, давайте швидко додамо кілька тимчасових зображень попереднього перегляду:

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

Тепер спробуйте, виберіть пару зображень і спостерігайте за магією. Божевільний, правда? Отже, як це працює? Що ж, Livewire обробляє всі вибрані файли і поміщає ці файли в приватний тимчасовий каталог.

Livewire відстежує всі наші завантаження. Властивість $pendingFiles поверне масив Livewire\TemporaryUploadedFile об'єктів. Метод temporaryUrl поверне підписаний маршрут, щоб зробити завантажений файл доступним. З міркувань безпеки ця тимчасова URL-адреса працюватиме лише для затверджених розширень файлів. Тож якщо ви завантажите zip-файл, це не спрацює.

Якщо потрібно змінити цю поведінку за промовчанням, можна настроїти livewire.php файл конфігурації. Вам потрібно запустити таку команду, щоб опублікувати файл конфігурації:

php artisan livewire:publish --config

Якщо ви відкриєте цей файл, ви можете прокрутити вниз до розділу «Конфігурація кінцевої точки тимчасового завантаження файлів Livewire» і налаштувати конфігурацію на свій смак:Будь ласка, врахуйте,

'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, 
],

що, хоча Livewire поставляється з деякими розумними налаштуваннями, такими як обмеження завантаження 12 МБ та дросельна заслінка, хтось може заповнити ваш дисковий простір, якщо ця людина цього хоче. Тож якась додаткова безпека не зашкодить. Можна, наприклад, обмежити кількість тимчасових файлів для одного користувача.

Перевірка файлів за допомогою Livewire Типово, Livewire

реалізує requiredправила , fileта max:12288 правила для будь-яких тимчасових завантажень файлів. Щоб додати нашу перевірку, ми можемо надати наші правила до методуvalidate.

Давайте створимо загальнодоступний метод під назвою initiateTransfer() та деякі наші власні правила перевірки (файл повинен бути зображенням і максимальним розміром 5 МБ), зв'яжіть його з кнопкою, щоб користувач міг надіслати вибрані файли для передачі.

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

Наприклад, якщо ви завантажите занадто велике зображення, помилки ще не з'являться. Отже, давайте додамо це до нашого перегляду:Якщо, наприклад, я завантажу

<div>
    @error('pendingFiles.*')
    {{ $message }}
    @enderror
</div>

файл розміром 70 МБ, він негайно не вдасться, оскільки він не проходить початкове правило перевірки (макс. 12 МБ), визначене в конфігурації livewire.php.

Validation error

Якщо ми завантажимо зображення розміром 6.7 МБ, ви побачите тимчасовий попередній перегляд, але без помилки. Помилка з'явиться лише після виконання перевірки, тому, якщо ви натиснете кнопку «Зробити магію», ви побачите помилку про те, що файл не може перевищувати 5 МБ.

Красномовні моделі та міграції

баз даних Щоб відстежувати статус нашої партії, нам потрібно прикріпити нашу партію до чогось. Наприклад, ви можете захотіти прикріпити ці файли до документа, проекту; Ви називаєте це. Ви також можете показати весь вміст таблиці "job_batches", але я не думаю, що це ймовірний варіант використання.

Таблиця партій завдань
Перш ніж ми зможемо відправити пакети, нам потрібно запустити ремісничу команду для створення таблиці бази даних, яку Laravel буде використовувати для збереження даних, пов'язаних з нашою партією. Отже, виконайте таку команду:Таблиця job_batches містить наступну інформацію:

php artisan queue:batches-table
Migration created successfully!

  • Назва вашої роботи.
  • Загальна кількість робочих місць дана партія має.
  • Загальна кількість незавершених завдань, які очікують обробки працівником черги.
  • Загальна кількість робочих місць, які не були оброблені працівником черги.
  • ID невдалих завдань, це посилання на failed_jobs таблицю.
  • Будь-які параметри, визначені як then, catch або finally.
  • Мітка часу, коли партія була скасована.
  • Позначка часу під час створення партії.
  • Мітка часу, коли працівник черги закінчив обробку всіх завдань для заданої партії.

Ми будемо використовувати деякі з цих даних на майбутньому кроці, щоб, наприклад, показати прогрес певної партії.

Модель користувача
Я хочу продемонструвати, як вести трансляцію приватно, щоб лише користувач, який увійшов у систему, бачив завдання передачі та прогрес у режимі реального часу. Laravel вже поставляється з моделлю користувача, тому єдине, що вам потрібно зробити, це відкрити DatabaseSeeder та розкоментувати фабрику користувачів.

Риштування інтерфейсу користувача для аутентифікації тощо. виходить за рамки цієї статті. Тим не менш, я хотів би поділитися альтернативою під час локальної роботи та швидко увійти в будь-який обліковий запис, не надаючи жодних облікових даних.

Відкрийте свій routes/web.php і додайте наступне:

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

Це буде працювати лише локально abort_unless завдяки функції, яка видасть помилку 403, якщо середовище не дорівнює local. Я використовую помічник auth() для входу першого користувача в базі даних.

Передача та файлова модель
Ми будемо використовувати красномовну модель для відстеження всіх файлів. Як згадувалося раніше, ви можете розглядати це як проект (або надіслати електронний лист чи твіт з одним або кількома вкладеннями), і ви хочете завантажити та пов'язати конкретні файли з цим проектом.

Отже, давайте створимо модель і міграцію як для передачі, так і для файлу.

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

Для нашої transfers таблиці нам потрібно додати лише одне додаткове поле, яке є batch_id:

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();
});

Поки transfer_files ми тут, давайте встановимо зв'язок між ними та визначимо поля, які можна заповнити.

<?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);
    }
}

Збереження даних у базі даних

Запуститиphp artisan migrate, щоб виконати всі перенесення, якщо ви ще цього не зробили. Тепер ми можемо зберігати файли, що очікують на розгляд, які пройшли перевірку.

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(),
                ]);
            })
    );
}

Коли ви виберете чотири зображення та натиснете чарівну кнопку, ви побачите один новий запис бази даних у transfers таблиці та чотири записи в transfer_files таблиці.

Тепер скинемо властивість $pendingFiles на порожній масив для скидання форми і, нарешті, події (цей клас ми створимо на наступному кроці).

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

Dispatch LocalTransferCreated подія

Тепер у нас зберігаються всі наші дані, ми можемо відправити подію, яку ми можемо прослухати на наступних кроках. Давайте створимо подію, на яку ми посилалися в методі initiateTransfer вище:

php artisan make:event LocalTransferCreated
Event created successfully.

Ми в кінцевому підсумку хочемо оновлювати нашу таблицю кожного разуLocalTransferCreated, коли подія відправляється. Для цього ми будемо використовувати Laravel Echo. Ми поговоримо про це на майбутньому кроці, але оскільки ми тут, давайте переконаємось, що наша подія реалізує ShouldBroadcast інтерфейс, щоб Laravel знав, що ми хочемо транслювати подію.

<?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');
    }
}

Створіть і призначимо слухача

Давайте створимо нового слухача, який буде слухати LocalTransferCreated подію і відправляти нашу партію.

php artisan make:listener CreateTransferBatch
Listener created successfully.

Далі додайте відображення до нашого, щоб доручити Laravel зателефонувати нам EventServiceProvider CreateTransferBatch, коли LocalTransferCreated подія буде відправлена:Створіть нашу роботу з передачі Перш ніж ми зможемо створити нашу партію, нам потрібно мати роботу

<?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
        ]
    ];
}

,

щоб перейти до нашої партії. Отже, давайте створимо його:

php artisan make:job TransferLocalFileToCloud
Job created successfully.

тепер це відносно проста робота. Беремо локальний файл, завантажуємо його в наше хмарне сховище, оновлюємо запис бази даних, змінивши диск на 's3', і шлях до згенерованого шляху, що містить унікальне ім'я файлу, повернуте методом put'. Нарешті, ми очищаємо, видаляючи файл з нашого локального сховища.

Щоб зробити вашу роботу пакетною, вам потрібно додати ознаку «Пакетний»; В іншому випадку ви отримаєте наступний виняток:Залежно від вашого постачальника хмарного сховища, вам може знадобитися адаптер, наприкладcomposer require "league/flysystem-aws-s3-v3 ~1.0"

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

:

<?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
    }
}

Оскільки ми хочемо оновлювати інтерфейс у режимі реального часу, нам потрібно надсилати подію кожного разу, коли передається файл. Давайте створимо нову подію:

php artisan make:event FileTransferredToCloud 
Event created successfully.

Давайте візьмемо модель Transfer як наш параметр події та переконаємось, що подія реалізує інтерфейс ShouldBroadcastNow. Ми хочемо транслювати цю подію відразу, інакше вона опиниться в нижній частині відставання черги, через що ця подія спрацює, коли вся партія вже готова. Враховуючи, що ми хочемо показати прогрес у режимі реального часу, нам потрібно, щоб ця трансляція відбувалася в режимі реального часу.

<?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');
    }
}

Тепер надішліть подію з нашої TransferLocalFileToCloud роботи:

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

Створіть пакет

завдань Час створити нашу партію завдань всередині нашого CreateTransferBatch слухача. Ми можемо використовувати mapInto метод збору для швидкого створення завдання для кожного файлу.

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

Тепер ми можемо використовувати фасад, Batch щоб відправити всі ці роботи за один раз.

public function handle(LocalTransferCreated $event)
{
    $jobs = $event->getTransfer()->files->mapInto(TransferLocalFileToCloud::class);
    $batch = Bus::batch($jobs)->dispatch();
}

Не соромтеся спробувати! Пакет повинен перенести ваші файли в хмарне сховище. Ви можете переконатися в цьому, поглянувши на свою job_batches таблицю. Тут ви повинні побачити новий запис і кількість оброблених робочих місць.

Збереження та налаштування нашої партії Вся суть нашої партії

полягає в тому, що ми хочемо показати прогрес нашим користувачам. Отже, давайте почнемо з прикріплення ідентифікатора партії до нашої моделі передачі.

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

Далі ми хочемо звільнити подію, коли всі наші завдання будуть оброблені. Фасад Batch передбачає кілька методів, які ви можете використовувати для досягнення цього.

$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();

У нашому випадку ми будемо використовувати finally метод лише для запуску TransferCompleted події. Тож давайте спочатку створимо цю подію.

php artisan make:event TransferCompleted
Event created successfully.

Ця подія прийме Transfer модель як параметр і реалізує інтерфейс, ShouldBroadcast як ми це робили раніше.

<?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');
    }
}

Далі ми додамо його до finally закриття.

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
        ]);
    }
}

Я також призначив змінну моделі передачі, щоб передати модель замість усієї події. Laravel серіалізує закриття та збереже його в options стовпці job_batches таблиці.

Якщо ви хочете, ви можете дати всьому потоку ще один хід і використатиphp artisan queue work, щоб побачити оброблені роботи.

Досить круто, правда?

Перелік наших переказів

Тепер ми можемо зробити нашу статичну таблицю динамічною, перерахувавши всі перекази. Отже, поверніться до свого класу компонентів Livewire і передайте колекцію Eloquent, що містить наші моделі передачі, нашому погляду.

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

Відкриємо наш manage-transfers.blade.php і додамо в @forelse нашу таблицю.

<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>

Нам потрібно показати статус нашої роботи і розрахувати сумарний розмір всіх файлів передачі. Тепер Laravel поставляється з методом пошуку партій за їх ідентифікатором: Bus::findBatch($batchId);. Цей метод буде використовувати "DatabaseBatchRepository" за лаштунками, щоб отримати дані з бази даних і перетворити дані в \Illuminate\Bus\Batch об'єкт.

Метод findBatch працює ідеально, але я хотів би використовувати Eloquent, щоб прагнути завантажити всі пакети, виконати області запитів тощо. Отже, давайте створимо для наших партій готову тільки модель.

Laravel відповідає за збереження цілісності даних нашої job_batches таблиці, і я хочу зберегти її такою, тому мені подобається, щоб модель була доступна лише для читання.

Пакагіст на допомогу. Якщо ви виконаєте швидкий пошук лише для читання, ви побачите наступний пакет. Цей пакет представить рису, яка робить моделі доступними лише для читання. Отже, давайте встановимо його:

composer require michaelachrisco/readonly

Далі ми також хочемо створити нашу модель:

php artisan make:model JobBatch

Давайте почнемо з додавання ознаки лише для читання до нашої моделі, тому наша модель кине виняток, якщо ви викличете такий метод, як створення, збереження, видалення тощо.

<?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;
}

Якщо ми швидко подивимося на job_batches таблицю, ви можете побачити, що кожна партія має UUID замість наростаючого цілого числа, а також не має мітки updated_at часу. Отже, давайте оновимо нашу модель:Щоб трохи полегшити роботу, ми можемо відкинути властивості моделі, щоб ми отримали Carbon об'єкт, коли ми робимо щось подібне$jobBatch->finished_at, або колекцію, коли ми робимо $jobBatch->options:Далі, ми хочемо відтворити деякі методи, які ми зазвичай отримуємо відIlluminate\Bus\Batch   об'єкта:

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;
}

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',
    ];
}

  • processedJobs: Отримайте загальну кількість робочих місць, які були оброблені партією до цього часу.
  • Прогрес: Отримайте відсоток робочих місць, які були оброблені (від 0 до 100).
  • Закінчили: Визначте, чи закінчила виконання партія.
  • hasFailues: Визначте, чи є у партії збої в роботі.
  • Скасовано: Визначте, чи що-небудь скасувало партію.

Ми можемо дублювати більшість цих методів у нашій моделі та відповідно коригувати код, щоб отримати дані з нашої моделі:

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);
    }
}

ми майже готові завантажити наші партії завдань. Залишається тільки визначити відносини. Отже, давайте відкриємо нашу Transfer модель і додамо взаємозв'язок:

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

Тепер ми можемо оновити наш код всередині нашого ManageTransfers компонента Livewire, щоб прагнути завантажувати наші партії.

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

Нарешті, давайте оновимо наш HTML, щоб відобразити різні стани роботи:

@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

Чудово! Речі починають набувати форми. Давайте завершимо наш перегляд таблиці, показавши комбінований розмір усіх файлів. Ми могли б зробити це, прагнучи завантажити файли та підсумувавши загальний розмір:

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

Але це завантажить усі моделі файлів; хоча це працює, ми можемо трохи оптимізувати це, дозволивши Eloquent обчислити суму за допомогою SQL.

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

Досить зручний маленький помічник для підвищення продуктивності вашого додатка.    Тепер наш погляд готовий, і у вас повинно бути щось на зразок цього:

Якщо ви хочете побачити різні стани, ви можете підробити це, відкоригувавши job_batches записи. Наприклад, встановіть для стовпця failed_jobs or pending_jobs значення 1.

Прогрес у режимі реального часу за допомогою Laravel Echo

Момент, якого всі чекали, показуючи прогрес передачі та результати нашим користувачам у режимі реального часу. Ми збираємося зробити це, використовуючи Laravel Echo та Livewire. Отже, перш ніж рухатися далі, давайте встановимо бібліотеку Laravel Echo Javascript.

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

Далі ви можете розкоментувати наступний код у своєму 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
});

У цьому прикладі я буду використовувати Pusher як нашого мовника. Ви можете зареєструвати безкоштовний обліковий запис і отримати облікові дані свого облікового запису. Вам потрібно додати їх у свій .env файл.

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

Запустіть npm run dev після встановлення пакета npm та визначення змінних середовища. Далі нам також потрібно вимагати конфігурацію Pusher SDK:

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

Channel
У нас є три події, які транслюються:

  1. LocalTransferCreated
  2. LocalTransferCreated
  3. TransferCompleted

Ці події транслюються на приватний канал. Повернемо назву каналу, яка містить ідентифікатор користувача (власника) певної моделі передачі.

У більшості випадків ви використовуєте приватні канали, але якщо вам не потрібна автентифікація, ви можете повернути об'єкт.Illuminate\Broadcasting\Channel

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}");
    }
}

Авторизація каналу
Laravel Echo запитає певну кінцеву точку, щоб перевірити, чи маєте ви доступ до приватного каналу. Ця кінцева точка не існує за промовчанням, оскільки її BroadcastServiceProvider вимкнуто за промовчанням. Відкриваємо свій app.php і включаємо

App\Providers\BroadcastServiceProvider::class Далі ми можемо визначити авторизацію в файлі.

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

routes/channels.phpЗамикання має два значення; Перше значення - це поточний користувач, який увійшов у систему, друге значення - ідентифікатор користувача широкомовної трансляції, який автоматично розпізнається при введенні підказки про закриття так само, як при використанні маршрутів.

Інтеграція Laravel Livewire з Laravel Echo
Тепер ми можемо налаштувати наш компонент Livewire і доручити йому оновитися, коли ми отримаємо нову подію від Pusher. Livewire має вбудовану підтримку Laravel Echo, тому для цього потрібно лише кілька рядків коду. ManageTransfers Відкрийте компонент і створіть новий getListeners() метод.

public function getListeners()
{
    return [];
}

Цей метод повинен повертати масив ключ-значення, де ключ є подією для прослуховування, а значення є методом запуску при виникненні події.

У нашому випадку ми хочемо оновити весь компонент. Найцікавіше в Livewire полягає в тому, що він знає, які елементи змінилися, і оновлює DOM лише для цих елементів. Це означає, що CSS-анімація буде працювати, і ми побачимо оновлення нашої панелі прогресу з крутою анімацією.

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

Якщо ви використовуєте Laravel Echo, вам потрібно додати префікс сповіщенняecho-private, щоб Livewire знав, що йому потрібно працювати разом із Laravel Echo. Далі ми надаємо йому назву notifications.{$userId} нашого приватного каналу, а потім подію, яку ми хочемо прослухати, у цьому випадку . У нас немає жодного конкретного методу, який ми хочемо запустити; Ми просто хочемо оновити компонент, FileTransferredToCloudпередавши його $refresh.  

Це воно! Ідіть і спробуйте і спостерігайте, як відбувається магія.

Бонус

за конфетті Який прогрес у реальному часі без конфетті? Точно, абсолютно нічого! Давайте дамо йому вибух конфетті, коли це буде зроблено з передачею.

Давайте встановимо нашу гармату confetti:Open resources/app.js і скористаємося Livewire.on() функцією прослуховування події confetti:

npm install --save canvas-confetti

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

Далі поверніться до компонента Livewire і зареєструємо нового слухача.

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

Далі створіть новий метод під назвою fireConfettiCannon і використовуйте помічника emit для випромінювання confetti події:

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

Це обгортка! 🎉 Я хотів би почути, як ви використовували цю техніку, щоб показати своїм клієнтам прогрес у реальному часі (і, можливо, трохи конфетті). Напишіть мені твіт @Philo01 і поговоріть! 🙌🏼


Wire Elements Pro - Майстерно створені компоненти Livewire Красиві компоненти 🚀

, створені за допомогою Livewire. Запустивши найближчим часом, обов'язково підпишіться.


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

Я думаю про випуск преміального відеокурсу найближчим часом (якщо достатньо людей зацікавлені), де я покажу вам, як створити реальну програму від початку до кінця за допомогою Laravel, Livewire та Tailwind CSS. Я буду охоплювати все, починаючи від реєстрації домену, налаштування сервера, написання тестів, ви називаєте це. Підпишіться, якщо ви зацікавлені та хочете отримувати сповіщення, і ви також можете отримати доступ безкоштовно, оскільки я буду роздавати під час запуску курсу.

Comments

No comments yet
Yurij Finiv

Yurij Finiv

Full stack

Про мене

Professional Fullstack Developer with extensive experience in website and desktop application development. Proficient in a wide range of tools and technologies, including Bootstrap, Tailwind, HTML5, CSS3, PUG, JavaScript, Alpine.js, jQuery, PHP, MODX, and Node.js. Skilled in website development using Symfony, MODX, and Laravel. Experience: Contributed to the development and translation of MODX3 i...

Про автора CrazyBoy49z
WORK EXPERIENCE
Контакти
Ukraine, Lutsk
+380979856297