kolyadin/sonata-media-chunk-uploader
Adds chunked upload support to Sonata Media, allowing large files to be sent in smaller pieces to avoid request-size limits and improve upload reliability. Integrates with the Sonata admin and media workflow for smoother file handling.
Start with the disclaimers, because they change everything about how you leverage this: kolyadin/sonata-media-chunk-uploader is a Symfony bundle for Sonata Media (and Sonata Admin), not a Laravel package. There is no service provider, no config/*.php, no Laravel facades, and nothing that autoloads in a standard Laravel application. If your day-to-day is Laravel, the honest assessment is: this package is a reference implementation you read, not a dependency you install. The idea — split a large file into parts, persist parts to a temp directory keyed by an upload id, then reassemble into a normal media record — is what you port. The code is not.
So the first decision is a fork in the road:
pion/laravel-chunk-upload (or a tus server such as tus-php/tusd) and store the final asset through Laravel's Storage facade or direct-to-S3 multipart with presigned URLs. The section "Implementation Patterns" below still applies conceptually to that build.If you are inside a Sonata project, minimal steps:
sonata-project/media-bundle is installed and a media context exists (e.g. a default context with a filesystem/lsystem provider). This package is a thin layer on top of Sonata Media — it will not bootstrap media, providers, or admin for you.composer require kolyadin/sonata-media-chunk-uploader and pin an exact version (or a commit hash) in composer.json. With 0 stars and 0 dependents there is no release cadence to lean on.config/bundles.php (or AppKernel::registerBundles() on pre-Flex) after SonataMediaBundle.resource: "@SonataMediaChunkUploaderBundle/Resources/config/routing.xml" at /admin/media/chunk. Keep it inside the admin firewall.DependencyInjection/Configuration.php before trusting any default — chunk size, temp directory, target media context, and provider are the keys that matter.Where to look first, in this order:
README.md — probably the only docs. Treat it as a hypothesis, not a contract.src/Controller/ — the upload/receive/finalize endpoints are the product. Read them line by line; this is where auth, filename handling, and temp paths live.src/Resources/config/ — services, routing, DI parameters; the temp directory and chunk-naming scheme are defined here.src/Resources/public/js/ or assets/ — if a JS chunker ships, that's your whole client contract; if it doesn't, you're supplying resumable.js/flow.js/Uppy yourself.composer.json — PHP constraint, Symfony LTS range, Sonata Media major. This single file tells you whether the spike is even viable.The first concrete use case to validate: an editor uploads a 1.5–2 GB video through the Sonata admin form on a throttled connection, kills the browser mid-upload, resumes, and the resulting Media record is a normal Sonata media item that the existing provider and front-end template render without modification. If that doesn't work end to end, nothing else in the package matters.
Treat it as vendored glue, not a library. The pattern that survives in practice is fork-and-pin: copy the source into your monorepo (or a private fork), pin the commit, and add a thin internal wrapper service that your admin form and controllers call. Never let application code import the bundle's classes directly — the wrapper is the seam that lets you swap to a maintained alternative (OneupUploaderBundle, a tus server, direct-to-S3) without touching call sites.
Model the contract explicitly. Chunked upload is a two-sided protocol, and packages in this space frequently ship only half. Write down the contract you're committing to: a create/init call returning an upload id, a per-chunk POST carrying (upload id, chunk index, total chunks), and a finalize/assemble call. Then decide on an idempotency strategy — a client-supplied Idempotency-Key per upload that finalize honors, so a retried finalize returns the existing Media id instead of creating a second record. Clients will retry finalize after a timeout.
Keep the admin-only blast radius. Wire this behind the Sonata admin firewall and a feature flag scoped to staff roles. This is not a public upload endpoint, and a zero-adoption package should not become one. If you need public uploads, build that separately with review.
Match chunk size to your actual PHP/nginx limits. The whole point is bypassing upload_max_filesize, but each chunk is still a normal request — it must fit under post_max_size and nginx's client_max_body_size. Set chunk size to something like 4–8 MB, comfortably under both, and document it in config rather than relying on the package default.
Centralize temp storage and lifecycle. All chunk parts should land in one configured directory (or object-store prefix). Pair that with a scheduled cleanup command — a Symfony console command, or a queued job if you're cross-service — that deletes chunk directories older than N hours. Orphaned partials are the slow-burn failure mode: disk fills silently over weeks of abandoned uploads.
Integrate at the form level, not the model level. The value proposition is that reassembly produces a standard Sonata media item, so providers, thumbnails, and templates keep working. Preserve that: the finalize step should hand off to Sonata's normal media creation path (MediaManager::save(), provider create()) rather than writing to storage itself. That's what keeps the "drop-in" claim true and keeps your exit path cheap.
Plan for multi-node from day one. If you run more than one app server behind a load balancer, temp chunks must live on shared storage (object store or a shared volume) or a resumed chunk will hit a node that never saw the earlier parts. If the package assumes sys_get_temp_dir(), that's a blocking mismatch, not a config tweak.
Client-side, borrow rather than build. Uppy, resumable.js, or flow.js give you chunking, retry, and pause/resume for free. Whatever ships in the package, verify its retry behavior on a flaky connection with throttling enabled — that resume path is the actual feature.
Test the failure modes deliberately. Run a spike against your exact Symfony/Sonata versions: upload 2 GB over throttled network, kill the browser mid-flight, resume, retry finalize twice, and inspect the temp directory afterwards. Add a quick security pass on the chunk and finalize endpoints — authorization, filename sanitization, MIME validation, and a cap on concurrent in-flight uploads — before this touches real content.
The biggest gotcha is the category error. It is not a Laravel package. Every hour spent trying to make it autoload in a Laravel app is wasted; the correct move is to read the chunk-assembly code for ideas and build (or adopt pion/laravel-chunk-upload) natively.
Zero adoption means zero safety net. No dependents, no bug reports to mine, no de facto security review, no upgrade path when Sonata or Symfony majors move. Budget for owning it: a CI pipeline you write, periodic rebases, and a named maintainer. Without a name attached, it orphans the moment the integrating engineer rotates off.
Check composer.json and the license first. PHP constraint, Symfony LTS range, and the Sonata Media major are the most likely breakage vectors, and the license may not be what you assume for a package you intend to fork.
Per-chunk limits still bite. upload_max_filesize is bypassed, but post_max_size, max_input_time, nginx client_max_body_size, and proxy read timeouts all still apply — the last two especially on the finalize request, which is I/O-heavy over a large assembled file and prone to 504s. Tune the finalize route's timeout separately from the chunk route's.
Sanitize filenames and don't trust the client MIME type. Chunk upload handlers are a well-known attack surface: path traversal via crafted filenames, extension spoofing, chunk-flood DoS, weak or absent auth on finalize. Sniff the assembled file's real type server-side and generate the stored name yourself.
Expect orphaned chunks and plan for them. Abandoned uploads leave directories behind; without a cleanup cron this becomes disk exhaustion. Log chunk directory creation and deletion so you can see the leak in staging before it hits production.
Watch for duplicate media on retry. A client retrying finalize without idempotency produces two Media rows pointing at (or copying) the same asset. Verify whether the package guards this; if not, add the guard in your wrapper.
Multi-node deployments break local temp storage. Resumability across a load balancer requires shared chunk storage. If your infrastructure is horizontally scaled and ephemeral, this is the blocker that kills the spike.
Asset pipeline drift. If the bundle ships JS assuming Assetic and you're on Webpack Encore (or vice versa), you'll be hand-wiring the client anyway. Check Resources/public/ before promising a UI.
Debugging tips. When an upload fails, look in this order: the configured temp/chunk directory on the node that served the request (are partials present and correctly numbered?), the bundle's service config for the target media context (wrong context = silent failure at finalize), Symfony logs for the finalize exception, and the browser network tab for the actual chunk size versus post_max_size. Most "it just hangs" reports are a chunk exceeding the server limit or finalize hitting a proxy timeout — not a package bug.
Extension points worth knowing. The wrapper service you write is the real extension point; the bundle's DI parameters (temp path, chunk size, media context) are the others. Keep configuration in your app's config files rather than editing vendored source, so rebases stay cheap.
How can I help you explore Laravel packages today?