kitpages/file-bundle
A Symfony bundle for file and image upload management. It offers Doctrine-backed file entities, configurable storage, and thumbnail support, simplifying attaching and reusing uploaded files across your application.
Start with a reality check. kitpages/file-bundle is a Symfony 2.x-era bundle built as a satellite of KitpagesCmsBundle — there is no service provider, no facade, no publishable config, no Laravel integration surface, and no composer.json type that Laravel understands. composer require kitpages/file-bundle in a Laravel app will fail on transitive constraints (symfony/symfony: 2.*, old doctrine/common) long before you get to anything useful. For a Laravel codebase, the correct mental model is: this is reference material and a migration source, not a dependency. Do not add it to composer.json.
Given that, the minimal "getting started" is a 30-minute orientation against a scratch clone:
git clone https://github.com/kitpages/KitpagesFileBundle /tmp/kfb and open the tree. You are looking for four things: the File entity (the data model), the Doctrine event listeners (upload/delete lifecycle), the storage helper (where bytes actually go on disk), and the Twig extension (how templates render/download files).Resources/config/ and the bundle's DI extension class — the YAML knobs for upload path, allowed file types, and thumbnail sizes are the actual contract. Everything else is plumbing around those three values.Symfony\Component\HttpFoundation\UploadedFile, a target entity reference, and a persist/redirect. That whole flow is what Laravel's Storage + a Media model replaces.First use case — pick one:
File table columns, the on-disk directory convention, and how files are associated to CMS pages/blocks. That document is the input to your Laravel media migration and ETL script. This is the highest-value thing the repo will ever do for you.Storage disks, a polymorphic Media model, Intervention\Image (or Glide) in a queued job, a Blade component, and a deleting model hook. You will find the native Laravel version is smaller than the bundle — that is the useful takeaway, and you should close the tab and not think about the repo again.The patterns below are how a Laravel developer actually uses this bundle: as a porting blueprint for the CMS file layer, and as a read-only partner during a strangler migration off a legacy Kitpages deployment.
Porting the File entity to Eloquent. The bundle's File entity is close to a minimal Media model. Expect fields roughly equivalent to: id, original filename, storage filename/path, mime type, size, and a back-reference to the owning CMS record. Port it as App\Models\Media with morphTo ownership (mediable_type / mediable_id) so the "attach one or many files to any entity" feature maps to a polymorphic relation instead of the bundle's CMS-bound association. The bundle's one-or-many distinction is usually a cardinality convention on the owner side, not a different table — reproduce that with morphMany vs morphOne on the parent model.
Mapping Doctrine listeners to Laravel lifecycle hooks. The bundle handles upload and delete inside Doctrine event listeners. The direct equivalents are model events or an observer class:
saved → verify the file landed on the disk, then write the derivative/thumbnail record.deleting → dispatch a DeleteMediaFiles job or delete synchronously, and — critically — do it after the DB transaction commits, not during the listener.GenerateThumbnails job replaces whatever the bundle does inline. The bundle almost certainly generates thumbnails synchronously in the request; do not copy that. Push to a queue.Storage abstraction → Flysystem disk. The "configurable storage abstraction for local filesystem uploads" is the piece Laravel solves most thoroughly. Map the bundle's upload-path config to a named disk in config/filesystems.php (media, backed by local in dev and s3 in prod), and store disk-relative paths in the Media row, never absolute URLs. The bundle's URL-building helper becomes a url() accessor on the model that resolves through Storage::disk()->url(). Keep a disk column on Media if you are mid-migration and files live in two places during cutover.
Thumbnails as derived artefacts. The bundle ships thumbnail generation as a built-in. In Laravel, treat thumbnails as derived and never authoritative: store the source, generate derivatives into a deterministic path ({id}/{size}.jpg), and make the model's thumbUrl($size) resolve or lazily trigger regeneration. This is the single biggest structural improvement over the bundle — it makes rebuilding derivatives after a resize-config change a job, not a schema change.
Twig helper → Blade component. The bundle's Twig helpers (render an image, render a download link) port to a <x-media :media="$m" size="thumb" /> Blade component or a media_url() helper. Resist porting the helper API one-to-one; you only need two or three shapes in practice (image tag, anchor, raw URL), and Blade components make the "which disk / which size" arguments explicit.
Controller pattern for uploads. The bundle's upload action accepts a file plus an owner reference. In Laravel, that becomes a FormRequest with file|mimes:...|max:... rules (mirroring the bundle's allowed file types config), a controller that stores via $request->file('upload')->store('media', 'media'), and returns a MediaResource. Do the validation before the store call — the bundle's era of Symfony apps was inconsistent about this and it is the number one thing not to copy.
Migration/ETL workflow (the one legitimate "integration"). If you are moving off Kitpages, the sequence is:
File table and the on-disk directory convention from the bundle's code — this tells you exactly where bytes live and how filenames are stored.media migration in Laravel with a superset schema (add disk, path, size, mime, mediable_*, legacy_id).legacy_id.Keep the bundle running in the legacy service only, behind the redirect, never in the new app's request path.
Composer will not resolve — stop trying. The bundle targets Symfony 2.x and old Doctrine. Modern Laravel shares symfony/* components transitively, so even a --no-scripts require risks version conflicts. If you must inspect it in a runnable state, use a throwaway Symfony 2.8 container, not your Laravel vendor/.
Orphaned files are the bundle's classic failure mode, and they will be yours too if you port the listener pattern naively. A listener that writes the file during prePersist/preUpdate leaves the binary behind if the transaction rolls back. In Laravel: store first (idempotent path), persist the row, and if the persist fails, clean up via a queued DeleteOrphan job or a scheduled sweeper that reconciles disk against DB. Never let the write and the DB commit live in the same untracked unit of work.
Two UploadedFile classes. Symfony's UploadedFile and Illuminate\Http\UploadedFile are unrelated. Any adapter code bridging the bundle's controller into Laravel has to translate between them, and the translation loses pieces (mime detection, test fakes). This is a strong reason to reimplement rather than bridge.
Sanitize filenames and never trust the client-supplied extension. The storage helper in a Symfony 2-era bundle predates a lot of the hardening we now take for granted. In your port: generate a random storage name, derive the extension server-side from a validated mime, keep the original name in a separate column purely for display, and store outside the webroot or on a private disk with signed URLs for anything non-public.
Public disk exposure and executable uploads. If the bundle stores into web/uploads/ (typical for its vintage), porting that path as-is means PHP/.phtml/.svg can end up executable. Use a private disk + Storage::temporaryUrl(), or a Route::get that streams through a controller with an auth policy. Also reject SVG unless you have a sanitizer.
Thumbnail staleness. The bundle ties derivative paths to the owner; if you replicate that, changing thumbnail dimensions later won't regenerate old files. Make derivative paths a function of (media_id, preset), not the owner, so regeneration is a single job dispatch.
Config drift between YAML and config/filesystems.php. The bundle's upload path and allowed file types have no analog as a single config surface in Laravel — allowed types belong in FormRequest rules (mimes:), not in a filesystems disk. Keep them in one place (config/media.php referenced by the request class) or you will end up with the validation list and the thumbnailer disagreeing about what is an image.
Debugging: go straight to the byte layer. When something is wrong with the bundle (or your port), the answer is almost always on disk, not in the code. find uploads/ -newer marker and compare against the File rows. For a migration, checksum the binaries in both stores before you trust counts.
Extension points you should not lean on. There is no CVE pipeline, no dependent ecosystem, and no active maintenance here. Do not extend the bundle, do not fork it into production, and do not wrap it behind an internal interface "in case." Treat it as frozen. If any legacy Kitpages surface still depends on it, contain it, schedule an exit, and put the file layer behind your own thin MediaRepository interface so it can be removed without touching product code.
Twig helpers will not port. Do not translate the Twig extension class. Rewrite the two or three render shapes you actually use as Blade components against your Media model — the Twig version carries CMS-specific assumptions (owner types, page context) that will drag the CMS model into your Laravel views.
Testing: never test the bundle. Test your own Media model, your upload endpoint's validation, and your thumbnail job. The bundle's 8-star/0-dependent status means its behaviour is undocumented in practice — you cannot use it as an oracle.
How can I help you explore Laravel packages today?