gaufrette/extras
Gaufrette Extras provides additional adapters and plugins for the Gaufrette filesystem abstraction library, helping PHP developers connect applications to cloud storage, archive formats, and other filesystem backends through a consistent API.
Install and inspect.
composer require gaufrette/extras
Two classes matter on day one:
Gaufrette\Extras\Resolvable\ResolvableFilesystem – a FilesystemInterface implementation that delegates every operation to whichever underlying filesystem owns the key.Gaufrette\Extras\Resolvable\Resolver\Resolver – the built-in router that maps a scheme://key path to a registered Gaufrette\Filesystem.First use case: one injected filesystem for many backends. Instead of injecting $localFilesystem into your upload service, $s3Filesystem into your CDN service, and so on, inject a single ResolvableFilesystem and address targets by scheme.
use Gaufrette\Filesystem;
use Gaufrette\Adapter\Local;
use Gaufrette\Adapter\AwsS3;
use Gaufrette\Extras\Resolvable\ResolvableFilesystem;
use Gaufrette\Extras\Resolvable\Resolver\Resolver;
$resolver = new Resolver([
'local' => new Filesystem(new Local('/var/app/storage')),
's3' => new Filesystem(new AwsS3($s3Client, 'bucket')),
]);
$filesystem = new ResolvableFilesystem($resolver);
$filesystem->write('local://cache/report.json', $json);
$filesystem->read('s3://assets/logo.png');
Read the source of Resolver::resolve() first — that is the entire contract you are programming against. Everything else is delegation.
Registry service. Wrap the resolver in your DI container and register named filesystems as tagged services. The scheme names then become a small, documented vocabulary (local://, s3://, uploads://) rather than a stringly-typed free-for-all.
// config/services.php
$container->set(ResolvableFilesystem::class, function ($c) {
return new ResolvableFilesystem(new Resolver([
'local' => $c->get('fs.local'),
's3' => $c->get('fs.s3'),
'uploads' => $c->get('fs.uploads'),
], $c->get('fs.local'))); // optional default filesystem
});
Type-hint Gaufrette\FilesystemInterface (or ResolvableFilesystem where you need scheme awareness) in services that already work with Gaufrette. Existing code keeps working: unprefixed keys fall through to the default.
Lazy resolution for expensive adapters. Don't construct every filesystem eagerly if a request only touches one scheme. Either construct adapter objects lazily inside the resolver array, or implement Gaufrette\Extras\Resolvable\Resolvable yourself and instantiate on first hit.
Custom routing logic. The resolver interface (resolve($path) returning a Resolved with getFilesystem() and getKey()) is the extension point. Implement it for tenant routing, sharding, or feature-flagged cutovers:
final class TenantResolver implements Resolvable
{
public function __construct(private array $filesystems) {}
public function resolve(string $path): Resolved
{
[$scheme, $key] = explode('://', $path, 2) + [1 => $path];
// e.g. tenant1 -> filesystems['tenant1']
return new Resolved($this->filesystems[$scheme], $key);
}
}
Migration/fallback pattern. Compose a resolver that tries the new backend, catches "not found" from the underlying filesystem, and falls back to the legacy one. This lets you move buckets or tenants without changing call sites.
Stream wrapper integration. If your codebase is fopen-oriented, register the resolvable filesystem as a stream wrapper once at bootstrap and then use scheme://key natively in templates, file_get_contents(), and third-party libraries.
Testing. Unit-test the resolver in isolation (scheme → filesystem + key) with in-memory adapters, and integration-test the ResolvableFilesystem against a real local adapter. Because the public surface is FilesystemInterface, anything consuming it can be tested with a stub Filesystem.
FilesystemInterface has churned across Gaufrette releases). Run composer why gaufrette/extras and lock compatible versions; do not assume PHP 8.2+ compatibility without testing. If you hit fatal errors, a two-method custom Resolvable implementation is often cheaper than waiting for an update.foo://bar means scheme foo, key bar. Keys containing ://, Windows drive letters, or URLs stored as object keys will be misrouted. If you accept arbitrary user keys, encode the separator or use a custom resolver that only splits on the first :// and requires the scheme to be in an allow-list.s3://private/... becomes an authorization bypass in code that expected local://. Resolution is routing, not access control.UnresolvablePathException (from the resolver) is what unknown schemes throw. Catch it at your boundary and convert to a 400/404 domain exception; don't let it bubble as a 500.listKeys() / keys() operate on one resolved filesystem — the resolver has no cross-scheme notion of directories. Aggregate listings yourself if you need a virtual root.mkdir, has, rename straight through; renaming across schemes is not supported and will look like a missing file on the source or destination. Copy + delete explicitly for cross-backend moves.Log::debug('resolved', [$path, get_class($resolved->getFilesystem())]) in a decorator around the resolver saves hours versus staring at adapter exceptions.Resolvable for custom routing, the Resolved value object for tests, and decoration of ResolvableFilesystem itself for cross-cutting concerns (logging, metrics, retries) — since it is just another FilesystemInterface, wrapping it is trivial.read() loading whole objects) applies unchanged.src/Resolvable directory of the installed version rather than blog posts.How can I help you explore Laravel packages today?