makinacorpus/filechunk-bundle
A Symfony bundle for reliable chunked file uploads: large files are split into smaller parts on the client and reassembled server-side, helping you bypass upload size limits and handle big transfers with Symfony forms and storage.
Where to look first. Before writing a line of code, clone the repo and read four things in this order: the README.md, src/DependencyInjection/Configuration.php (the authoritative list of every config key — the README drifts), src/Form/Type/ (the form type you actually mount in your form), and src/Resources/public/ (the JS widget). The code, not the prose, is the spec; that is doubly true now that a 2.0.0 line exists but is still shipping alpha builds.
Minimal install. composer require makinacorpus/filechunk-bundle gets you the latest tag — which as of 2.0.0-alpha11 is alpha. Don't accept that implicitly in a production composer.json: pin an explicit constraint ("2.0.0-alpha11") or a VCS commit, because alpha releases can and do break the form-type and controller signatures between builds. If you need something stable, you're stuck on the ancient 1.x line and its 2019-era Symfony 4/5 constraints, which is its own fork-or-downgrade fight. Register the bundle in config/bundles.php, add a filechunk: block to config/packages/ with a writable staging directory, a maximum file size, and allowed MIME types. Publish/symlink the public assets (bin/console assets:install) or import the widget JS through Encore/AssetMapper.
First use case. The one that justifies the dependency: a form where the user uploads a file larger than upload_max_filesize/post_max_size — a video, a database dump, a multi-hundred-MB archive. The widget slices the file in the browser and POSTs numbered chunks; the server appends to a staging file and, on the final chunk, hands you back something you can move into permanent storage. Wire that single field and verify the round trip end to end before touching anything else. The new "directory listing" capability is a maintenance/admin nicety, not the reason you're here — ignore it until the upload path is proven.
If you are on Laravel: stop. This is a Symfony bundle with a Twig-rendered form type and Symfony's Form/HttpFoundation contracts; there is still no Laravel service provider, no config publishing, and no Laravel-native tests. Use pion/laravel-chunk-upload or Livewire's built-in chunked uploads, and mine this bundle's JS widget only for the chunking logic. The 2.0.0 alpha has not changed this.
Form-type integration (the intended path). Mount the bundle's chunk form type inside your existing Symfony form, mapped to whichever property holds the file path or File reference. Keep it a separate field rather than swapping out an existing FileType, so small files stay on the battle-tested path. Pair it with VichUploader or a Flysystem-backed entity listener for final persistence — the bundle's job ends when the assembled temp file exists.
Standalone endpoint pattern. If the shipped controller's storage semantics don't fit (likely), reuse only the front-end widget and point it at your own controller. The contract: a POST carrying an upload identifier, a chunk index, and the blob; the server writes {$staging}/{$id}.{$index} and returns 200. A finalize request merges in index order, validates, and atomically moves to permanent storage. Keep a small JSON manifest per upload id so you can resume and a janitor can find orphans.
Directory listing (new in 2.0.0-alpha11). Treat this as an admin/maintenance surface, not part of the upload flow. The realistic day-to-day use is an internal status page — list staging contents for a given upload id, spot orphaned partials, or build a small ops dashboard. Do not expose it to end users. If you wire it up, gate it behind your admin firewall and a role check, and point it at a dedicated (non-public) directory.
Chunk size is a config decision, not a default. Set chunk_size < post_max_size and < nginx client_max_body_size, with headroom for multipart overhead. 5–8 MB is sane; larger buys nothing and reintroduces the original problem on constrained hosts.
Validate after merge, never per chunk. Individual chunks are meaningless fragments — MIME-sniffing a 4 MB slice of an MP4 tells you nothing. Run size, extension, and real MIME checks on the assembled file, and sanitize the client filename then (basename it, strip .., generate your own storage name).
Front-end wiring. Enqueue the widget's JS through your normal pipeline, pass the endpoint URL and CSRF token into the template, and surface per-chunk progress/retry. Long uploads outlive short sessions — refresh the token or use a dedicated upload token not bound to the user session.
Operational hygiene. A --dry-run-capable cleanup command that deletes staging files older than N hours is not optional; abandoned uploads are the normal case. The new directory listing helps you audit that directory, but it doesn't replace the scheduled janitor. Monitor staging directory size — it's the first thing to fill a disk.
The "abandoned" story is out of date — the risk just changed shape. This is no longer a last-release-2019 dead project: there's an active 2.0.0 line, currently at alpha11. That is better than nothing and materially better on paper than the old assessment, but alpha software on the eleventh alpha is a signal in itself: expect churn, incomplete 2.x compatibility work, and no stable tag to pin. The headline risk is now "unstable, alpha-only 2.x line with no LTS," not "abandoned since 2019." Budget for API churn between alpha builds and pin exactly.
GPL-2.0 is still a product decision. Copyleft, no exception. If this ships in a closed-source SaaS or distributed commercial product, get legal sign-off before it lands in composer.json. The JS widget alone may be separable; the bundle as a whole is not. The 2.0.0 alpha did not change the license.
Directory listing = information disclosure if misconfigured. A new listing surface is a classic way to leak staged filenames, user data, or full paths. Confirm it is access-controlled, that it cannot be pointed outside the staging root, and that it does not render server-side absolute paths. Assume default-deny until you have read the controller.
Path traversal and filename trust. Whatever the bundle does with $_FILES['name'], assume the worst and validate. Client filenames reach the staging path in most implementations of this era. The new listing feature can expose the result of any traversal bug that already exists.
Reverse proxy will bite you first. Nginx client_max_body_size defaults to 1 MB, and PHP-FPM request_terminate_timeout kills long-running finalize requests. Nearly every "works locally, fails in staging" report on chunked uploads is one of these two.
Debugging sequence. Network tab first: confirm every chunk returns 200 and indices are contiguous. Then check the staging directory actually contains the files (the new listing endpoint is a convenient way to do this). Then var/log/dev.log for the finalize step. If chunks arrive but the merge is empty, it's almost always an ordering or index-offset bug in the widget config, not the server.
Alpha upgrade discipline. Before bumping 2.0.0-alphaN to alphaN+1, diff the form type and controller signatures, and run your chunk→merge→move integration test. Alphas don't promise BC, and form-type option changes are the most common break.
Non-atomic merges. If the process dies mid-merge you get a truncated file with a valid-looking name. Always merge to foo.part and rename() on success — a same-filesystem rename is atomic and cheap.
Extension points worth knowing. The Twig form theme is overridable per form, the storage/controller layer is decoratable via the service container, and the JS widget is plain and forkable. Plan to own all three.
If you fork it: pin the commit in composer.json with a VCS repository entry, vendor the diff, and keep at least one integration test that exercises a full chunk→merge→move cycle. That test is the only thing that will tell you when the next alpha or a Symfony upgrade breaks it.
How can I help you explore Laravel packages today?