PHPackages                             netipar/laravel-chunky - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [File &amp; Storage](/categories/file-storage)
4. /
5. netipar/laravel-chunky

ActiveLibrary[File &amp; Storage](/categories/file-storage)

netipar/laravel-chunky
======================

Chunk-based file upload package for Laravel with event-driven architecture, resume support, batch upload, and framework-agnostic frontend clients for Vue 3, React, Alpine.js, and Livewire.

v1.0.0(5d ago)02.1k↑1342.1%[6 PRs](https://github.com/NETipar/laravel-chunky/pulls)MITPHPPHP ^8.3CI passing

Since Mar 8Pushed 5d agoCompare

[ Source](https://github.com/NETipar/laravel-chunky)[ Packagist](https://packagist.org/packages/netipar/laravel-chunky)[ Docs](https://github.com/NETipar/laravel-chunky)[ RSS](/packages/netipar-laravel-chunky/feed)WikiDiscussions main Synced 2d ago

READMEChangelog (10)Dependencies (50)Versions (47)Used By (0)

  ![laravel-chunky](art/banner.svg)Chunky for Laravel
==================

[](#chunky-for-laravel)

[![Latest Version on Packagist](https://camo.githubusercontent.com/f3713b5fcf138a14f3b712841e611d572b06058b1ebd83eae8d3a073405fe896/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6e6574697061722f6c61726176656c2d6368756e6b792e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/netipar/laravel-chunky)[![Tests](https://camo.githubusercontent.com/d41f574160853c2bc2406288bf89456493936ca360c168dd92914803d684472a/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f4e4554697061722f6c61726176656c2d6368756e6b792f74657374732e796d6c3f6272616e63683d6d61696e266c6162656c3d7465737473267374796c653d666c61742d737175617265)](https://github.com/NETipar/laravel-chunky/actions?query=workflow%3ATests)[![Total Downloads](https://camo.githubusercontent.com/6de5c7920b3dca0138f4d2c87960e0b4b7938270997b70d2da1cd87014d78dad/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f6e6574697061722f6c61726176656c2d6368756e6b792e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/netipar/laravel-chunky)

Resumable, chunk-based file uploads for Laravel — upload large files reliably over flaky connections. A small, typed backend built on **ports &amp; adapters**, and a framework-agnostic frontend engine with first-class wrappers for **Vue 3**, **React**, **Alpine.js**, and **Livewire**.

Works out of the box with **no queue worker and no broadcasting** — the direct upload response already carries the result. Turn those on later for scale.

> **v1.0 is a ground-up rewrite.** Coming from `0.x`? See [UPGRADE.md](UPGRADE.md).

Contents
--------

[](#contents)

- [Why Chunky](#why-chunky)
- [Requirements](#requirements)
- [5-minute quickstart](#5-minute-quickstart)
- [How it works](#how-it-works)
- [Upload profiles](#upload-profiles)
- [Frontend](#frontend)
- [Assembly modes (sync / queue / auto)](#assembly-modes)
- [Direct-to-S3 uploads](#direct-to-s3-uploads)
- [Batch uploads](#batch-uploads)
- [Authorization](#authorization)
- [Events &amp; broadcasting](#events--broadcasting)
- [Errors](#errors)
- [Console commands](#console-commands)
- [Configuration](#configuration)

Why Chunky
----------

[](#why-chunky)

- **Resumable.** Chunks are tracked server-side; a reload resumes the same upload via a file fingerprint instead of re-sending bytes.
- **Navigation-proof.** On the frontend, uploads are owned by a module-level manager — switching pages (SPA) doesn't cancel them.
- **Complete without infra.** In the default `auto` mode small files assemble in-request and the final response contains the file + your payload. No Echo, no worker required.
- **One state machine, two drivers.** Database or filesystem tracking sit behind the same contract, verified by the same test suite — no driver drift.
- **Typed end to end.** PHPStan level max on the backend, strict TypeScript on the frontend.

Requirements
------------

[](#requirements)

- PHP **8.3+**, Laravel **12 or 13**
- A queue worker only if you use `assembly.mode = queue` (or `auto` for files above the threshold)
- Broadcasting (Echo/Reverb) is entirely optional

5-minute quickstart
-------------------

[](#5-minute-quickstart)

**1. Install the backend.**

```
composer require netipar/laravel-chunky
php artisan chunky:install   # publishes config + migrations
php artisan migrate
```

**2. Register an upload profile** — where files go and what to do when they finish. In a service provider:

```
use NETipar\Chunky\Facades\Chunky;

Chunky::simple('avatars', 'avatars', ['max_size' => 10 * 1024 * 1024, 'mimes' => ['image/jpeg', 'image/png']]);
```

For real logic (e.g. creating a Media record), generate a class:

```
php artisan make:chunky-profile AvatarProfile
```

```
use NETipar\Chunky\Profiles\CompletedUpload;
use NETipar\Chunky\Profiles\UploadContext;
use NETipar\Chunky\Profiles\UploadProfile;

class AvatarProfile extends UploadProfile
{
    public function rules(): array
    {
        return ['file_size' => ['max:10485760'], 'mime_type' => ['in:image/jpeg,image/png']];
    }

    public function directory(UploadContext $context): string
    {
        return "avatars/{$context->userId()}";
    }

    public function authorize(UploadContext $context): bool
    {
        return $context->user !== null;
    }

    public function completed(CompletedUpload $upload): array
    {
        $media = Media::create(['disk' => $upload->disk, 'path' => $upload->path, 'user_id' => $upload->userId]);

        return ['media_id' => $media->id]; // becomes the response `payload`
    }
}
```

Register it in `config/chunky.php`:

```
'profiles' => ['avatar' => \App\Chunky\Profiles\AvatarProfile::class],
```

**3. Upload from the frontend.**

```
npm install @netipar/chunky-core
```

```
import { manager, configure } from '@netipar/chunky-core';

configure({
    baseUrl: '/api/chunky',
    headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]')!.getAttribute('content')! },
});

const uploader = manager.upload(file, { profile: 'avatar' });

uploader.subscribe((state) => {
    console.log(state.status, state.progress, state.etaSeconds);
});

const result = await uploader.upload(); // resolves once assembled
console.log(result.file?.url, result.payload?.media_id);
```

That's it — no worker, no broadcasting. Files up to 256 MB assemble in the request and the result comes straight back.

How it works
------------

[](#how-it-works)

```
initiate ──▶ upload chunks (concurrent, retried) ──▶ assemble ──▶ completed
   POST /upload          POST /upload/{id}/chunks         (sync or queued)

```

1. **Initiate** returns an `upload_id`, `chunk_size`, and `total_chunks`. If a matching fingerprint is found, it resumes an existing upload instead.
2. The client uploads chunks concurrently, retrying transient failures.
3. On the final chunk the server **assembles** the file (streaming, never loading it all into memory), runs your profile's `completed()` hook, and — in sync mode — returns the result inline.

The full wire protocol is documented in [`docs/en/protocol.md`](docs/en/protocol.md) ([magyarul](docs/hu/protocol.md)) and [`docs/openapi.yaml`](docs/openapi.yaml).

Upload profiles
---------------

[](#upload-profiles)

A profile is the single place that decides validation, destination, authorization, and the post-assembly hook. Two ways to define one:

- **`Chunky::simple($name, $directory, $options)`** — a fixed directory + optional `max_size` / `mimes`.
- **A class extending `UploadProfile`** — override `rules()`, `disk()`, `directory()`, `authorize()`, `fileName()`, `completed()`, `maxFileSize()`.

The batch member endpoint validates against the **batch's** profile, so a client can't smuggle in a more permissive one.

Frontend
--------

[](#frontend)

All wrappers are thin adapters over `@netipar/chunky-core`. The golden rule: **a component unmounting only unsubscribes — it never cancels the upload.** The upload lives in the manager and survives navigation.

### Core (any framework)

[](#core-any-framework)

```
import { manager, configure, Uploader } from '@netipar/chunky-core';

const uploader = manager.upload(file, { profile: 'avatar' });
uploader.on('completed', (result) => { /* ... */ });
uploader.on('failed', (error) => console.error(error.code, error.message));

uploader.pause();
uploader.resume();
await uploader.cancel();

// Warn on real page unload while uploads are active (SPA nav is unaffected):
manager.installUnloadGuard();
```

`uploader.getState()` returns an immutable snapshot: `{ status, progress, uploadedChunks, totalChunks, bytesPerSecond, etaSeconds, file, result, error }`.

For image uploads, `uploader.previewUrl()` lazily creates a cached object URL you can drop into an `` (null for non-image files); the manager revokes it when the upload is evicted via `manager.remove()`. Need a real downscaled thumbnail instead? `await createThumbnail(file, { maxDimension: 256 })` returns a `Blob` (WebP by default), dependency-free.

Want end-to-end verification? `manager.upload(file, { profile: 'avatar', fileChecksum: true })` hashes the whole file (SHA-256, dependency-free) in parallel with the upload and the server verifies the assembled result against it.

### Vue 3 — `@netipar/chunky-vue3`

[](#vue-3--netiparchunky-vue3)

```
import { createChunky } from '@netipar/chunky-vue3';

app.use(createChunky({ baseUrl: '/api/chunky', headers: { 'X-CSRF-TOKEN': token } }));
```

```

import { useUpload } from '@netipar/chunky-vue3';

const { start, state, previewUrl } = useUpload();
const onPick = (e: Event) => start((e.target as HTMLInputElement).files![0], { profile: 'avatar' });

```

`useUploads()` gives a reactive list of all active uploads (for a global tray); `UploadTray` and `ChunkDropzone` are headless components you can style.

### React — `@netipar/chunky-react`

[](#react--netiparchunky-react)

```
import { ChunkyProvider, useUpload } from '@netipar/chunky-react';

function Uploader() {
    const { start, state } = useUpload();
    return (

             start(e.target.files![0], { profile: 'avatar' })} />
            {state && }

    );
}

// …
```

### Alpine.js — `@netipar/chunky-alpine`

[](#alpinejs--netiparchunky-alpine)

```
import { registerChunky } from '@netipar/chunky-alpine';
Alpine.plugin((Alpine) => registerChunky(Alpine, { baseUrl: '/api/chunky' }));
```

```

```

### Livewire

[](#livewire)

`composer require livewire/livewire`, register the Alpine component as above, then drop in the widget:

```

```

Assembly modes
--------------

[](#assembly-modes)

`config('chunky.assembly.mode')` — `auto` (default), `sync`, or `queue`.

ModeFinal chunk responseNeeds a worker?`sync``{"status":"completed", "file":…, "payload":…}`No`queue``{"status":"assembling"}` — client polls statusYes`auto``sync` under `assembly.sync_threshold` (256 MB), else `queue`Only for large filesThe frontend `await uploader.upload()` resolves with the final result in **all** modes — it polls automatically when assembly is queued.

Direct-to-S3 uploads
--------------------

[](#direct-to-s3-uploads)

For the biggest scale win, a profile can opt into the `direct_s3` transport — chunks go straight to S3 as multipart parts via presigned URLs, and Laravel only orchestrates (initiate, URL issuing, complete, abort). PHP never touches the bytes and there is no merge step: assembly *is* the S3 `CompleteMultipartUpload`.

```
class VideoProfile extends UploadProfile
{
    public function transport(): string
    {
        return 'direct_s3';
    }

    // directory(), completed(), … as usual
}
```

Set `chunky.transports.direct_s3.disk` to your s3 disk, require `aws/aws-sdk-php`, and add `ExposeHeaders: ETag` to the bucket CORS ([recipe](docs/en/configuration.md)). The frontend clients pick the transport up automatically from the initiate response — the caller API (`upload`/`pause`/`resume`/`cancel`/`subscribe`) is unchanged. Works with S3-compatible targets (MinIO; Cloudflare R2 best effort). Wire details in [`docs/en/protocol.md`](docs/en/protocol.md).

Batch uploads
-------------

[](#batch-uploads)

```
const batch = manager.batch([file1, file2, file3], { profile: 'gallery', concurrency: 3 });
batch.subscribe((s) => console.log(`${s.completed}/${s.total}`));
const result = await batch.upload(); // { status: 'completed' | 'partially_completed' | 'cancelled', results }
```

Cancelling a batch cancels every non-terminal member on **any** tracker driver.

Authorization
-------------

[](#authorization)

Uploads are owned by the authenticating user. Non-owners are answered with **404** on status/cancel (so upload ids can't be enumerated) and **403** on chunk POST. Anonymous uploads stay open for backward compatibility. Swap the `Authorizer` in `config/chunky.php` to customize.

Events &amp; broadcasting
-------------------------

[](#events--broadcasting)

Eleven events fire through the lifecycle (`UploadInitiated`, `ChunkUploaded`, `UploadCompleted`, `BatchCompleted`, …). Listen to them like any Laravel event.

Broadcasting is **off by default** (`broadcasting.enabled`). When enabled, payloads are sanitized (`disk`, `final_path`, `user_id` never leave the server) and versioned (`{"v":1,…}`). High-frequency events (`ChunkUploaded`, …) are excluded by default so turning it on only pushes the useful completion events.

Errors
------

[](#errors)

Every non-2xx response is a machine-readable envelope:

```
{ "error": { "code": "upload_expired", "message": "The upload has expired." } }
```

The frontend surfaces this as a typed `ChunkyError` with a stable `.code` (`validation_failed`, `unauthorized`, `upload_not_found`, `invalid_state`, `checksum_mismatch`, `lock_timeout`, …). Retry decisions use the code, never the message. Full table in [`docs/en/protocol.md`](docs/en/protocol.md).

Console commands
----------------

[](#console-commands)

CommandWhat it does`chunky:install`Publish config + migrations`chunky:doctor`Live health checks: disks, queue worker probe (`--wait=5`), broadcast driver, locking, tracker — exits non-zero on errors (CI/deploy gate)`chunky:cleanup`Remove expired, unfinished uploads and their chunks (schedule it)`make:chunky-profile`Generate an `UploadProfile` classConfiguration
-------------

[](#configuration)

`config/chunky.php` is validated at boot into a typed `ChunkyConfig` — a bad value fails fast with the offending key. Full reference and deployment recipes in [`docs/en/configuration.md`](docs/en/configuration.md) ([magyarul](docs/hu/configuration.md)).

---

- [UPGRADE.md](UPGRADE.md) — migrating from `0.x`
- [CHANGELOG.md](CHANGELOG.md) — release history
- [SECURITY.md](.github/SECURITY.md) — supported versions and reporting
- [CONTRIBUTING.md](.github/CONTRIBUTING.md) — development setup

Licensed under the [MIT license](LICENSE.md).

###  Health Score

51

—

FairBetter than 95% of packages

Maintenance99

Actively maintained with recent releases

Popularity21

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity62

Established project with proven stability

 Bus Factor1

Top contributor holds 95% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~4 days

Recently: every ~23 days

Total

37

Last Release

5d ago

Major Versions

v0.22.6 → v1.0.02026-08-06

PHP version history (2 changes)v0.2.0PHP ^8.2

v1.0.0PHP ^8.3

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/13118776?v=4)[NETipar](/maintainers/NETipar)[@NETipar](https://github.com/NETipar)

---

Top Contributors

[![hegedustibor](https://avatars.githubusercontent.com/u/6483104?v=4)](https://github.com/hegedustibor "hegedustibor (96 commits)")[![dependabot[bot]](https://avatars.githubusercontent.com/in/29110?v=4)](https://github.com/dependabot[bot] "dependabot[bot] (5 commits)")

---

Tags

laravels3filelivewireuploadchunkreactechoBroadcastingfile-uploadalpinevueresumablelarge filesbatch-uploadnetipar

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/netipar-laravel-chunky/health.svg)

```
[![Health](https://phpackages.com/badges/netipar-laravel-chunky/health.svg)](https://phpackages.com/packages/netipar-laravel-chunky)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3345.4M354](/packages/psalm-plugin-laravel)[laravel/scout

Laravel Scout provides a driver based solution to searching your Eloquent models.

1.7k57.2M676](/packages/laravel-scout)[roots/acorn

Framework for Roots WordPress projects built with Laravel components.

9922.4M146](/packages/roots-acorn)[api-platform/laravel

API Platform support for Laravel

58190.1k21](/packages/api-platform-laravel)[flat3/lodata

OData v4.01 Producer for Laravel

99357.6k](/packages/flat3-lodata)[pressbooks/pressbooks

Pressbooks is an open source book publishing tool built on a WordPress multisite platform. Pressbooks outputs books in multiple formats, including PDF, EPUB, web, and a variety of XML flavours, using a theming/templating system, driven by CSS.

45844.8k1](/packages/pressbooks-pressbooks)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
