2lenet/entity-file-bundle
EntityFileBundle lets Symfony entities attach and manage files through configurable FlySystem storage adapters, with simple save, retrieve, access, delete, rename, and move operations via a per-configuration manager.
Architecture fit. Despite the framing in the brief, this is a Symfony bundle, not a Laravel package. It targets Doctrine ORM entities, Symfony bundle configuration (lle_entity_file.yaml), Symfony routing, and the Symfony security layer; storage is delegated to FlySystem via the FlySystem Symfony Bundle. The core abstraction is sound: one configuration maps exactly one entity class to exactly one storage adapter plus policy settings (role, disposition, entity_file_class), with a manager resolved through EntityFileLoader::get($configName). That gives clean multi-bucket semantics (seller logos vs. product photos) without custom code. Extensibility is reasonable: a custom EntityFileInterface implementation plus EntityFileTrait allows domain metadata, and paths are caller-supplied, enabling dynamic partitioning (Y-m/id.xml).
Integration feasibility. Low ceremony for an existing Symfony/Doctrine app: one Composer require, bundle registration, one YAML block, one entity class, one schema migration, and an optional route import. The bundle does not ship a first-class migration, so the entity_file table (and the indexes added in 1.2.4) must be generated and owned by the consuming application. UI value is gated on 2LE's Crudit bundle (EntityFileBrick); without Crudit, you get service-layer file handling and two controller routes but build your own front end. For Laravel/Eloquent stacks this is effectively a non-starter — no service provider, no config/filesystems.php support, no Eloquent events, and no Doctrine unless you have deliberately adopted doctrine/orm in Laravel.
Technical risk.
role for the whole configuration. Any user holding that role can read any file under that config by iterating IDs or guessing paths; PUBLIC_ACCESS disables gating entirely. Fine-grained ownership requires a custom Symfony voter — which the README acknowledges but does not demonstrate.readbypath route accepts an arbitrary path query parameter; safety depends on FlySystem's path normalization and the bundle's own guarding. Needs explicit traversal testing before exposure.save() writes the file and returns an entity the caller must persist/flush; delete() removes both entity and file; move() requires a manual flush. Failure between storage write and DB commit produces orphans or dangling rows, and there is no transactional or compensating mechanism.readStream(), no audit trail.Key questions.
composer.json constraints for Symfony, PHP, Doctrine DBAL, FlySystem, and Crudit? FlySystem v1 → v3 is a breaking API shift.entity_file table, and are schema changes treated as semver-major?path parameter validated against traversal, and how are storage exceptions translated to HTTP responses?Stack fit. Adopt only where the stack is Symfony 6.4/7/8 with Doctrine ORM and FlySystemBundle already (or acceptably) in place. If the platform is Laravel/Eloquent, stop here and evaluate spatie/laravel-medialibrary; the cost of introducing Doctrine purely to use this bundle is not justified by its scope. If the platform is Symfony but uses a bespoke upload layer, the bundle replaces that layer rather than complementing it — plan for removal, not coexistence.
Migration path. Run a bounded spike on one non-critical entity (e.g., seller logos) before committing:
flysystem.yaml) pointing at a non-production directory; do not rely on the default data/<config> path for anything persistent.entity_file, including the 1.2.4 indexes; hand-tune column types and add FKs matching house conventions.EntityFileTrait and add domain fields (uploader, checksum, original name, retention) at creation time — retrofitting metadata later is a data migration.role; test PUBLIC_ACCESS is unreachable for private configs.readStream().Compatibility. Pin exact versions and verify against the target Symfony minor before merging. Crudit must move in lockstep if the brick is used — a Crudit upgrade can break the admin UI independently of this bundle. FlySystem adapter configuration lives in the FlySystem bundle, so storage credentials and visibility rules are owned outside this package; document that boundary explicitly.
Sequencing. Spike and security design first (weeks 1–2), schema and backfill second (week 3), route/UI cutover third (week 4), legacy decommission last with a rollback window. Gate each stage on a contract test suite you write yourself — assume none is provided.
Maintenance. Treat this as a vendored dependency, not a community library. Pin to a minor version, mirror the repository internally, and budget roughly 10–15% of one engineer's time per year for Symfony major-version bumps and security patches. MIT licensing makes forking viable; decide the fork trigger in advance (e.g., no release within 90 days of a Symfony major). Watch two upgrade surfaces: Symfony framework majors and FlySystem majors.
Support. No SLA, no commercial backing, no dependents to consult. Issue response is best-effort via GitHub. Escalation is the agency relationship, not a support contract. Internal runbooks must cover storage adapter failures, permission misconfiguration, and orphan-file cleanup, because none of that is documented.
Scaling. Metadata grows linearly with files in the application database — acceptable, but keep the table lean and rely on the added indexes for lookups by config and entity. The real ceiling is delivery: file bytes are streamed through PHP via the bundle's controller, consuming application workers and bandwidth with no CDN, cache headers, or signed-URL offload. At meaningful volume, front the storage with Nginx X-Accel-Redirect or presigned object-storage URLs and bypass the controller. Local-disk storage is unsuitable for multi-node or containerized deployments; object storage must be chosen up front if horizontal scaling is on the roadmap. No queue integration means large uploads sit inside the request cycle.
Failure modes. (1) Non-atomic write-then-flush — storage succeeds, DB fails, orphaned files accumulate; add a reconciliation job. (2) delete() removes the physical file, so a failed flush leaves a row pointing at nothing — decide ordering deliberately and log both sides. (3) move() silently diverges if the caller forgets flush(). (4) Over-broad role values or an accidental PUBLIC_ACCESS expose private documents; enforce via config linting in CI. (5) read() on a large file loads it fully into memory — prefer readStream(). (6) Storage adapter misconfiguration surfaces as FlySystem exceptions rather than user-facing errors; wrap at the boundary.
Ramp-up. A Symfony developer can ship a basic attachment flow in a day from the README; the undocumented parts — schema ownership, the security model, failure semantics, and Crudit integration — take closer to a week. Write an internal one-pager covering storage config location, voter behavior, and the persist/flush contract, and require tests on upload, delete, and unauthorized access as part of onboarding. Do not assume the README's brevity reflects simplicity; it reflects missing detail.
How can I help you explore Laravel packages today?