File storage
Overview
Tempest provides the ability to interact with the local filesystem and many cloud storage solutions, such as Cloudflare R2 or Amazon S3, using the same interface.
This implementation is built on top of Flysystem—a reliable, battle-tested abstraction layer for file systems.
Getting started
To get started with file storage, you will first need to create a configuration file for your desired filesystem.
Tempest provides a different configuration object for each provider. For instance, if you wish to interact with an Amazon S3 bucket, you may create a s3.config.php
file returning an instance of S3StorageConfig
:
return new S3StorageConfig( bucket: env('S3_BUCKET'), region: env('S3_REGION'), accessKeyId: env('S3_ACCESS_KEY_ID'), secretAccessKey: env('S3_SECRET_ACCESS_KEY'), );
In this example, the S3 credentials are specified in the .env
, so a different bucket and credentials can be configured depending on the environment.
Once your storage is configured, you may interact with it by using the Tempest\Storage\Storage
interface. This is usually done through dependency injection:
final readonly class UserService { public function __construct( private Storage $storage, ) {} public function getProfilePictureUrl(User $user): string { return $this->storage->publicUrl($user->profile_picture_path); } // … }
The storage interface
Once you have access to the the Storage
interface, you gain access to a few useful methods for working with files, directory and streams. All methods are documented, so you are free to explore the source to get an understanding of what you can do with it.
Below are a few useful methods that you may need more often than the others:
/** * Gets a public URL to the file at the specified `$location`. */ $storage->publicUrl($location); /** * Writes the given `$contents` to the specified `$location`. */ $storage->write($location, $contents); /** * Reads the contents of the file at the specified `$location`. */ $storage->read($location); /** * Deletes the contents of the file at the specified `$location`. */ $storage->delete($location); /** * Determines whether a file exists at the specified `$location`. */ $storage->fileOrDirectoryExists($location);
Configuration
Tempest provides a different configuration object for each storage provider. Below are the ones that are currently supported:
Tempest\Storage\Config\LocalStorageConfig
Tempest\Storage\Config\R2StorageConfig
Tempest\Storage\Config\S3StorageConfig
Tempest\Storage\Config\AzureStorageConfig
Tempest\Storage\Config\FTPStorageConfig
Tempest\Storage\Config\GoogleCloudStorageConfig
Tempest\Storage\Config\InMemoryStorageConfig
Tempest\Storage\Config\SFTPStorageConfig
Tempest\Storage\Config\StorageConfig
Tempest\Storage\Config\ZipArchiveStorageConfig
Tempest\Storage\Config\CustomStorageConfig
Multiple storages
If you need to work with multiple storage locations, you create multiple storage configurations using tags. This tag may then be used to resolve the Storage
interface, which will use the corresponding configuration.
It's a good practice to use an enum for the tag:
return new S3StorageConfig( tag: StorageLocation::USER_DATA, bucket: env('USERDATA_S3_BUCKET'), region: env('USERDATA_S3_REGION'), accessKeyId: env('USERDATA_S3_ACCESS_KEY_ID'), secretAccessKey: env('USERDATA_S3_SECRET_ACCESS_KEY'), );
return new R2StorageConfig( tag: StorageLocation::BACKUPS, bucket: env('BACKUPS_R2_BUCKET'), endpoint: env('BACKUPS_R2_ENDPOINT'), accessKeyId: env('BACKUPS_R2_ACCESS_KEY_ID'), secretAccessKey: env('BACKUPS_R2_SECRET_ACCESS_KEY'), );
Once you have configured your storages and your tags, you may inject the Storage
interface using the corresponding tag:
final readonly class BackupService { public function __construct( #[Tag(StorageLocation::BACKUPS)] private Storage $storage, ) {} // … }
Read-only storage
A storage may be restricted to only allow read operations. Attempting to write to such a storage will result in a League\Flysystem\UnableToWriteFile
exception being thrown.
First, the league/flysystem-read-only
adapter needs to be installed:
composer require league/flysystem-read-only
Once this is done, you may pass the readonly
parameter to the adapter configuration and set it to true
.
return new S3StorageConfig( tag: StorageLocation::DATA_SNAPSHOTS, readonly: true, bucket: env('DATA_SNAPSHOTS_S3_BUCKET'), region: env('DATA_SNAPSHOTS_S3_REGION'), accessKeyId: env('DATA_SNAPSHOTS_S3_ACCESS_KEY_ID'), secretAccessKey: env('DATA_SNAPSHOTS_S3_SECRET_ACCESS_KEY'), );
Custom storage
If you need to implement your own adapter for an unsupported provider, you may do so by implementing the League\Flysystem\FilesystemAdapter
interface.
Tempest provides a CustomStorageConfig
configuration object which accepts any FilesystemAdapter
, which will be resolved through the container.
return new CustomStorageConfig( adapter: App\MyCustomFilesystemAdapter::class, );
Testing
By extending Tempest\Framework\Testing\IntegrationTest
from your test case, you gain access to the storage testing utilities through the storage
property.
These utilities include a way to replace the storage with a testing implementation, as well as a few assertion methods related to files and directories.
Faking a storage
You may generate a fake, testing-only storage by calling the fake()
method on the storage
property. This will replace the storage implementation in the container, and provide useful assertion methods.
// Replace the storage with a fake implementation $storage = $this->storage->fake(); // Replace the specified storage with a fake implementation $storage = $this->storage->fake('user-profile-pictures'); // Asserts that the specified file exists $storage->assertFileExists('file.txt');
These fake storages are located in vendor/.tempest/tests/storage
. They get erased every time the fake()
method is called. To prevent this, you may set the persist
argument to true
.
Preventing storage access during tests
It may be useful to prevent code from using any of the registered storages during tests. This could happen when forgetting to fake a storage for a specific test, for instance, and could result in unexpected costs when relying on a cloud storage provider.
This may be achieved by calling the preventUsageWithoutFake()
method on the storage
property.
$this->storage->preventEventHandling();