0

Custom filesystem drivers

Advanced5 min read·lv-23-004

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:

  1. Implement a Flysystem adapter by implementing League\Flysystem\FilesystemAdapter.
  2. Register the driver in a service provider using Storage::extend().
  3. Configure the disk in config/filesystems.php with 'driver' => 'your-driver'.
  4. 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
<?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
}