Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Azure Blob Storage Laravel Package

steffjenl/laravel-azure-blob-storage

Laravel filesystem driver for Azure Blob Storage, letting you store, retrieve, and manage files on Azure directly through Laravel's familiar Storage facade and Flysystem integration.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Confirm platform fit before installing. The package's last release (v1.2.0) requires PHP ≥ 8.0.2 and was written against an older Flysystem generation. Laravel 9+ ships Flysystem 3. If your app is on Laravel 9/10/11, run composer require steffjenl/laravel-azure-blob-storage in a branch and immediately check what Flysystem version Composer resolved. If it tries to pull league/flysystem ^1.0 or ^2.0 alongside Laravel's ^3.0, you have a conflict and should stop here — see the alternative in Gotchas.

2. Install and let auto-discovery wire it up.

composer require steffjenl/laravel-azure-blob-storage

The service provider registers a custom filesystem driver (typically named azure) via Storage::extend(). If you're on an older Laravel without auto-discovery, add the provider to config/app.php.

3. Add a disk to config/filesystems.php. The exact key names come from the package README — verify them, they differ between this package and its ancestors (some use name/key, some account_name/account_key, and some support connection_string). A representative block:

'azure' => [
    'driver'    => 'azure',
    'name'      => env('AZURE_STORAGE_NAME'),
    'key'       => env('AZURE_STORAGE_KEY'),
    'container' => env('AZURE_STORAGE_CONTAINER'),
    'prefix'    => '',
],

4. Populate .env. Quote any value containing ;, =, or + — base64 account keys and full connection strings are the #1 source of "403 AuthenticationFailed" in .env files:

AZURE_STORAGE_NAME=myaccount
AZURE_STORAGE_KEY="base64key=="
AZURE_STORAGE_CONTAINER=uploads

5. First smoke test in php artisan tinker:

Storage::disk('azure')->put('healthcheck.txt', 'ok');
Storage::disk('azure')->get('healthcheck.txt');   // "ok"
Storage::disk('azure')->exists('healthcheck.txt'); // true
Storage::disk('azure')->delete('healthcheck.txt');

If that round-trips, everything else is just the standard Storage API. First real use case: move user uploads (avatars, documents, invoices) off the local disk and swap Storage::disk('local') for Storage::disk('azure') behind a config value so local dev keeps working.

Implementation Patterns

Bind the disk name once, not everywhere. Hardcoding 'azure' across the codebase makes local dev and multi-tenant setups painful. Either set FILESYSTEM_DISK=azure in production and use the default Storage::put(...), or introduce a tiny resolver:

// config/filesystems.php
'uploads' => [
    'driver' => env('UPLOADS_DRIVER', 'local'), // 'azure' in prod
    // ...
],

Then the entire app calls Storage::disk('uploads') and only the config changes between environments.

Stream, don't buffer. The get()/put() helpers load the whole blob into PHP memory. For anything user-uploaded, use streams:

// Upload
Storage::disk('azure')->putFileAs(
    'invoices/2024',
    $request->file('invoice'),
    $invoice->id . '.pdf'
);

// Download
return response()->streamDownload(function () use ($path) {
    $stream = Storage::disk('azure')->readStream($path);
    fpassthru($stream);
    if (is_resource($stream)) fclose($stream);
}, basename($path));

Store relative paths in the database, never URLs. URLs are ephemeral and differ between the emulator, a public container, and a SAS-signed private container. Persist invoices/2024/123.pdf and generate the URL at render time.

Private container access pattern. Blob containers default to private. Either:

  • Serve through your app (streamDownload above, gated by a policy), or
  • Generate a SAS URL. If the package doesn't implement temporaryUrl(), build the SAS yourself with microsoft/azure-storage-blob (already a transitive dependency) and cache the signed URL for the token lifetime rather than regenerating per request.

Tenant / per-container isolation. The disk config is static, so for per-tenant containers register additional disks at runtime — but use a unique disk name per tenant. FilesystemManager caches resolved disks by name, so re-configuring filesystems.disks.azure and calling Storage::disk('azure') again returns the old instance. Either build the config array once in a service provider before first use, or call Storage::forgetDisk('azure') after mutating config.

Testing. Storage::fake('azure') works even though the real driver is remote — the fake swaps in a local temp disk, so tests never touch Azure and stay fast. Do it in setUp():

protected function setUp(): void
{
    parent::setUp();
    Storage::fake('uploads');
}

Keep one or two opt-in integration tests (tagged @group azure) that hit a real container or Azurite, because the fake will happily accept paths that Azure would reject (uppercase container names, backslashes, 4096-char keys).

Asset pipeline / backup pattern. Backups and generated exports are a great low-risk first migration: write to local, then Storage::disk('azure')->putFileAs(...), then delete the local copy. It gives you a rollback path if the driver misbehaves.

Gotchas and Tips

The Flysystem generation gap is the real risk. Laravel 9+ requires league/flysystem ^3.0; adapters written for Flysystem 1/2 are incompatible at the method-signature level. Since this package's last release was 2023 and its stated support is "Laravel 5+", verify on your actual Laravel version before committing. If it doesn't fit, the maintained alternative is league/flysystem-azure-blob-storage (official, Flysystem v3) registered as a custom driver in a small service provider — same Storage API, no behavioural surprises.

404 / 403 triage table. Nearly every failure maps to one of these:

  • ContainerNotFound (404) → wrong container name, or it was never created. Blob storage does not auto-create containers; create it in a setup command or via Bicep/Terraform.
  • AuthenticationFailed (403) → key mangled by .env quoting, or clock skew.
  • AuthorizationPermissionMismatch (403) → you're using an AAD/service-principal identity without the Storage Blob Data Contributor role (the control-plane role is not enough), or the SAS is scoped to the wrong container.
  • InvalidResourceName (400) → path contains \, a leading /, or a container name with uppercase/underscores.

Container names are stricter than local paths. Lowercase, 3–63 chars, alphanumeric and hyphens only, must start/end alphanumeric. A test that passes on the local driver with Uploads/Invoices will fail on Azure.

Directories are virtual. There's no mkdir. directoryExists() is really "does any blob exist under this prefix", and deleteDirectory() is a recursive list-and-delete — on a container with 100k blobs that's slow and billed per operation. Don't put allFiles() or deleteDirectory() on request paths; move them to queued jobs.

Listing operations cost money and time. Every allFiles() is a paginated LIST call. Cache directory listings, or — better — mirror the metadata you need into your database at upload time (size, mime, uploaded_at) so the UI never lists the container.

url() behaviour depends on container ACL. Storage::disk('azure')->url($path) typically returns https://{account}.blob.core.windows.net/{container}/{path}, which 404s for private containers and will work for public ones. If you make the container public for convenience, remember anyone with the URL can read every blob — fine for CSS/JS assets, wrong for invoices.

Config caching bites. php artisan config:cache freezes disk config. Runtime disk registration (multi-tenant, dynamic containers) must run in a service provider after config is loaded and must not depend on .env values read at runtime — env() outside config files returns null once cached.

Local development with Azurite. Run azurite --silent --location ./azurite and point the disk at http://127.0.0.1:10000/devstoreaccount1 with the well-known dev key (or set UseDevelopmentStorage=true if the driver accepts a connection string). This is the only way to exercise real driver behaviour in CI without cloud credentials.

Timeouts and retries. The underlying SDK has a default request timeout; for large uploads over slow links, raise it and wrap in retry(). Note that put() is generally safe to retry (idempotent overwrite), but copy and multipart flows are not — don't blanket-retry everything.

Extension points are minimal — expect to fork. There's no published config file or event layer. If you need custom behaviour (metadata tags, tiering to Cool/Archive, content-type overrides), reach for the underlying BlobRestProxy directly: Storage::disk('azure')->getDriver() (or the package's adapter accessor) exposes the client, and you can call createBlockBlob/setBlobProperties yourself. Wrap that in a repository class so the dependency is in one place if you later switch to the official Flysystem adapter.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings