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

arxy/files

Arxy Files simplifies file management in PHP apps with FlySystem: store files anywhere, persist metadata in the database, and deduplicate uploads using MD5 checksums. Automatic upload/delete hooks and flexible naming strategies keep entities in sync.

View on GitHub
Deep Wiki
Context7

Getting Started

The bundle splits responsibilities into four collaborators you will touch constantly: a Manager (upload/read/delete orchestration), a Storage (Flysystem adapter wrapper), a NamingStrategy (hash → path), and a Repository (metadata persistence). Learn those four and the rest is configuration.

1. Install and register

composer require arxy/files

Register Arxy\FilesBundle\ArxyFilesBundle in config/bundles.php. You need league/flysystem-bundle (or hand-wired Flysystem services) plus doctrine/orm.

2. Create the entity. The bundle ships an abstract Arxy\FilesBundle\Entity\File; you add the id mapping yourself:

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

/** @ORM\Entity() @ORM\Table() */
class File extends \Arxy\FilesBundle\Entity\File
{
    /** @ORM\Id @ORM\Column(type="integer") @ORM\GeneratedValue */
    protected $id;

    public function getId(): ?int { return $this->id; }
    public function setId(?int $id): void { $this->id = $id; }
}

3. Create the repository — one trait, nothing else:

class FileRepository extends EntityRepository implements \Arxy\FilesBundle\Repository
{
    use \Arxy\FilesBundle\Repository\ORM;
}

4. Configure a manager (the arxy_files tree is the fastest path; the manual-services approach in the README is for when you need several managers or non-standard wiring):

# config/packages/arxy_files.yaml
flysystem:
    storages:
        uploads:
            adapter: 'local'
            options: { directory: '%kernel.project_dir%/public/uploads' }

arxy_files:
    managers:
        public:
            driver: orm
            class: 'App\Entity\File'
            storage: 'uploads'
            naming_strategy: 'Arxy\FilesBundle\NamingStrategy\SplitHashStrategy'
            repository: 'App\Repository\FileRepository'

5. First upload. The critical mental model: upload() only builds the entity and computes the target path. Bytes hit the filesystem in a Doctrine postPersist hook.

$file = $manager->upload($request->files->get('avatar')); // UploadedFile is an SplFileInfo
$user->setAvatar($file);
$em->persist($user);
$em->flush(); // ← now the file is actually written

Where to look first when something is off: Arxy\FilesBundle\Manager (the whole lifecycle lives here), Arxy\FilesBundle\Model\File (the contract your entity inherits), Storage\FlysystemStorage, and EventListener\DoctrineORMListener. Read those four classes before debugging any config.

First real use case to build: a user avatar. It exercises upload, dedup, naming, serving, and deletion in one flow, and it will surface the two-phase-write gotcha immediately.

Implementation Patterns

Form-driven upload. FileType bridges Symfony's file input to the manager, so the form field's data is already a File entity:

$builder->add('image', \Arxy\FilesBundle\Form\Type\FileType::class, [
    'required' => false,
    'constraints' => [/* constraints on the resulting File entity */],
    'input_options' => [
        'attr' => ['accept' => 'image/*'],
        'constraints' => [/* Symfony File constraints, run on the raw upload */],
    ],
]);

Keep the two constraint buckets separate: input_options.constraints validates the raw UploadedFile (size, mime); constraints validates the resulting entity. Validating mime only on the entity is too late — the bytes have already been hashed and staged.

Embeddable files on domain entities. When the file is really a column of News, not a first-class record, embed it:

class News
{
    /** @ORM\Embedded(class=EmbeddableFile::class) */
    private ?EmbeddableFile $image = null;

    public function getImage(): ?File { return $this->image; }
    public function setImage(?File $file): void { $this->image = $file; }
}

An embeddable has no id, which rules out IdToPathStrategy. Pair embeddables with SplitHashStrategy/UuidV5Strategy or PersistentPathStrategy so the path never depends on a database identity.

Serving files — three tiers, pick deliberately.

Need Approach
Authenticated / gated download DownloadUtility::createResponse($file) in a controller
Custom filename, inline vs attachment, cache expiry wrap in DownloadableFile: new DownloadableFile($file, 'invoice.pdf', false, new \DateTimeImmutable('+1 year'))
CDN / public bucket URL in an API payload PathResolver::getPath($file) inside a serializer normalizer

The normalizer pattern is the one to copy for JSON APIs:

public function normalize($object, $format = null, array $context = [])
{
    $data = $this->objectNormalizer->normalize($object, $format, $context);
    $data['url'] = $this->pathResolver->getPath($object); // join with CDN base if needed
    return $data;
}

For image variants, decorate the same normalizer with LiipImagine\FileFilterPathResolver and loop FilterConfiguration::all() to emit a formats map (squared_thumbnail, large, …). This keeps the client from constructing URLs and lets you swap CDN/filter sets in one place.

Multiple managers for public vs private storage. Define two entries in arxy_files.managers (public, private) with different Flysystem storages, classes, and naming strategies. Since ManagerInterface only aliases one of them, inject with a named parameter or define explicit service ids — do not rely on the generic interface once you have more than one manager.

Naming strategy selection.

Strategy Path shape Use when
SplitHashStrategy 098f6bcd/4621d373/.../098f... Default; no DB dependency, sharded dirs
DateStrategy 2021/05/17/<md5> You want date-based archival/retention
UuidV5Strategy UUIDv5(namespace + md5) Opaque paths, stable across deployments
PersistentPathStrategy whatever is stored on the entity Random UUIDv4 paths; requires PathAware
AppendExtensionStrategy decorator, adds .jpg/.pdf Almost always
DirectoryPrefixStrategy / NullDirectoryStrategy decorators Multi-tenant prefixes, flat layouts

Compose them: AppendExtensionStrategy(SplitHashStrategy) is the sane default, because a hash-only path gives browsers and CDNs no extension to reason about.

Custom extension points. Implement Arxy\FilesBundle\NamingStrategy for tenant-aware paths (acme/2024/05/<md5>), or implement Storage if you need signed S3 URLs / per-request credentials without going through Flysystem. Both are small interfaces — the manager takes them by constructor, so tests can substitute an in-memory Flysystem adapter:

flysystem:
    storages:
        test: { adapter: 'memory' }

Gotchas and Tips

  1. Nothing is on disk until flush(). upload() returns a fully-populated entity whose path exists nowhere yet. If the flush fails, you have no file and no row — but you also have no cleanup problem. Conversely, if the DB commit succeeds and the filesystem write in postPersist throws, you get a row pointing at a missing file. Wrap flush in try/catch and treat filesystem failures as fatal, logged and surfaced.

  2. Only postPersist and preRemove are registered in the documented setup. That means replacing a file on an already-persisted entity may never be written, because no postUpdate fires. Verify against DoctrineORMListener in your installed version; if it holds, register postUpdate yourself or force a delete+persist cycle when swapping files.

  3. Dedup plus preRemove is the sharpest edge. Identical content maps to the same md5/namespace/path, and dedup "reuses" the file. Two owners can therefore reference the same underlying object. Deleting one owner triggers file removal on preRemove — check whether that unlinks content another row still points at. If you rely on dedup in production, add reference counting (or a soft-delete flag on File) rather than trusting the hook.

  4. IdToPathStrategy cannot be used with EmbeddableFile. No id, no path. Also avoid it for prePersist-time path computation if your storage layer needs the path before the id is assigned.

  5. The extension comes from the client. AppendExtensionStrategy appends the original filename's extension — sanitize/whitelist it, or the path inherits whatever the browser sent.

  6. read() loads the whole file into memory. For anything user-supplied, use readStream() and hand the handle to a StreamedResponse. Same for DownloadUtility if you enable inline serving of large media.

  7. PathResolver returns a storage path, not an absolute URL. With a local adapter under public/uploads you still need to prepend the base URL; with a CDN you need the domain. Centralize that in the normalizer, not in templates.

  8. Path resolution bypasses authorization. Any endpoint that emits PathResolver URLs is effectively public if the storage is public. Private documents must be served exclusively through a controller using DownloadUtility with an access check before it.

  9. Set cache headers explicitly. DownloadableFile accepts a cache-expiry DateTimeImmutable — without it you get no useful Cache-Control/ETag, and every download re-streams from storage.

  10. Don't mix the arxy_files config tree with the manual service definitions. Pick one. The manual block exists for custom storages/strategies; duplicating services leads to two managers with subtly different naming strategies and files landing in two directory trees.

  11. Debugging a missing file: check (a) did flush() run, (b) is the naming strategy the one you think (dump $file->getPathname()), (c) is the storage name in arxy_files.managers spelled exactly like the Flysystem storage key, (d) is the Doctrine listener actually registered for the event you expect (bin/console debug:event-dispatcher).

  12. Maturity risk. 18 stars, 0 dependents, essentially a single maintainer, and no upgrade guide. Pin the version in composer.json, read Manager and the listener source before upgrading, and keep your integration behind one service of your own so swapping to a hand-rolled Flysystem + Doctrine solution stays a contained change. The package description mentions ODM documents, but the README states only Doctrine ORM is supported — do not plan on MongoDB ODM.

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.
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels
wazza/laravel-db-encryption
hokny/laravel-generator
vonsogt/backup
jengo/queues
jeremykenedy/laravel-observability
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names