zenstruck/filesystem
Zenstruck Filesystem is a Symfony bundle for attaching files and images to Doctrine entities. It handles storage via Flysystem adapters, image transformations, metadata, and automatic orphan cleanup, keeping file management clean and testable.
Architecture fit. Zenstruck Filesystem implements a clean value-object pattern: File and Image wrap upload metadata and storage references, while Doctrine lifecycle listeners handle persist/update/remove semantics. For a Symfony + Doctrine codebase this is a strong fit — it removes file-handling boilerplate from entities and keeps domain models testable. For a Laravel codebase the fit is poor: the package is a Symfony bundle, registered through the Symfony kernel and DI container, and its persistence model assumes Doctrine ORM mappings and Doctrine event subscribers. Laravel's default ORM is Eloquent, which has no lifecycle integration with this bundle. The underlying abstractions (Flysystem-backed storage, orphan cleanup) are portable as patterns, not as a package.
Integration feasibility.
Storage facade is already Flysystem-backed, so the storage abstraction maps cleanly; the value-object and orphan-cleanup semantics would need a small in-house reimplementation using Eloquent casts, observers, and a scheduled cleanup command.Technical risk.
Key questions.
Stack fit. Treat this as a Symfony-only component. In a Laravel-first estate, do not attempt direct integration; the cost of standing up Doctrine + bundle bootstrapping exceeds the benefit. Instead, adopt the design: a StoredFile value object, an Eloquent cast for persistence, a Storage disk per file class, and a scheduled files:prune-orphans command. Laravel's native Flysystem integration makes the storage half nearly free.
Migration path.
File/Image lifecycle, orphan removal, and LiipImagine filter path against a real S3 bucket.File/Image value objects, an Eloquent cast, a FileStorage service over Storage::disk(), and a queued orphan-prune job. Target parity on: persist, replace, delete, and orphan sweep.Compatibility. Verify PHP version floor, Symfony 6.x/7.x support, Doctrine ORM 2.x/3.x, and Flysystem 3.x before committing. For the Laravel port, confirm the Storage disk config matches required backends (S3, GCS, local) and that image filtering is covered by Intervention Image or Glide rather than LiipImagine.
Sequencing. Storage abstraction first (lowest risk, highest reuse), then persistence, then deletion/orphan cleanup (highest risk — put it behind audit logging and a dry-run mode), then image filtering last.
Maintenance. Single-maintainer, 43-star package used for a business-critical concern (file persistence). Plan for the possibility of forking. Budget a quarterly dependency review to track Symfony/Doctrine/Flysystem compatibility. The MIT license is permissive but does not transfer maintenance obligation — treat the fork as a contingency, not a plan.
Support. No commercial support; GitHub issues only. Internal ownership must be assigned explicitly: one engineer accountable for the file subsystem, with the runbook documented regardless of whether the Symfony bundle or a Laravel port is used.
Scaling. The storage layer scales with the underlying Flysystem adapter (S3/GCS are effectively unbounded). The real scaling pressure is metadata volume in the DB (one row per file, plus join columns on entities) and image filter cache growth, which needs a TTL and a purge job. Orphan sweeps should be incremental and indexed on created_at to avoid full-table scans.
Failure modes.
Ramp-up. For Symfony developers: low — a day to first productive use. For Laravel developers forced through the Doctrine compatibility path: high — Doctrine entity mapping, event lifecycle, and bundle config are all unfamiliar territory, and debugging is materially harder. This asymmetry is the strongest argument for the port-and-reuse strategy in a Laravel estate, and the strongest argument for direct adoption in a Symfony estate.
How can I help you explore Laravel packages today?