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

Storage Blob Laravel Laravel Package

azure-oss/storage-blob-laravel

Azure Blob Storage filesystem driver for Laravel. Register an Azure container as a disk and use Laravel's Storage facade to upload, download, list, delete files, and generate URLs with familiar APIs—no custom SDK code required.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire it up

composer require azure-oss/storage-blob-laravel

The service provider is auto-discovered. If you're on an older Laravel or disabled discovery, register AzureOss\StorageBlobLaravel\AzureStorageBlobServiceProvider manually in config/app.php.

Add a disk in config/filesystems.php:

'disks' => [
    'azure' => [
        'driver'            => 'azure-blob',
        'connection_string' => env('AZURE_STORAGE_CONNECTION_STRING'),
        'container'         => env('AZURE_STORAGE_CONTAINER'),
    ],
],
AZURE_STORAGE_CONNECTION_STRING="DefaultEndpointsProtocol=https;AccountName=myaccount;AccountKey=...;EndpointSuffix=core.windows.net"
AZURE_STORAGE_CONTAINER=uploads

Some versions also accept account_name / account_key / endpoint_suffix instead of a connection string — check vendor/azure-oss/storage-blob-laravel/src/ before you copy config from a blog post, since this is a small package and the key names have shifted between releases.

Where to look first

  1. config/filesystems.php — the only place most people ever edit.
  2. vendor/azure-oss/storage-blob-laravel/src/ — one service provider plus a filesystem adapter. Read the adapter class end-to-end; it's small, and it tells you exactly which FilesystemAdapter methods are implemented versus inherited as no-ops.
  3. The underlying SDK (microsoft/azure-storage-blob) — this package is a thin translation layer over BlobRestProxy / BlobClient. When something behaves oddly, the answer is almost always in the SDK docs, not the Laravel wrapper.

First use case: offloading user uploads

The lowest-friction win is moving generated files and uploads off the app server:

use Illuminate\Support\Facades\Storage;

Storage::disk('azure')->put('exports/2026-09/report.csv', $csv);
$url = Storage::disk('azure')->url('exports/2026-09/report.csv');

Pre-create the container before your first request — either in the portal, or:

az storage container create --name uploads --account-name myaccount

Then verify with Storage::disk('azure')->exists('exports/2026-09/report.csv') in php artisan tinker. If that round-trips, the rest is standard Laravel filesystem work.

Implementation Patterns

Drive everything off the disk name, not a hardcoded adapter

The main value of this package is that it is not special. Keep code written against the Storage facade and select the driver in config:

// config/filesystems.php
'assets' => [
    'driver' => env('ASSETS_DRIVER', 'local'),
    // ...
],

This lets you run local in dev/CI and azure-blob in production, and it makes Storage::fake('assets') in tests a drop-in swap.

Request uploads

$path = $request->file('avatar')->store('avatars/'.auth()->id(), 'azure');
// or with a controlled name
$request->file('avatar')->storeAs('avatars/'.auth()->id(), $filename, 'azure');

store() returns the object key. Persist the key, not the URL — you can always regenerate a URL, but you can't recover a key from a private-container URL cleanly.

Streaming instead of buffering

$handle = fopen($localPath, 'r');
Storage::disk('azure')->writeStream('imports/big.csv', $handle);
fclose($handle);

For anything over a few hundred MB, prefer streams or the SDK's block-blob upload path over put() with a string. A single-shot put() of a multi-GB file is where timeouts and memory exhaustion show up.

Reads, copies, moves

$contents = Storage::disk('azure')->get('imports/big.csv');
Storage::disk('azure')->copy('a/report.pdf', 'archive/report.pdf');
Storage::disk('azure')->move('inbox/x.csv', 'processed/x.csv');

copy and move map to Azure server-side copy, which is asynchronous at the service level. Confirm in the adapter whether it polls or returns immediately; if it returns immediately, don't chain a read of the destination in the same request/job.

Directory conventions

Azure Blob has a flat namespace. The adapter synthesizes folders from / in the key and by writing zero-byte marker blobs for makeDirectory. Adopt a key convention once and enforce it:

$prefix = "tenants/{$tenantId}/documents/{$documentId}";
Storage::disk('azure')->put("{$prefix}/original.pdf", $bytes);

Per-tenant or per-environment containers

$disk = Storage::build([
    'driver'            => 'azure-blob',
    'connection_string' => config('services.azure.connection_string'),
    'container'         => "tenant-{$tenant->id}",
]);

$disk->put('logo.png', $bytes);

Storage::build() gives you dynamic disks without touching config. Cache the built disk if you create it per request in a hot path.

Presigned URLs for private containers

$url = Storage::disk('azure')->temporaryUrl(
    'reports/q3.pdf',
    now()->addMinutes(15),
);

If the adapter version you're on doesn't implement temporaryUrl, generate a SAS directly with the SDK's BlobSharedAccessSignatureHelper from your account key, or route through a controller that streams via response()->streamDownload(fn () => readfile(...)).

Queue jobs and cleanup

class ProcessUpload implements ShouldQueue
{
    public function handle(): void
    {
        $disk = Storage::disk('azure');
        $disk->put('processing/'.$this->id, $payload);

        try {
            // ...
        } finally {
            $disk->delete('processing/'.$this->id);
        }
    }
}

Wrap writes that matter in retry(3, fn () => $disk->put(...), 200) — the underlying SDK doesn't give you Laravel-style retry semantics, and transient 5xx/timeouts do happen.

Testing

Storage::fake('azure') works because Laravel swaps in a local adapter regardless of the configured driver. Use it for 99% of tests, and keep one opt-in integration test against Azurite:

docker run -d -p 10000:10000 mcr.microsoft.com/azure-storage/azurite \
  azurite-blob --blobHost 0.0.0.0
AZURE_STORAGE_CONNECTION_STRING="DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;"

Fakes do not exercise container ACLs, async copy, or SAS signing — those are exactly the things that break in production.

Gotchas and Tips

The driver string is azure-blob, not azure

'driver' => 'azure-blob' must match the key the service provider registers. A typo produces a confusing "Driver [azure] is not supported" or a silent fallback to a local disk. Grep the service provider for the exact string.

Containers are not auto-created

Unlike S3-backed Laravel setups where the bucket often exists already, a missing container throws ServiceException with ContainerNotFound. Either provision containers in your deploy script/IaC, or catch it at boot:

try {
    Storage::disk('azure')->put('.probe', '');
    Storage::disk('azure')->delete('.probe');
} catch (\MicrosoftAzure\Storage\Common\Exceptions\ServiceException $e) {
    if ($e->getErrorCode() === 'ContainerNotFound') {
        // create via SDK or fail loudly
    }
}

url() on a private container gives you a 403-generating string

Azure has no per-blob ACL. Public access is a container-level setting. So:

  • url() is only correct for containers configured for public/blob access.
  • setVisibility($path, 'public') cannot make a single blob public — if the adapter implements it, it's changing container ACL or silently doing nothing. Don't design around per-file visibility.
  • For private containers always use temporaryUrl() and set a short TTL.

files() / directories() are the weak spot

Implementations typically use delimiter-based listing. Watch for:

  • Pagination: Azure returns up to 5,000 entries per page. If the adapter doesn't loop continuationToken, allFiles() silently truncates on large containers. Confirm in the adapter source before relying on it for a full export.
  • Empty folders: a "directory" only exists if a marker blob does. directories() will miss prefixes that contain only deeper keys, depending on implementation.
  • deleteDirectory() may delete only marker blobs and leave the actual objects. Test this explicitly with a nested prefix.

Paths with spaces and special characters

Blob names are URL-encoded on the wire. url() and temporaryUrl() can disagree about encoding for keys containing spaces, #, +, or non-ASCII. Stick to [a-z0-9/._-] in generated keys and you'll never think about this.

Case sensitivity bites late

Blob names and container names are case-sensitive. A macOS/Windows dev machine and a case-insensitive test setup will hide bugs that appear on Linux and in Azure. Add an assertion on stored keys in tests rather than trusting the round-trip.

Errors are SDK errors, not Laravel errors

You'll catch MicrosoftAzure\Storage\Common\Exceptions\ServiceException — inspect getErrorCode() (BlobNotFound, AuthenticationFailed, ContainerNotFound, AuthorizationPermissionMismatch) and getErrorText(). Laravel's UnableToReadFile / UnableToWriteFile may or may not be wrapped around it depending on the adapter version; catch both in job code.

Listing metadata is N+1

size(), lastModified(), and mimeType() each cost a getBlobProperties round trip. If you render a file browser with 50 rows × 3 calls, you've added 150 requests of latency. Either fetch properties once via a single listing call with metadata included (SDK listBlobs with include=metadata), or cache the results.

No connection pooling / retry middleware from the SDK

microsoft/azure-storage-blob predates the modern Azure SDK and has no built-in retry policy. Add your own for idempotent operations, and if the disk config forwards Guzzle options, set sane connect_timeout and timeout values. Uploads that take longer than your default PHP max_execution_time belong in a queue.

Configuration and secrets

  • php artisan config:cache freezes env values; re-run it after changing AZURE_STORAGE_*.
  • Keep the connection string out of config/filesystems.php literals and out of version control; env() in config is fine.
  • The connection-string model assumes account key auth. SAS tokens, Azure AD/managed identity, and workload identity are not covered by the package's standard config — for those, register your own driver:
// AppServiceProvider::boot()
Storage::extend('azure-blob', function ($app, $config) {
    // build your own adapter with a SAS-bearing BlobClient, then wrap it
    return new YourAzureAdapter(...);
});

Version risk is real

67 stars, zero dependents, single-maintainer project. Pin it in composer.json ("azure-oss/storage-blob-laravel": "^1.0") and keep your own integration test against Azurite so a composer update or an upstream SDK deprecation doesn't take you by surprise. If the package stalls, the escape hatch is straightforward: it's a thin adapter, and porting your usage to League\Flysystem\AzureBlobStorage or a hand-rolled FilesystemAdapter over the newer Azure\Storage\Blob\BlobServiceClient is a day's work, not a rewrite.

Extension points worth knowing

  • Storage::extend('azure-blob', ...) — wrap the adapter for logging, metrics, key prefixing, or swapping in a newer SDK.
  • Storage::build([...]) — dynamic per-tenant disks without config edits.
  • The adapter's getDriver() / client accessor (if present) — reach for the raw BlobRestProxy/BlobClient when you need features the Laravel contract doesn't expose: container ACLs, blob tiers (Hot/Cool/Archive), lifecycle rules, or listBlobs with metadata.
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.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource