Custom Laravel disks & environment-based filesystems

Laravel provides a flexible way of using different "disks" or filesystems to store files. This means you can store files locally in a path relative to your code or using a remote storage solution such as Amazon S3.

Out of the box, Laravel allows you to switch between different default filesystem providers. When working locally your content can be served from a local filesystem, yet in production, you can use a more robust remote solution. This is controlled by your .env configuration, which can be set differently per environment.

FILESYSTEM_DRIVER=local # local / testing
FILESYSTEM_DRIVER=s3 # production

Custom disks

I like setting up custom disks, specific to the content I am using. For example, I have disk configuration for my movie, blog and photo sections, even splitting the movie posters and stills based on sub folders.

// config/filesystems.php
'disks' => [
	'posters' => [
		'driver' => 'local',
		'root' => storage_path('app/images/movies/posters'),
		'url' => env('APP_URL') . '/images/movies/posters',
		'visibility' => 'public',
	],
	'stills' => [
		'driver' => 'local',
		'root' => storage_path('app/images/movies/stills'),
		'url' => env('APP_URL') . '/images/movies/stills',
		'visibility' => 'public',
	],
	'photos' => [
		'driver' => 'local',
		'root' => storage_path('app/images/photos'),
		'url' => env('APP_URL') . '/images/photos',
		'visibility' => 'public',
	],
];

Instead of combining the folder path and filename whenever I need these image, I can use the correct disk, pass in the filename and then get the correct URL. For example, the movie model might have the following methods (or attribute accessor).

class Movie extends Model
{
	public function poster(): string
	{
		return Storage::disk('posters')->url($this->fileName);
	}
	public function still(): string
	{
		return Storage::disk('stills')->url($this->fileName);
	}
}

Then when interacting with the model, I can easily output the image, with the correct folder / path.

<img src="{{ $movie->poster() }}" />
<img src="{{ $movie->still() }}" />

Custom disk and different filesystems

The downside of these custom disks is there is no way with the configuration above to change their filesystem. The custom disks could be removed and the code changed to use the base "local" or "s3" filesystems. That would mean refactoring all the modelling by adding in the correct paths wherever the custom disk was used.

Part of my "todo" for this website was to add in the flexibility of using the custom disks with environment-driven filesystem options. Serendipitously I saw a tweet from Tony Messias who came up with a nice solution.

They have used the PHP 8 method match() to return the different filesystem configuration based on the env('APP_ENV'). By default the local filesystem is used, but in production the S3 filesystem is used.

I loved the simplicity of this code, but this website is currently running on PHP 7.4 (another item in my "todo" is to upgrade), so I needed a different solution. I also didn't want to tie this switch in configuration to the application environment (local, testing, production). Instead, I prefer the same solution used by the default provider, by defining a FILESYSTEM_DRIVER environment variable per disk.

My solution

Using Laravel’s value() helper, I can get code similar to the match() solution — which returns a value — by using a closure instead. Inside this closure I can use a switch() statement to recreate the same behaviour as match().

// config/filesystems.php
'disks' => [
    'posters' => value(static function () use ($s3Disk): array {
        $disk = env('FILESYSTEM_DRIVER_POSTERS', 'local');
        $path = 'movies/poster';

        switch ($disk) {
            case 's3':
                return array_merge($s3Disk, [
                    'root' => $path,
                ]);
                break;

            case 'local':
            default:
                return [
                    'driver' => 'local',
                    'root' => storage_path("app/images/{$path}"),
                    'url' => env('APP_URL') . "/images/{$path}",
                    'visibility' => 'public',
                ];
                break;
        }
    })
]

One thing to note with the above code snippet is that the $s3Disk variable is an array with the default S3 configuration. The code then merges any alterations we want to override, such as the addition of the root path. This reduces code duplication if you have many custom disks. There is also room for improvement and customisation, by defining different buckets, regions or even api keys per custom disk.

Without a FILESYSTEM_DRIVER_POSTERS environment variable being defined in the .env, the disk will default to the local filesystem provider, with the customised root and URL. Each environment is then free to configure the appropriate filesystem per disk. I can easily check the different filesystems by changing the .env file;

FILESYSTEM_DRIVER_POSTERS=s3
FILESYSTEM_DRIVER_STILLS=s3
FILESYSTEM_DRIVER_PHOTOS=local

Once I upgrade this website to PHP 8, I will refactor the configuration to use match() as it simplies the code quite a lot.