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

Gaufrette Laravel Package

knplabs/gaufrette

Gaufrette is a PHP filesystem abstraction layer that lets your app read and write files without knowing where they're stored, so you can switch between local storage, Amazon S3, FTP, and other adapters without changing your code.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the core plus exactly one adapter metapackage. knplabs/gaufrette ships the core (Filesystem, File, Stream, InMemory, Local, Zip vary by version) but every maintained backend now lives in its own metapackage that pulls its own third-party dependencies:

composer require knplabs/gaufrette gaufrette/local-adapter
# later, when you outgrow the disk:
composer require gaufrette/aws-s3-adapter

Where to look first

  • docs/adapters/<name>.md — one page per adapter, with the constructor signature and a runnable snippet. Read the page for your storage before reading the code.
  • The adapter table in README.md — tells you which adapters are deprecated (e.g. AmazonS3/AclAwareAmazonS3) and who maintains each one.
  • src/Gaufrette/Filesystem.php — the whole public API is ~15 methods; skim it once and you know the package.
  • spec/ — the phpspec suite is the real behaviour spec for write, rename, error types.

Minimal wiring in Laravel. There is no official Laravel bundle (Symfony uses KnpGaufretteBundle), so you bind it yourself. Keep the config dumb and the construction in one class:

// app/Storage/GaufretteManager.php
namespace App\Storage;

use Gaufrette\Adapter;
use Gaufrette\Adapter\AwsS3;
use Gaufrette\Adapter\Local;
use Gaufrette\Filesystem;

class GaufretteManager
{
    /** @var array<string, Filesystem> */
    private array $disks = [];

    public function __construct(private array $config) {}

    public function disk(?string $name = null): Filesystem
    {
        $name ??= $this->config['default'];

        return $this->disks[$name] ??= new Filesystem(
            $this->makeAdapter($this->config['disks'][$name])
        );
    }

    private function makeAdapter(array $c): Adapter
    {
        return match ($c['adapter']) {
            'local'  => new Local($c['directory'], true, 0750),
            'aws_s3' => new AwsS3($c['client'], $c['bucket'], $c['options'] ?? []),
            default  => throw new \InvalidArgumentException("Unknown adapter {$c['adapter']}"),
        };
    }
}
// app/Providers/GaufretteServiceProvider.php  (register())
$this->app->singleton(GaufretteManager::class, fn ($app) =>
    new GaufretteManager($app['config']['gaufrette'])
);

First use case — an avatar uploader. One write, one read, one delete, and the key stored in your own table:

$fs = app(GaufretteManager::class)->disk('media');

$key = sprintf('avatars/%d/%s.webp', $user->id, Str::uuid());

try {
    $fs->write($key, $request->file('avatar')->getContent(), true); // true = overwrite
} catch (\Gaufrette\Exception\FileAlreadyExists $e) {
    // only reachable when overwrite is false
}

$user->update(['avatar_key' => $key]);

// later
$bytes = $fs->read($user->avatar_key);
$fs->delete($user->avatar_key);

That is the entire day-to-day surface: write / read / has / delete / rename / keys / size / mtime / mimeType / get / createStream. Everything else is adapters.

Implementation Patterns

Config-driven adapter swapping. The whole point of Gaufrette is that avatars/12/uuid.webp means the same thing on a local disk and in a bucket. Structure your config so the disk name is the only thing that changes:

// config/gaufrette.php
return [
    'default' => env('GAUFRETTE_DISK', 'media'),
    'disks' => [
        'media' => [
            'adapter'   => env('GAUFRETTE_ADAPTER', 'local'),
            'directory' => storage_path('app/media'),
            'bucket'    => env('AWS_BUCKET'),
            'options'   => ['directory' => 'media'], // S3 prefix
        ],
    ],
];

Never let a controller or model name Local or AwsS3. Inject GaufretteManager (or a thin MediaStorage service that wraps one disk) and keep the adapter namespace out of your domain code — that is what makes the migration to S3 a config change plus a copy job.

A dedicated storage service, not the Filesystem in controllers. Wrap it so you own the key convention, the overwrite policy, and the exception translation:

class MediaStorage
{
    public function __construct(private Filesystem $fs) {}

    public function putUploadedFile(UploadedFile $file, string $prefix): string
    {
        $key = $prefix.'/'.Str::uuid().'.'.$file->extension();

        $this->fs->write($key, $file->getContent(), false);

        return $key;
    }

    public function putStream($resource, string $key): void
    {
        $stream = $this->fs->createStream($key);
        $stream->open();
        while (! feof($resource)) {
            $stream->write(fread($resource, 8192));
        }
        $stream->close();
    }
}

For genuinely large files use createStream() (or register Gaufrette\StreamWrapper and use fopen('gaufrette://key') with stream_copy_to_stream) rather than read()/write(), which materialise the whole payload in a string.

Metadata, size, checksum. These are optional adapter capabilities — feature-detect instead of assuming:

use Gaufrette\Adapter\ChecksumCalculator;
use Gaufrette\Adapter\MetadataSupporter;

$adapter = $fs->getAdapter();

if ($adapter instanceof ChecksumCalculator) {
    $hash = $fs->checksum($key);          // may be null/false on unsupported backends
}

if ($adapter instanceof MetadataSupporter) {
    $adapter->setMetadata($key, ['Cache-Control' => 'public, max-age=31536000']);
}

Listing with intent. $fs->keys() returns everything; on S3 that is a paginated round trip per 1000 objects. For anything user-facing, list by prefix using the adapter's native client, or keep a media table and query that. Reserve keys() for maintenance commands and tests.

Async and lifecycle. Uploads to remote adapters belong in a queued job (putStream inside handle()), with the DB row written as status = pending first and flipped on success. There is no transaction across storage + database, so always write the DB row before the object and treat orphans as a cleanup job's problem.

Testing. InMemory is the perfect fake — same interface, no filesystem, no network:

protected function setUp(): void
{
    parent::setUp();

    $fs = new Filesystem(new \Gaufrette\Adapter\InMemory());
    $this->app->instance(MediaStorage::class, new MediaStorage($fs));
}

public function test_avatar_is_stored_under_user_prefix(): void
{
    // ... assert $this->app->make(MediaStorage::class) has the key
}

For integration coverage of Local, point the adapter at a sys_get_temp_dir() folder created in setUp and removed in tearDown.

Interop with Laravel's Flysystem. If you already have a configured Storage disk and want to expose it through Gaufrette's API (or migrate incrementally), wrap the underlying Flysystem instance in Gaufrette\Adapter\Flysystem. Pin the versions: that adapter tracks a specific Flysystem major, and Laravel 9+ ships Flysystem 3 as FilesystemOperator, so verify the constructor argument before committing to the bridge.

Gotchas and Tips

  • write() throws on existing keys. The third argument defaults to false and you get Gaufrette\Exception\FileAlreadyExists, not a silent overwrite. Re-uploads of the same key will blow up in production; either always pass true or generate a unique key per version.
  • Keys are relative and must not start with /. Local joins the key onto its root directory, so /avatars/a.png produces a path outside (or a mangled path inside) the root. Normalise with ltrim($key, '/') at the boundary. Trailing slashes and .. are your responsibility, not the abstraction's.
  • Directory semantics are not portable. Local and Zip can have real directories; S3 has only keys containing /. Deleting a "folder" works only on Local, and only via $filesystem->getAdapter()->delete($dirKey) — Filesystem deliberately has no directory API, and you cannot delete the adapter's root.
  • SafeLocal base64-encodes keys on disk. Keys that look odd in the storage layer are expected; verify what keys() returns for your version rather than assuming decoded values, and never mix SafeLocal and Local on the same directory.
  • mtime(), size(), checksum() can return false. They are adapter-dependent, not guaranteed. Guard with the instanceof checks above; don't put size() behind a int return type.
  • No URL generation in the abstraction. Presigned and CDN URLs are backend concerns. Keep the constructed S3Client in the container (or use Laravel's Storage::disk('s3')->temporaryUrl()) instead of reaching for adapter-specific methods that may not exist in your version.
  • rename() is copy + delete on several adapters. It is not atomic and can half-fail, leaving both copies. Treat a failed rename as recoverable, not exceptional.
  • Streams must be closed. createStream() on an adapter that doesn't implement the stream factory falls back to an in-memory buffer — meaning a 200 MB video silently becomes 200 MB of RAM. Check the adapter, and always close() in a finally.
  • InMemory is per-request. It's a test double, not a cache; a second HTTP request sees an empty filesystem.
  • AmazonS3 / AclAwareAmazonS3 are deprecated and were slated for removal in 1.0 — use AwsS3 (aws-sdk v2 and v3 both supported, but the client must be built with 'version' => 'latest' or a pinned API version, otherwise the SDK throws on construction). Use a dedicated IAM user with s3:PutObject, s3:GetObject, s3:DeleteObject on bucket/* and s3:ListBucket on the bucket only; create the bucket out-of-band and drop s3:CreateBucket.
  • Azure lazy-loads via the proxy factory, so a bad connection string fails on first read/write, not at boot — a classic "works in dev, 500s in prod" vector. Omit the container name deliberately if you want multi-container mode where the first key segment picks the container.
  • DoctrineDbal needs a real table (key, content, mtime, checksum) before the adapter is usable; the adapter will not create it for you.
  • Version pinning matters. The project has no 1.0 yet despite the 0.x line being stable in practice; pin ^0.x and read the CHANGELOG before minor bumps, especially around adapter splits and interface additions.
  • Debugging tip: when a call misbehaves, log get_class($filesystem->getAdapter()) and the exact key string — 90% of "file not found on S3 but it exists in the console" cases are a double slash, a leading slash, or an unexpected prefix option.
  • Extension points: for a proprietary backend, implement Gaufrette\Adapter (read, write, exists, mtime, keys, delete, rename, isDirectory) and add ChecksumCalculator / SizeCalculator / MetadataSupporter / MimeTypeProvider only for what you can genuinely support. For cross-cutting behaviour — logging, per-disk prefixing, read-only enforcement — write a decorator that implements Adapter and delegates, then hand it to new Filesystem($decorator). That composes far better than subclassing a concrete adapter.
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.
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
adachsoft/php-code-style