Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Files Laravel Package

fsi/files

FSI Files is a Laravel/PHP package for managing file uploads and storage. It provides a clean way to organize uploaded files, handle common file operations, and integrate file management into your application with minimal setup.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the package and, if Flex doesn't auto-register it, add the bundle to config/bundles.php:

composer require fsi/files
// config/bundles.php
FSi\Bundle\FilesBundle\FilesBundle::class => ['all' => true],

Note on the platform: this ships as a Symfony bundle, not a Laravel package — there is no service provider or facade. If you're coming from Laravel, the only piece you can realistically lift into a Laravel app is the framework-agnostic core (FSi\Component\Files\*); the form type, upload controller, Doctrine mapping and Twig helpers are all Symfony-specific and won't follow you.

The package is split into two layers you need to keep straight:

  • FSi\Component\Files\* — the framework-agnostic core: FileInterface, FileType, FileTypeManager, FileManager (storage abstraction), variant/filter logic. This is where the actual behaviour lives.
  • FSi\Bundle\FilesBundle\* — Symfony glue: DI configuration, FileType form type, an AJAX upload controller, Doctrine entity mapping, Twig helpers.

First three things to look at, in order:

  1. FSi\Component\Files\Model\AbstractFile and FileInterface — this is what your entity will extend/implement.
  2. FSi\Bundle\FilesBundle\DependencyInjection\Configuration — the canonical source of truth for config keys (docs/README lag behind releases).
  3. The bundle's Tests/Functional fixtures — they contain a working fsi_files.yaml and a mapped entity you can copy verbatim.

Minimum viable config (config/packages/fsi_files.yaml) — the shape is a set of named filesystems plus named file types with variants:

fsi_files:
    default_filesystem: uploads
    filesystems:
        uploads:
            adapter: flysystem          # or gaufrette, depending on release line
            config:
                adapter: local
                options:
                    directory: '%kernel.project_dir%/public/uploads'
    file_types:
        image:
            variants:
                thumb:
                    filter: thumbnail
                    options: { size: [300, 300] }

Then map an entity that extends AbstractFile (or the bundle's ready-made FSi\Bundle\FilesBundle\Entity\File if you don't need extra columns) and run a migration.

First use case — attach an image to a product and render a thumbnail:

use FSi\Component\Files\Upload\FileFactory;   // exact namespace varies by release

$file = $this->fileFactory->create($uploadedFile, 'uploads', 'image');
$product->setImage($file);
$em->persist($product);
$em->flush();
<img src="{{ product.image|fsi_file_variant_uri('thumb', 'uploads') }}">

If that renders, your filesystem, file type, and variant filter are all wired correctly — which is the majority of the package's moving parts in one shot.

On 3.0.3 specifically: it's a dependency-widening patch — doctrine/dbal ^4.0 is now allowed and the package is prepared for doctrine/orm ^4.0. No config keys, service IDs or public API changed, so an existing 3.0.x install needs no code edits. What can change is what Composer resolves for you: a plain composer update fsi/files may now pull DBAL 4 (and ORM 4, if your other packages allow it) into an app that was previously pinned to DBAL 3. If you're not ready for that, constrain Doctrine in your root composer.json before updating, then run bin/console doctrine:schema:validate afterwards.

Implementation Patterns

Entity attachment (the 90% case). Model the relation as ManyToOne to your File subclass with cascade: ['persist'] and orphanRemoval: true for single-file fields, and OneToMany/ManyToMany for galleries. Keep the File column nullable and drive the setter from the form — the package assumes you persist the File alongside the owning entity, not separately.

Mapping style must be attributes. As of 3.0.3 the package is preparing for doctrine/orm ^4.0, which drops the annotation driver and the Doctrine\ORM\Mapping annotation classes entirely. Write your File subclass with PHP attributes (#[ORM\Entity], #[ORM\Table], #[ORM\ManyToOne]) rather than docblock annotations, even if you're still on ORM 3 — otherwise the DBAL/ORM 4 bump lands as a hard failure rather than a routine upgrade.

Form integration. Use FSi\Bundle\FilesBundle\Form\Type\FileType. In single mode it renders a hidden file_id input backed by the bundle's upload endpoint; in multiple: true mode it renders a collection. The controller returns JSON with the persisted file id(s), which the form then posts back as ids — so your form must be submitted after the upload completes, and your entity setter receives managed File instances, not UploadedFiles. Don't try to reuse the upload endpoint outside the form without matching its JSON contract.

Define one FileType per role, not per entity. image, document, avatar are roles. Variants attach to the role, so a thumb defined on image is available everywhere an image file is used. This is the single biggest lever for consistency across a large app.

Generate variants eagerly, serve lazily. Variants are computed on first access by default, which means the first request after every upload pays the (potentially multi-second) resize cost. Wire a post-flush listener or a Messenger handler that calls into the variant generator for every configured variant of a newly uploaded file. Treat the lazy path as a fallback, not the plan.

Storage abstraction is the point of the package. Domain code should never touch UploadedFile::move() or file_put_contents. Everything goes through the named filesystem, so swapping local → S3 is a config change plus an adapter package. If you find yourself writing a path by hand, you've left the abstraction and the swap will break.

Twig helpers for output. fsi_file_uri(file, filesystem) and fsi_file_variant_uri(file, variant, filesystem) resolve the storage path to a URL. Use them rather than concatenating public/uploads/..., otherwise a CDN/S3 move silently breaks every template.

Cleanup as a first-class concern. orphanRemoval deletes the row, not the bytes. Pair it with a Doctrine postRemove subscriber (or the bundle's cleanup command, if your release line ships one) that calls FileManager::remove(). Add a nightly reconciliation command that walks storage and deletes files with no DB row — this is the only thing that recovers from a failed transaction after upload. If you wrote that subscriber against DBAL 3 APIs (Statement::fetch*, Connection::executeQuery with string types), check it against DBAL 4's Result API when you take the upgrade.

Custom DBAL types belong in root config. If you added a DBAL type for a File column (UUID, JSON array of ids), DBAL 4 removes the old static Type::addType() registration path. Declare types under doctrine.dbal.types in config/packages/doctrine.yaml so they survive the bump.

Testing. Configure a local filesystem pointed at %kernel.cache_dir%/uploads in config/packages/test/fsi_files.yaml, and assert on FileManager state rather than the real disk. For unit tests of variant logic, construct FileType objects directly from the core namespace — no kernel needed. Run the suite once against doctrine/dbal ^3 and once against ^4 if you're claiming compatibility; the widening in 3.0.3 makes both installable.

Multi-tenant / per-tenant storage. Register one named filesystem per bucket/tenant and select it at runtime via the FileManager registry. Don't try to encode the tenant into the filename; the filesystem name is the isolation boundary and it's what the rest of the package keys off.

Gotchas and Tips

3.0.3 is a patch that can move a major dependency. The version number says patch, but the constraint change means composer update can swap DBAL 3 → 4 under you. If you have other bundles with doctrine/dbal ^3 constraints, Composer will keep you on 3 and nothing changes; if you're already on ORM 3 + DBAL 4-compatible packages, you may silently jump. Read composer.lock after updating, not just the package's version number.

ORM 4 is "prepared for", not "supported". The release allows doctrine/orm ^4.0 to resolve; ORM 4 itself is still the moving target. Pin doctrine/orm ^3 in your root until DoctrineBundle, migrations, fixtures and your own mapping are on attributes-only. composer why-not doctrine/orm 4.0 is the fastest way to see what's holding you back.

Version pinning is still not optional. Recent release dates combined with zero dependents means the API surface is still moving: FileFactory, the FileManager interface, and the DI service IDs have changed across lines. Pin an exact minor version and read UPGRADE/CHANGELOG for that tag before touching anything. Treat the README as a starting point, not documentation.

AbstractFile + Doctrine inheritance. If you extend AbstractFile, you need a mapped superclass or single-table inheritance configuration, and the bundle's own mapping must not collide with yours. The fastest way to lose an afternoon is two mappings claiming the same table. Copy the functional-test entity, then delete what you don't need — and re-verify after any Doctrine major bump, since mapping-driver differences surface here first.

Variant N+1. Calling $file->getVariant('thumb') inside a loop over 50 products issues 50 queries against the variants relation (or 50 storage calls if not persisted). Always eager-load variants with a JOIN/addSelect on the file association, or fetch all variants at once via getVariants().

Filename collisions and client names. The package generates its own stored name; never trust UploadedFile::getClientOriginalName() for anything but a display label. Also note that two File rows can point to the same storage path if you clone entities carelessly — clone the file, not the reference, if you need two independent records.

Adapter/service naming. The DI service IDs follow fsi_files.file_manager.<filesystem_name>. When debugging "file not found," run bin/console debug:container fsi_files first — nine times out of ten the filesystem name in the call doesn't match the YAML key, and the package silently falls back to default_filesystem.

Flysystem major version mismatch. The bundle has supported both Flysystem v1 and v3 across its lifetime, and the adapter service definitions differ (league/flysystem-bundle vs. the older oneup/flysystem-bundle). If you get "service not found" for the adapter, check which Flysystem bundle you actually installed before changing the YAML.

Public URL vs. storage path. The local adapter returns paths relative to its configured root. If that root isn't under public/, the Twig helper will emit a URL that 404s. Either point the local adapter at public/uploads or add a route that streams through FileManager — don't mix the two approaches for the same filesystem.

Validation is yours. The package stores what it's given. File/Image Symfony constraints, maxSize, and an extension/MIME allowlist belong on your form or entity, not in the bundle config. This is the most commonly skipped step and the one with the worst consequences.

Removal ordering. Delete the DB row and flush before removing bytes, and make the byte removal idempotent. The reverse order leaves you with rows pointing at nothing, which surfaces as broken images rather than an exception — much harder to notice.

Extension points worth knowing: implement FSi\Component\Files\FileManager for a storage backend the package doesn't ship; implement the variant filter interface for custom image processing (watermarks, format conversion); and hook a Symfony event subscriber on the upload flow if you need to strip EXIF, virus-scan, or push to a CDN. These three cover nearly every "we need to do X to every upload" request without forking.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect