academe/laravel-azure-file-storage-driver
A Laravel filesystem driver that connects Azure File Storage to Laravel's Storage facade. Add an Azure disk to your config and read, write, and delete files through the same familiar API you use for local and S3 storage.
composer require academe/laravel-azure-file-storage-driver. Before you commit, run composer why-not academe/laravel-azure-file-storage-driver if you're on Laravel 10/11 — the last tagged release is from mid‑2022, so Illuminate/Flysystem constraints may need review (see Gotchas).Storage::extend(...) (or the equivalent Flysystem adapter registration) to attach a driver name — typically azure-file — to Laravel's FilesystemManager.config/filesystems.php:'azure-files' => [
'driver' => 'azure-file', // confirm exact string in the package's provider/README
'name' => env('AZURE_FILE_ACCOUNT'),
'key' => env('AZURE_FILE_KEY'),
'container'=> env('AZURE_FILE_SHARE'), // this is an Azure File *share*, not a blob container
'endpoint' => env('AZURE_FILE_ENDPOINT', 'https://'.env('AZURE_FILE_ACCOUNT').'.file.core.windows.net'),
'url' => env('AZURE_FILE_URL'), // optional; enables Storage::url()
],
.env only. Prefer a storage-account key or a SAS token scoped to the share. Create the share first (az storage share create --name my-share --account-name ... or via Portal/Bicep) — drivers like this manage files and directories inside a share; they do not provision the share itself.php artisan tinker,Storage::disk('azure-files')->put('invoices/2024/001.pdf', $pdf);
Storage::disk('azure-files')->exists('invoices/2024/001.pdf'); // true
Storage::disk('azure-files')->files('invoices/2024');
academe/flysystem-azure-file-storage) for the real semantics of temporaryUrl, visibility, and directory listing. The Laravel layer is thin; the adapter is where behavior lives.Storage::disk('azure-files'): put, putFile, putFileAs, get, getStream, exists, size, lastModified, copy, move, delete, makeDirectory, deleteDirectory, files, allFiles, directories. No Azure-specific code should leak into business logic — that's the point of the driver.$path = $request->file('invoice')->store('invoices/'.$tenantId, 'azure-files');
Invoice::create(['path' => $path]);
put()/get() load the whole payload into memory. For anything above a few MB, use writeStream / readStream / Storage::disk(...)->getDriver()->readStream($path) and return it from a controller with response()->stream(...). This also lets Azure handle the transfer and avoids PHP memory limits.temporaryUrl() is supported by your adapter version, use it with a short expiry (Storage::disk('azure-files')->temporaryUrl($path, now()->addMinutes(10))) — it returns a SAS-signed URL. If not, stream through a controller after your own auth check, or generate a SAS manually with the Azure SDK.root. If several apps share one share, set 'root' => 'app-name' on each disk so paths can't collide, and keep path building ("invoices/{$id}.pdf") relative to that root.Storage::build([...]) (Laravel 9+) or config()->set('filesystems.disks.tenant', [...]) from a service provider or middleware. This is also how you switch driver by environment: local in dev, Azure in staging/prod, behind one Storage::disk(config('filesystems.default')) call..env credentials. Add a retry policy for 5xx/ServiceException responses ($this->release(30)) so transient Azure hiccups don't fail the job.Storage::disk(...)->move('a/x.pdf', 'b/x.pdf') is a metadata/copy operation against Azure; use it freely inside one disk. Across shares/accounts it won't work — download + upload.allFiles(...) and prunes on lastModified() older than N days. Beware: listing is per-directory and can be slow on large shares; keep a prefix scheme (tmp/) to limit scope.Storage::fake('azure-files') swaps in a local temp disk and keeps all assertions (assertExists, assertMissing) working — use it for feature tests. Keep one opt-in integration test against a real or emulated share (gated on env vars) to catch driver-specific issues like path encoding and temporaryUrl.visibility('public'), direct url() — do not apply. If you actually want Blob, use league/flysystem-azure-blob-storage instead. This is the single most common misconfiguration.visibility()/setVisibility() to either do nothing or throw UnableToSetVisibility; don't build authorization on top of them." \ / : | < > * ? plus control chars are invalid (also be careful with % and # in URLs). Sanitize user-supplied filenames before they reach the disk.Logo.png and logo.png are the same file — two uploads will silently overwrite. Normalize paths in your app if uniqueness matters.lastModified() returns a Unix timestamp or a Carbon depending on the Flysystem version. Normalize in one place rather than sprinkling Carbon::createFromTimestamp everywhere.409 Conflict or 423 Locked. Wrap writes/deletes in a small retry with backoff; log $e->getErrorCode() from the underlying ServiceException. A 409 can also mean the parent directory doesn't exist — call makeDirectory or put with a path whose ancestors already exist.UnableToWriteFile, UnableToReadFile) wrapping MicrosoftAzure\Storage\Common\Exceptions\ServiceException. Catch the Flysystem type in app code, and dig into getErrorCode() only in logs.allFiles() recurses — one request per directory. On deep trees this is slow and bills per transaction. Prefer files($prefix) with a sharded prefix scheme, and cache listings for read-heavy screens (Laravel 9+'s 'cache' disk option may not be honored by this older adapter — verify).url must be configured. If url is missing in the disk config, Storage::url() returns a bare path, not a usable link. For anything user-facing, rely on temporaryUrl() or a controller route and test that the SAS actually works from a browser.composer.json, and budget time for a fork if microsoft/azure-storage-file (or the maintainer's fork) trips PHP 8.1+ deprecations or an Illuminate constraint. Run a smoke test in the target Laravel version before architecture decisions depend on it.php artisan tinker and a single put/get. If that fails, check (in order): driver name typo, share exists, credentials/permissions (403 vs 404 tells you which), endpoint for sovereign clouds (*.file.core.chinacloudapi.cn, etc.), then path characters. If you need request-level logs, you'll have to wire a Guzzle handler into the SDK client yourself — the adapter usually doesn't expose middleware hooks.php artisan config:cache after every filesystems.php or .env change; env() called outside config files returns null in cached deployments and is a common "works on my machine" failure.Storage::extend, you can re-register your own driver string in a later provider, or subclass the adapter and override temporaryUrl/path mapping. That's the cleanest escape hatch if the package's URL or visibility behavior doesn't match your needs.How can I help you explore Laravel packages today?