Custom filesystem drivers
Concept
Custom filesystem drivers — Laravel's Flysystem integration allows you to register your own storage drivers. When the built-in drivers (local, S3, SFTP, FTP) don't meet your needs, you implement a custom adapter and bind it to a named disk.
When you need a custom driver:
- Storing files in a proprietary cloud (Cloudinary, Bunny CDN, Backblaze B2).
- Encrypting all stored files transparently.
- A virtual filesystem (database-backed storage, in-memory for testing).
- Wrapping an existing service with additional logic (logging, virus scanning on write).
How it works:
- Implement a Flysystem adapter by implementing
League\Flysystem\FilesystemAdapter. - Register the driver in a service provider using
Storage::extend(). - Configure the disk in
config/filesystems.phpwith'driver' => 'your-driver'. - Use
Storage::disk('your-disk')as normal — the same API regardless of driver.
Flysystem: The underlying library Laravel uses for all file storage. All drivers (local, S3, SFTP) are Flysystem adapters. Custom drivers plug into the same layer.
Laravel Pennant / community packages: For many popular services (Cloudinary, B2, Dropbox), community packages already provide Flysystem adapters. You may not need to write one from scratch.
Extending vs replacing: Storage::extend() registers the driver factory for a driver name. The disk config in config/filesystems.php maps disk names to drivers. You can have multiple disks using the same custom driver with different configuration.
Code Example
<?php
// 1. IMPLEMENT the Flysystem adapter
// A minimal adapter wrapping a fictional cloud provider
use League\Flysystem\Config;
use League\Flysystem\FileAttributes;
use League\Flysystem\FilesystemAdapter;
use League\Flysystem\UnableToDeleteFile;
use League\Flysystem\UnableToReadFile;
use League\Flysystem\UnableToWriteFile;
class BunnyCdnAdapter implements FilesystemAdapter
{
public function __construct(
private readonly BunnyCdnClient $client,
private readonly string $zone,
) {}
public function write(string $path, string $contents, Config $config): void
{
try {
$this->client->upload($this->zone, $path, $contents);
} catch (\Throwable $e) {
throw UnableToWriteFile::atLocation($path, $e->getMessage(), $e);
}
}
public function read(string $path): string
{
try {
return $this->client->download($this->zone, $path);
} catch (\Throwable $e) {
throw UnableToReadFile::fromLocation($path, $e->getMessage(), $e);
}
}
public function delete(string $path): void
{
try {
$this->client->delete($this->zone, $path);
} catch (\Throwable $e) {
throw UnableToDeleteFile::atLocation($path, $e->getMessage(), $e);
}
}
public function fileExists(string $path): bool
{
return $this->client->exists($this->zone, $path);
}
public function directoryExists(string $path): bool
{
return $this->client->directoryExists($this->zone, $path);
}
// ... implement all FilesystemAdapter methods
public function writeStream(string $path, $contents, Config $config): void { /* ... */ }
public function readStream(string $path) { /* ... */ }
public function deleteDirectory(string $path): void { /* ... */ }
public function createDirectory(string $path, Config $config): void { /* ... */ }
public function setVisibility(string $path, string $visibility): void { /* ... */ }
public function visibility(string $path): \League\Flysystem\FileAttributes { /* ... */ }
public function mimeType(string $path): FileAttributes { /* ... */ }
public function lastModified(string $path): FileAttributes { /* ... */ }
public function fileSize(string $path): FileAttributes { /* ... */ }
public function listContents(string $path, bool $deep): iterable { /* ... */ }
public function move(string $source, string $destination, Config $config): void { /* ... */ }
public function copy(string $source, string $destination, Config $config): void { /* ... */ }
}
// 2. REGISTER the driver in a Service Provider
// app/Providers/FilesystemServiceProvider.php
use Illuminate\Support\ServiceProvider;
use Illuminate\Filesystem\FilesystemAdapter as LaravelAdapter;
use Illuminate\Support\Facades\Storage;
use League\Flysystem\Filesystem;
class FilesystemServiceProvider extends ServiceProvider
{
public function boot(): void
{
Storage::extend('bunnycdn', function (\Illuminate\Contracts\Foundation\Application $app, array $config) {
$client = new BunnyCdnClient(apiKey: $config['api_key']);
$adapter = new BunnyCdnAdapter($client, zone: $config['zone']);
$flysystem = new Filesystem($adapter, ['visibility' => 'public']);
return new LaravelAdapter($flysystem, $config, $adapter);
});
}
}
// 3. CONFIGURE the disk in config/filesystems.php
'disks' => [
// ... existing disks ...
'bunnycdn' => [
'driver' => 'bunnycdn', // matches the name in Storage::extend()
'api_key' => env('BUNNYCDN_API_KEY'),
'zone' => env('BUNNYCDN_STORAGE_ZONE'),
'url' => env('BUNNYCDN_URL', 'https://cdn.example.com'),
],
],
// 4. USE it — same API as any other disk
Storage::disk('bunnycdn')->put('uploads/image.jpg', $fileContents);
Storage::disk('bunnycdn')->exists('uploads/image.jpg');
Storage::disk('bunnycdn')->delete('uploads/image.jpg');
$url = Storage::disk('bunnycdn')->url('uploads/image.jpg');
// Set as default disk in .env:
// FILESYSTEM_DISK=bunnycdn
// DATABASE-BACKED custom driver (for testing without real storage)
class DatabaseFilesystemAdapter implements FilesystemAdapter
{
public function write(string $path, string $contents, Config $config): void
{
\DB::table('stored_files')->updateOrInsert(
['path' => $path],
['contents' => $contents, 'size' => strlen($contents), 'updated_at' => now()]
);
}
public function read(string $path): string
{
$row = \DB::table('stored_files')->where('path', $path)->first();
if (!$row) throw UnableToReadFile::fromLocation($path);
return $row->contents;
}
// ... other methods
}