ami-m/vxjs-upload-bundle
A Symfony bundle for integrating VXJS file uploads into PHP applications. It provides configurable server-side upload handling and endpoints, helping you add streamlined, form-friendly uploads with minimal setup.
Reality check first: this is a Symfony bundle, not a Laravel package. It ships a Bundle subclass, a DI Extension, YAML/XML service definitions, and Twig/asset references to Blueimp jQuery File Upload. composer require ami-m/vxjs-upload-bundle in a Laravel app will autoload classes but won't register anything — there is no service provider, no Laravel config publisher, and no facade. If you are strictly on Laravel, skip straight to the "harvest the pattern" path below.
If you are on Symfony (or a Symfony admin/back-office alongside Laravel):
config/bundles.php:
AmiM\VxJsUploadBundle\VxJsUploadBundle::class => ['all' => true],
php bin/console config:dump-reference vx_js_upload
DependencyInjection/VxJsUploadExtension.php) and the bundle's Resources/config/*.yml before writing any code. The service IDs and the controller route it registers are the real API surface, and they are not documented anywhere else./upload or configured via the extension). Hit it with curl to confirm it responds before wiring any front end:
curl -F "files[][email protected]" http://localhost:8000/upload
url: at that route. The default AJAX contract is Blueimp's — files[] multipart field names, JSON {files: [{name, size, url, thumbnailUrl, error}]} responses.First real use case: a single "attach a file" endpoint in a back-office form — avatar upload or a support-ticket attachment. That is the smallest thing the bundle does end-to-end, and it will expose every config quirk you need to know before scaling to multi-file or chunked flows.
If you are on Laravel: don't install it. Read the upload controller and validation config (10–15 minutes), extract the contract — accepted MIME/extensions, max size, error-message shape, response JSON — and reimplement it with UploadedFile, a Form Request, and a Flysystem disk. You will spend less time than you would porting Symfony DI into a service provider, and you inherit nothing unmaintained.
Configuration-driven limits, per environment. The bundle's value is that validation (max size, MIME types, extensions) lives in config rather than hardcoded controller logic. In Symfony, split this across config/packages/vx_js_upload.yaml and config/packages/dev/vx_js_upload.yaml so local dev accepts permissive types while prod is strict. The day-to-day habit: any change to accepted types is a config diff, reviewed like code, not a controller edit.
Storage target selection. The bundle abstracts storage behind a pluggable adapter. Two patterns in practice:
%kernel.project_dir%/var/uploads and serve via a controller or a Symfony route rather than exposing the directory directly. This is the path of least resistance and the most common failure point — see Gotchas.Front-end integration tip. Blueimp's widget expects its own response shape. If you swap in a service decorator or override the controller, keep the response contract identical:
{"files": [{"name": "a.jpg", "size": 1234, "url": "/uploads/a.jpg", "error": null}]}
Breaking this silently breaks per-file progress and error display in the UI, and the failure mode looks like "uploads just stopped" rather than a 500.
Overriding the controller. The clean Symfony pattern is to subclass or decorate the bundled upload service in services.yaml rather than fork the bundle. This lets you add auth checks (the bundle does not ship authorization), tenant-scoped paths, or post-upload events without touching vendor code. If you find yourself editing under vendor/, you have misread the extension points.
Post-processing hook. The request path should stay fast. Fire a Symfony event or Message dispatched to Messenger after the file lands, and do thumbnails, virus scan, and metadata extraction in a worker. This is the single most important pattern for making the bundle production-viable — the bundled flow persists the file and returns, and nothing else.
Laravel-side pattern (if harvesting). The equivalent shape is a Form Request with mimes:jpg,png,pdf|max:10240, a store() against a dedicated uploads disk, and a queued ProcessUpload job. Keep the Blueimp response contract only if you actually use Blueimp on the client; with Uppy/FilePond the contract changes and you should not force it.
Framework mismatch is the first gotcha. Any Laravel developer expecting php artisan vendor:publish will find nothing. The bundle has no Laravel integration, no facade, no config file to publish, and no route registration. Do not spend time trying to make it work — that rabbit hole is deeper than reimplementing the flow.
The bundle does not handle authorization. It exposes an upload endpoint; it does not ship CSRF handling, authentication guards, or rate limiting. In Symfony, secure the route with a security.yaml access control rule. In a harvested implementation, put it behind your framework's auth middleware. Leaving it open is a trivial DoS and storage-fill vector.
Content-type verification is your job. The bundle validates declared MIME and extension. That does not stop a PHP file renamed to .jpg with a spoofed header, and it does not stop polyglot files. Add magic-byte sniffing (finfo), and for images, re-encode rather than trust the upload. This is not a "later" item.
The jQuery/Blueimp front end is architecturally dated. If your UI is Vue, React, Inertia, or Livewire, wrapping this client will fight your component model and design system. Use only the server-side half as reference and pair it with Uppy, FilePond, or Dropzone — all of which are maintained and framework-agnostic.
Chunked and resumable uploads are deceptively stateful. If you enable them, test under load and with interruption — orphaned partial files on the server and truncated files on the client are the characteristic bugs, and they don't appear in happy-path testing. Reconcile and clean up partials on a schedule regardless.
File-naming collision handling is not free. Concurrent uploads of image.jpg will overwrite each other unless the storage adapter or your decorator generates unique names (UUID, hash, or timestamp+random). Check this explicitly before going to prod — the bundle does not guarantee uniqueness.
Serving uploaded files is a separate concern. Do not point a web server document root at the upload directory. Route file serving through your framework (Symfony controller / Laravel controller) so you can enforce access control, or use short-lived signed URLs from object storage. This is where most "uploaded file exposed" incidents originate.
Debugging tip. When uploads fail with no obvious error, the failure is almost always in one of four places: the multipart field name doesn't match what the controller expects (Blueimp sends files[]), the configured max_size is below the PHP upload_max_filesize/post_max_size (in which case PHP discards the request silently and you get a confusing empty $_FILES), the storage directory lacks write permissions, or the response JSON shape is wrong so the client reports a generic failure while the server actually succeeded.
Extension points worth knowing. The DI extension, the storage adapter interface, and the upload service are the three places to hook in without forking. Anything beyond these three and you are maintaining a fork of an unmaintained bundle — at which point the ownership question (who fixes the next CVE?) belongs in your ADR, not in the PR.
Maturity signal. Zero stars, zero dependents, zero score, no SECURITY.md. Treat it as reference code you may copy, not a dependency you install on a Tier-1 security path. If a team member insists on adopting it, write the ADR first and name the internal owner — an unmaintained upload handler without an owner is a latent incident.
How can I help you explore Laravel packages today?