feat: extract storage providers from app/Plug/Storage

Bunny Storage (Browse, Delete, Download, Upload, VBucket), StorageManager,
domain contracts (Browseable, Deletable, Downloadable, Uploadable)
with Core\Plug\Storage namespace alignment.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Snider 2026-03-09 17:29:23 +00:00
parent 619b53135d
commit 373b94efab
11 changed files with 1191 additions and 0 deletions

17
composer.json Normal file
View file

@ -0,0 +1,17 @@
{
"name": "core/php-plug-storage",
"description": "Storage provider integrations for the Plug framework",
"type": "library",
"license": "EUPL-1.2",
"require": {
"php": "^8.2",
"core/php": "^1.0"
},
"autoload": {
"psr-4": {
"Core\\Plug\\Storage\\": "src/"
}
},
"minimum-stability": "dev",
"prefer-stable": true
}

144
src/Bunny/Browse.php Normal file
View file

@ -0,0 +1,144 @@
<?php
declare(strict_types=1);
namespace Core\Plug\Storage\Bunny;
use Core\Plug\Concern\BuildsResponse;
use Core\Plug\Response;
use Core\Plug\Storage\Contract\Browseable;
use Bunny\Storage\Client;
use Illuminate\Support\Facades\Log;
/**
* Bunny Storage browsing operations.
*
* Supports:
* - List files in directory
* - Check file existence
* - Get file size
* - Public and private storage zones
*/
class Browse implements Browseable
{
use BuildsResponse;
protected string $apiKey;
protected string $storageZone;
protected string $region;
protected ?Client $client = null;
public function __construct(
?string $apiKey = null,
?string $storageZone = null,
?string $region = null,
string $zone = 'public'
) {
$this->apiKey = $apiKey ?? config("cdn.bunny.{$zone}.api_key", '');
$this->storageZone = $storageZone ?? config("cdn.bunny.{$zone}.storage_zone", '');
$this->region = $region ?? config("cdn.bunny.{$zone}.region", Client::STORAGE_ZONE_FS_EU);
}
/**
* Check if the service is configured.
*/
public function isConfigured(): bool
{
return ! empty($this->apiKey) && ! empty($this->storageZone);
}
/**
* Get the Bunny Storage client.
*/
protected function client(): ?Client
{
if ($this->client === null && $this->isConfigured()) {
$this->client = new Client($this->apiKey, $this->storageZone, $this->region);
}
return $this->client;
}
/**
* List files in a path.
*/
public function list(string $path = '/'): Response
{
if (! $this->isConfigured()) {
return $this->error('Bunny Storage not configured');
}
try {
$files = $this->client()->listFiles($path);
return $this->ok([
'path' => $path,
'files' => $files,
'count' => count($files),
]);
} catch (\Exception $e) {
Log::error('Bunny Storage: List failed', [
'path' => $path,
'error' => $e->getMessage(),
]);
return $this->error($e->getMessage());
}
}
/**
* Check if a file exists.
*/
public function exists(string $path): bool
{
if (! $this->isConfigured()) {
return false;
}
try {
// Try to get the file contents - if it fails, file doesn't exist
$this->client()->getContents($path);
return true;
} catch (\Exception $e) {
return false;
}
}
/**
* Get file size in bytes.
*/
public function size(string $path): ?int
{
if (! $this->isConfigured()) {
return null;
}
try {
$contents = $this->client()->getContents($path);
return strlen($contents);
} catch (\Exception $e) {
return null;
}
}
/**
* Create a browser for the public storage zone.
*/
public static function public(): self
{
return new self(zone: 'public');
}
/**
* Create a browser for the private storage zone.
*/
public static function private(): self
{
return new self(zone: 'private');
}
}

146
src/Bunny/Delete.php Normal file
View file

@ -0,0 +1,146 @@
<?php
declare(strict_types=1);
namespace Core\Plug\Storage\Bunny;
use Core\Plug\Concern\BuildsResponse;
use Core\Plug\Response;
use Core\Plug\Storage\Contract\Deletable;
use Bunny\Storage\Client;
use Illuminate\Support\Facades\Log;
/**
* Bunny Storage delete operations.
*
* Supports:
* - Single file deletion
* - Bulk file deletion
* - Public and private storage zones
*/
class Delete implements Deletable
{
use BuildsResponse;
protected string $apiKey;
protected string $storageZone;
protected string $region;
protected ?Client $client = null;
public function __construct(
?string $apiKey = null,
?string $storageZone = null,
?string $region = null,
string $zone = 'public'
) {
$this->apiKey = $apiKey ?? config("cdn.bunny.{$zone}.api_key", '');
$this->storageZone = $storageZone ?? config("cdn.bunny.{$zone}.storage_zone", '');
$this->region = $region ?? config("cdn.bunny.{$zone}.region", Client::STORAGE_ZONE_FS_EU);
}
/**
* Check if the service is configured.
*/
public function isConfigured(): bool
{
return ! empty($this->apiKey) && ! empty($this->storageZone);
}
/**
* Get the Bunny Storage client.
*/
protected function client(): ?Client
{
if ($this->client === null && $this->isConfigured()) {
$this->client = new Client($this->apiKey, $this->storageZone, $this->region);
}
return $this->client;
}
/**
* Delete a single file.
*/
public function path(string $remotePath): Response
{
if (! $this->isConfigured()) {
return $this->error('Bunny Storage not configured');
}
try {
$this->client()->delete($remotePath);
return $this->ok([
'deleted' => true,
'path' => $remotePath,
]);
} catch (\Exception $e) {
Log::error('Bunny Storage: Delete failed', [
'remote' => $remotePath,
'error' => $e->getMessage(),
]);
return $this->error($e->getMessage());
}
}
/**
* Delete multiple files.
*
* @param array<string> $remotePaths
*/
public function paths(array $remotePaths): Response
{
if (! $this->isConfigured()) {
return $this->error('Bunny Storage not configured');
}
$failed = [];
$deleted = [];
foreach ($remotePaths as $path) {
try {
$this->client()->delete($path);
$deleted[] = $path;
} catch (\Exception $e) {
$failed[] = $path;
Log::error('Bunny Storage: Delete failed', [
'remote' => $path,
'error' => $e->getMessage(),
]);
}
}
if (! empty($failed)) {
return $this->error('Some files failed to delete', [
'deleted' => $deleted,
'failed' => $failed,
'total' => count($remotePaths),
]);
}
return $this->ok([
'deleted' => count($deleted),
'paths' => $deleted,
]);
}
/**
* Create a deleter for the public storage zone.
*/
public static function public(): self
{
return new self(zone: 'public');
}
/**
* Create a deleter for the private storage zone.
*/
public static function private(): self
{
return new self(zone: 'private');
}
}

142
src/Bunny/Download.php Normal file
View file

@ -0,0 +1,142 @@
<?php
declare(strict_types=1);
namespace Core\Plug\Storage\Bunny;
use Core\Plug\Concern\BuildsResponse;
use Core\Plug\Response;
use Core\Plug\Storage\Contract\Downloadable;
use Bunny\Storage\Client;
use Illuminate\Support\Facades\Log;
/**
* Bunny Storage download operations.
*
* Supports:
* - Download file contents as string
* - Download file to local path
* - Public and private storage zones
*/
class Download implements Downloadable
{
use BuildsResponse;
protected string $apiKey;
protected string $storageZone;
protected string $region;
protected ?Client $client = null;
public function __construct(
?string $apiKey = null,
?string $storageZone = null,
?string $region = null,
string $zone = 'public'
) {
$this->apiKey = $apiKey ?? config("cdn.bunny.{$zone}.api_key", '');
$this->storageZone = $storageZone ?? config("cdn.bunny.{$zone}.storage_zone", '');
$this->region = $region ?? config("cdn.bunny.{$zone}.region", Client::STORAGE_ZONE_FS_EU);
}
/**
* Check if the service is configured.
*/
public function isConfigured(): bool
{
return ! empty($this->apiKey) && ! empty($this->storageZone);
}
/**
* Get the Bunny Storage client.
*/
protected function client(): ?Client
{
if ($this->client === null && $this->isConfigured()) {
$this->client = new Client($this->apiKey, $this->storageZone, $this->region);
}
return $this->client;
}
/**
* Get file contents as string.
*/
public function contents(string $remotePath): Response
{
if (! $this->isConfigured()) {
return $this->error('Bunny Storage not configured');
}
try {
$contents = $this->client()->getContents($remotePath);
return $this->ok([
'contents' => $contents,
'remote_path' => $remotePath,
'size' => strlen($contents),
]);
} catch (\Exception $e) {
Log::error('Bunny Storage: getContents failed', [
'remote' => $remotePath,
'error' => $e->getMessage(),
]);
return $this->error($e->getMessage());
}
}
/**
* Download file to local path.
*/
public function toFile(string $remotePath, string $localPath): Response
{
if (! $this->isConfigured()) {
return $this->error('Bunny Storage not configured');
}
try {
$contents = $this->client()->getContents($remotePath);
$directory = dirname($localPath);
if (! is_dir($directory)) {
mkdir($directory, 0755, true);
}
file_put_contents($localPath, $contents);
return $this->ok([
'downloaded' => true,
'remote_path' => $remotePath,
'local_path' => $localPath,
'size' => strlen($contents),
]);
} catch (\Exception $e) {
Log::error('Bunny Storage: Download to file failed', [
'remote' => $remotePath,
'local' => $localPath,
'error' => $e->getMessage(),
]);
return $this->error($e->getMessage());
}
}
/**
* Create a downloader for the public storage zone.
*/
public static function public(): self
{
return new self(zone: 'public');
}
/**
* Create a downloader for the private storage zone.
*/
public static function private(): self
{
return new self(zone: 'private');
}
}

139
src/Bunny/Upload.php Normal file
View file

@ -0,0 +1,139 @@
<?php
declare(strict_types=1);
namespace Core\Plug\Storage\Bunny;
use Core\Plug\Concern\BuildsResponse;
use Core\Plug\Response;
use Core\Plug\Storage\Contract\Uploadable;
use Bunny\Storage\Client;
use Illuminate\Support\Facades\Log;
/**
* Bunny Storage upload operations.
*
* Supports:
* - File uploads from local path
* - Direct content uploads
* - Public and private storage zones
*/
class Upload implements Uploadable
{
use BuildsResponse;
protected string $apiKey;
protected string $storageZone;
protected string $region;
protected ?Client $client = null;
public function __construct(
?string $apiKey = null,
?string $storageZone = null,
?string $region = null,
string $zone = 'public'
) {
$this->apiKey = $apiKey ?? config("cdn.bunny.{$zone}.api_key", '');
$this->storageZone = $storageZone ?? config("cdn.bunny.{$zone}.storage_zone", '');
$this->region = $region ?? config("cdn.bunny.{$zone}.region", Client::STORAGE_ZONE_FS_EU);
}
/**
* Check if the service is configured.
*/
public function isConfigured(): bool
{
return ! empty($this->apiKey) && ! empty($this->storageZone);
}
/**
* Get the Bunny Storage client.
*/
protected function client(): ?Client
{
if ($this->client === null && $this->isConfigured()) {
$this->client = new Client($this->apiKey, $this->storageZone, $this->region);
}
return $this->client;
}
/**
* Upload a file from local path.
*/
public function file(string $localPath, string $remotePath): Response
{
if (! $this->isConfigured()) {
return $this->error('Bunny Storage not configured');
}
if (! file_exists($localPath)) {
return $this->error('Local file not found', ['local_path' => $localPath]);
}
try {
$this->client()->upload($localPath, $remotePath);
return $this->ok([
'uploaded' => true,
'local_path' => $localPath,
'remote_path' => $remotePath,
'size' => filesize($localPath),
]);
} catch (\Exception $e) {
Log::error('Bunny Storage: Upload failed', [
'local' => $localPath,
'remote' => $remotePath,
'error' => $e->getMessage(),
]);
return $this->error($e->getMessage());
}
}
/**
* Upload content directly.
*/
public function contents(string $remotePath, string $contents): Response
{
if (! $this->isConfigured()) {
return $this->error('Bunny Storage not configured');
}
try {
$this->client()->putContents($remotePath, $contents);
return $this->ok([
'uploaded' => true,
'remote_path' => $remotePath,
'size' => strlen($contents),
]);
} catch (\Exception $e) {
Log::error('Bunny Storage: putContents failed', [
'remote' => $remotePath,
'error' => $e->getMessage(),
]);
return $this->error($e->getMessage());
}
}
/**
* Create an uploader for the public storage zone.
*/
public static function public(): self
{
return new self(zone: 'public');
}
/**
* Create an uploader for the private storage zone.
*/
public static function private(): self
{
return new self(zone: 'private');
}
}

308
src/Bunny/VBucket.php Normal file
View file

@ -0,0 +1,308 @@
<?php
declare(strict_types=1);
namespace Core\Plug\Storage\Bunny;
use Core\Crypt\LthnHash;
use Core\Plug\Concern\BuildsResponse;
use Core\Plug\Response;
use Bunny\Storage\Client;
use Illuminate\Support\Facades\Log;
/**
* Bunny Storage vBucket operations for workspace-isolated paths.
*
* vBuckets provide workspace isolation on CDN:
* - Each workspace gets a unique vBucket ID
* - All paths are automatically scoped to the vBucket
* - Prevents cross-workspace data access
*
* @example
* $vbucket = new VBucket('example.com');
* $vbucket->upload('/local/file.jpg', 'images/hero.jpg');
* // Uploads to: /{vbucket_id}/images/hero.jpg
*/
class VBucket
{
use BuildsResponse;
protected string $domain;
protected string $vBucketId;
protected string $apiKey;
protected string $storageZone;
protected string $region;
protected ?Client $client = null;
public function __construct(
string $domain,
?string $apiKey = null,
?string $storageZone = null,
?string $region = null,
string $zone = 'public'
) {
$this->domain = $domain;
$this->vBucketId = LthnHash::vBucketId($domain);
$this->apiKey = $apiKey ?? config("cdn.bunny.{$zone}.api_key", '');
$this->storageZone = $storageZone ?? config("cdn.bunny.{$zone}.storage_zone", '');
$this->region = $region ?? config("cdn.bunny.{$zone}.region", Client::STORAGE_ZONE_FS_EU);
}
/**
* Check if the service is configured.
*/
public function isConfigured(): bool
{
return ! empty($this->apiKey) && ! empty($this->storageZone);
}
/**
* Get the Bunny Storage client.
*/
protected function client(): ?Client
{
if ($this->client === null && $this->isConfigured()) {
$this->client = new Client($this->apiKey, $this->storageZone, $this->region);
}
return $this->client;
}
/**
* Get the vBucket ID for this domain.
*/
public function id(): string
{
return $this->vBucketId;
}
/**
* Get the domain for this vBucket.
*/
public function domain(): string
{
return $this->domain;
}
/**
* Build a vBucket-scoped path.
*/
public function path(string $path): string
{
return $this->vBucketId.'/'.ltrim($path, '/');
}
/**
* Upload a file to vBucket-scoped path.
*/
public function upload(string $localPath, string $remotePath): Response
{
if (! $this->isConfigured()) {
return $this->error('Bunny Storage not configured');
}
if (! file_exists($localPath)) {
return $this->error('Local file not found', ['local_path' => $localPath]);
}
$scopedPath = $this->path($remotePath);
try {
$this->client()->upload($localPath, $scopedPath);
return $this->ok([
'uploaded' => true,
'domain' => $this->domain,
'vbucket_id' => $this->vBucketId,
'local_path' => $localPath,
'remote_path' => $scopedPath,
'size' => filesize($localPath),
]);
} catch (\Exception $e) {
Log::error('Bunny Storage vBucket: Upload failed', [
'domain' => $this->domain,
'local' => $localPath,
'remote' => $scopedPath,
'error' => $e->getMessage(),
]);
return $this->error($e->getMessage());
}
}
/**
* Upload content directly to vBucket-scoped path.
*/
public function putContents(string $remotePath, string $contents): Response
{
if (! $this->isConfigured()) {
return $this->error('Bunny Storage not configured');
}
$scopedPath = $this->path($remotePath);
try {
$this->client()->putContents($scopedPath, $contents);
return $this->ok([
'uploaded' => true,
'domain' => $this->domain,
'vbucket_id' => $this->vBucketId,
'remote_path' => $scopedPath,
'size' => strlen($contents),
]);
} catch (\Exception $e) {
Log::error('Bunny Storage vBucket: putContents failed', [
'domain' => $this->domain,
'remote' => $scopedPath,
'error' => $e->getMessage(),
]);
return $this->error($e->getMessage());
}
}
/**
* Get file contents from vBucket-scoped path.
*/
public function getContents(string $remotePath): Response
{
if (! $this->isConfigured()) {
return $this->error('Bunny Storage not configured');
}
$scopedPath = $this->path($remotePath);
try {
$contents = $this->client()->getContents($scopedPath);
return $this->ok([
'contents' => $contents,
'domain' => $this->domain,
'vbucket_id' => $this->vBucketId,
'remote_path' => $scopedPath,
'size' => strlen($contents),
]);
} catch (\Exception $e) {
Log::error('Bunny Storage vBucket: getContents failed', [
'domain' => $this->domain,
'remote' => $scopedPath,
'error' => $e->getMessage(),
]);
return $this->error($e->getMessage());
}
}
/**
* Delete file from vBucket-scoped path.
*/
public function delete(string $remotePath): Response
{
if (! $this->isConfigured()) {
return $this->error('Bunny Storage not configured');
}
$scopedPath = $this->path($remotePath);
try {
$this->client()->delete($scopedPath);
return $this->ok([
'deleted' => true,
'domain' => $this->domain,
'vbucket_id' => $this->vBucketId,
'path' => $scopedPath,
]);
} catch (\Exception $e) {
Log::error('Bunny Storage vBucket: Delete failed', [
'domain' => $this->domain,
'remote' => $scopedPath,
'error' => $e->getMessage(),
]);
return $this->error($e->getMessage());
}
}
/**
* List files in vBucket-scoped path.
*/
public function list(string $path = ''): Response
{
if (! $this->isConfigured()) {
return $this->error('Bunny Storage not configured');
}
$scopedPath = $this->path($path);
try {
$files = $this->client()->listFiles($scopedPath);
return $this->ok([
'path' => $scopedPath,
'domain' => $this->domain,
'vbucket_id' => $this->vBucketId,
'files' => $files,
'count' => count($files),
]);
} catch (\Exception $e) {
Log::error('Bunny Storage vBucket: List failed', [
'domain' => $this->domain,
'path' => $scopedPath,
'error' => $e->getMessage(),
]);
return $this->error($e->getMessage());
}
}
/**
* Check if file exists in vBucket-scoped path.
*/
public function exists(string $path): bool
{
if (! $this->isConfigured()) {
return false;
}
$scopedPath = $this->path($path);
try {
$this->client()->getContents($scopedPath);
return true;
} catch (\Exception $e) {
return false;
}
}
/**
* Create a vBucket for a workspace (convenience method).
*/
public static function forWorkspace(string $workspaceUuid, string $zone = 'public'): self
{
return new self($workspaceUuid, zone: $zone);
}
/**
* Create a public zone vBucket.
*/
public static function public(string $domain): self
{
return new self($domain, zone: 'public');
}
/**
* Create a private zone vBucket.
*/
public static function private(string $domain): self
{
return new self($domain, zone: 'private');
}
}

View file

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Core\Plug\Storage\Contract;
use Core\Plug\Response;
/**
* File browsing and inspection operations.
*/
interface Browseable
{
/**
* List files in a path.
*/
public function list(string $path = '/'): Response;
/**
* Check if a file exists.
*/
public function exists(string $path): bool;
/**
* Get file size in bytes.
*/
public function size(string $path): ?int;
}

View file

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Core\Plug\Storage\Contract;
use Core\Plug\Response;
/**
* File deletion operations.
*/
interface Deletable
{
/**
* Delete a single file.
*/
public function path(string $remotePath): Response;
/**
* Delete multiple files.
*
* @param array<string> $remotePaths
*/
public function paths(array $remotePaths): Response;
}

View file

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Core\Plug\Storage\Contract;
use Core\Plug\Response;
/**
* File download operations.
*/
interface Downloadable
{
/**
* Get file contents as string.
*/
public function contents(string $remotePath): Response;
/**
* Download file to local path.
*/
public function toFile(string $remotePath, string $localPath): Response;
}

View file

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Core\Plug\Storage\Contract;
use Core\Plug\Response;
/**
* File upload operations.
*/
interface Uploadable
{
/**
* Upload a file from local path to remote path.
*/
public function file(string $localPath, string $remotePath): Response;
/**
* Upload string contents directly to remote path.
*/
public function contents(string $remotePath, string $contents): Response;
}

196
src/StorageManager.php Normal file
View file

@ -0,0 +1,196 @@
<?php
declare(strict_types=1);
namespace Core\Plug\Storage;
use Core\Plug\Storage\Contract\Browseable;
use Core\Plug\Storage\Contract\Deletable;
use Core\Plug\Storage\Contract\Downloadable;
use Core\Plug\Storage\Contract\Uploadable;
use InvalidArgumentException;
/**
* Storage Manager - Factory for storage provider operations.
*
* Resolves storage providers from config and provides type-safe access to operations.
* Supports zone switching for providers with multiple storage zones (public/private).
*
* @example
* $storage = app(StorageManager::class);
*
* // Get operations
* $uploader = $storage->upload(); // Returns Uploadable for default driver
* $downloader = $storage->download(); // Returns Downloadable
*
* // Specific driver
* $uploader = $storage->driver('bunny')->upload();
*
* // Zone switching (for Bunny)
* $uploader = $storage->zone('private')->upload();
*
* // vBucket for workspace isolation
* $vbucket = $storage->vbucket('example.com');
*/
class StorageManager
{
protected string $defaultDriver;
protected string $zone = 'public';
/**
* @var array<string, array{upload: class-string<Uploadable>, download: class-string<Downloadable>, delete: class-string<Deletable>, browse: class-string<Browseable>, vbucket?: class-string}>
*/
protected array $drivers = [
'bunny' => [
'upload' => Bunny\Upload::class,
'download' => Bunny\Download::class,
'delete' => Bunny\Delete::class,
'browse' => Bunny\Browse::class,
'vbucket' => Bunny\VBucket::class,
],
];
public function __construct()
{
$this->defaultDriver = config('cdn.storage_driver', 'bunny');
}
/**
* Get the default driver name.
*/
public function getDefaultDriver(): string
{
return $this->defaultDriver;
}
/**
* Set the default driver.
*/
public function setDefaultDriver(string $driver): self
{
$this->defaultDriver = $driver;
return $this;
}
/**
* Get a driver instance.
*/
public function driver(?string $driver = null): self
{
if ($driver !== null) {
$this->defaultDriver = $driver;
}
return $this;
}
/**
* Set the storage zone (public/private).
*/
public function zone(string $zone): self
{
$this->zone = $zone;
return $this;
}
/**
* Get the upload operation for the current driver.
*/
public function upload(): Uploadable
{
return $this->resolve('upload');
}
/**
* Get the download operation for the current driver.
*/
public function download(): Downloadable
{
return $this->resolve('download');
}
/**
* Get the delete operation for the current driver.
*/
public function delete(): Deletable
{
return $this->resolve('delete');
}
/**
* Get the browse operation for the current driver.
*/
public function browse(): Browseable
{
return $this->resolve('browse');
}
/**
* Get a vBucket for workspace-isolated storage.
*/
public function vbucket(string $domain): Bunny\VBucket
{
if (! isset($this->drivers[$this->defaultDriver]['vbucket'])) {
throw new InvalidArgumentException(
"vBucket not available for storage driver [{$this->defaultDriver}]."
);
}
$class = $this->drivers[$this->defaultDriver]['vbucket'];
return new $class($domain, zone: $this->zone);
}
/**
* Check if a driver is registered.
*/
public function hasDriver(string $driver): bool
{
return isset($this->drivers[$driver]);
}
/**
* Get all registered driver names.
*
* @return array<string>
*/
public function drivers(): array
{
return array_keys($this->drivers);
}
/**
* Register a custom driver.
*
* @param array{upload?: class-string<Uploadable>, download?: class-string<Downloadable>, delete?: class-string<Deletable>, browse?: class-string<Browseable>, vbucket?: class-string} $operations
*/
public function extend(string $driver, array $operations): self
{
$this->drivers[$driver] = array_merge($this->drivers[$driver] ?? [], $operations);
return $this;
}
/**
* Resolve an operation class for the current driver.
*/
protected function resolve(string $operation): object
{
if (! isset($this->drivers[$this->defaultDriver])) {
throw new InvalidArgumentException("Storage driver [{$this->defaultDriver}] not registered.");
}
if (! isset($this->drivers[$this->defaultDriver][$operation])) {
throw new InvalidArgumentException(
"Operation [{$operation}] not available for storage driver [{$this->defaultDriver}]."
);
}
$class = $this->drivers[$this->defaultDriver][$operation];
return new $class(zone: $this->zone);
}
}