azure-oss/storage-blob
A community-maintained PHP SDK for Azure Blob Storage supporting containers, blobs, leases, tags, versions, and SAS generation, with connection string, shared key, SAS, and Entra ID auth plus Azurite emulator support.
Install
composer require azure-oss/storage-blob
This is a framework-agnostic SDK — it ships no Laravel service provider, facade, or publishable config. Anything Laravel-shaped (config file, singleton binding, Storage::disk() driver) you wire yourself, or you install the sibling package azure-oss/storage-blob-laravel (a Flysystem driver) when you only need Storage::disk('azure')-style put/get/url.
Where to look first
https://php-oss-for-azure.github.io for the full docs.changelog/ + UPGRADE.md before any minor bump; 2.x changed SAS signing, lease headers, and list-response shapes.microsoft/azure-storage-blob.Wire it into the container (once, in AppServiceProvider::register):
$this->app->singleton(BlobServiceClient::class, fn () => BlobServiceClient::fromConnectionString(
config('services.azure.storage_connection_string')
));
// config/services.php
'azure' => [
'storage_connection_string' => env('AZURE_STORAGE_CONNECTION_STRING'),
'container' => env('AZURE_STORAGE_CONTAINER', 'app'),
],
For local dev point the connection string at Azurite (UseDevelopmentStorage=true style) and keep it in .env only.
First use case — accept an upload in a controller and hand back a private download link:
use AzureOss\Storage\Blob\BlobServiceClient;
use AzureOss\Storage\Blob\Models\UploadBlobOptions;
public function store(Request $request, BlobServiceClient $service)
{
$container = $service->getContainerClient(config('services.azure.container'));
$container->createIfNotExists();
$path = 'uploads/'.Str::uuid().'.jpg';
$container->getBlobClient($path)->upload(
$request->file('photo')->get(),
new UploadBlobOptions(contentType: $request->file('photo')->getMimeType())
);
return response()->json(['path' => $path]);
}
Then serve it without making the container public by generating a SAS — see Implementation Patterns.
Use the SDK directly for anything Flysystem can't express. A sane split: azure-oss/storage-blob-laravel for put/get/delete/url, raw BlobServiceClient for leases, SAS, snapshots, versions, tags, and conditional writes. Both read the same connection string, so keep exactly one config key.
Upload streams, not strings, for anything user-supplied.
$blob->upload(
fopen($request->file('video')->getRealPath(), 'r'),
new UploadBlobOptions(
contentType: 'video/mp4',
metadata: ['user_id' => (string) $user->id],
tags: ['state' => 'pending'],
)
);
UploadBlobOptions is where you tune transfer behaviour (block size / concurrency) for large objects. Never file_get_contents() a 2 GB file to build the payload.
Stream downloads straight into the response.
$download = $blob->downloadStreaming();
return response()->streamDownload(
fn () => fpassthru($download->content->detach()),
'report.pdf',
['Content-Type' => $download->contentType ?? 'application/octet-stream']
);
content is a PSR-7 stream — detach() hands it to the output buffer with no full copy in memory. Reserve getContents() for small config/JSON blobs.
List lazily, with prefixes, not by fetching everything.
foreach ($container->getBlobsByHierarchy(prefix: "users/{$id}/") as $item) { ... }
getBlobs() / getBlobsByHierarchy() return a paged iterator; maxResults is a page size, not a cap. Pass BlobInclude when the list response must carry metadata, tags, snapshots, versions, or deletion state — otherwise you get name/size only and make N follow-up HEAD calls.
Optimistic concurrency with ETags for read-modify-write blobs (feature flags, small JSON documents):
$props = $blob->getProperties();
try {
$blob->upload($newJson, new UploadBlobOptions(ifMatch: $props->etag));
} catch (BlobStorageException $e) {
if ($e->errorCode === BlobErrorCode::ConditionNotMet) { /* re-read and retry */ }
}
Leases for single-writer work — a queued job that must not double-process: BlobLeaseClient acquire/renew/release/break, and pass the lease ID on writes. Treat lease duration as shorter than your job timeout and renew in a heartbeat.
SAS for private delivery. Generation goes through BlobSasBuilder (permissions, start/expiry, resource), not a one-liner on the blob client. Build a fresh builder per URL — reusing one instance leaked blob-scoped fields into later container SAS calls (fixed in 2.2.1). Catch UnableToGenerateSasException and fail loudly rather than emitting a URL with a null expiry.
Use SAS URLs for <img>/<video> tags and queued emails; use Storage::disk('azure')->temporaryUrl() if you've installed the Flysystem driver and it covers your case.
Snapshots and versions for audit trails: createSnapshot(), then withSnapshot($id) / withVersion($id) to read immutably; undelete() plus DeleteSnapshotsOption to restore or purge soft-deleted data. Wire this into a scheduled command with retention rules enforced in code.
Tags as a work queue / cleanup index. setTags() at upload, then query by tag at account or container scope from a scheduled command (state=pending older than 24h → delete). Cheaper than listing and HEAD-ing every blob; watch BlobErrorCode::TagsTooLarge (64 tags / 4 KB per blob).
Async for fan-out. *Async() methods return Guzzle promises declaring their resolved types (2.3.0+), so use them in jobs that upload many parts/files — settle a pool and let one failure surface, instead of serial wait() calls.
Queue the heavy paths. Put uploads/downloads-to-disk in a job with $tries and a deterministic blob name so retries overwrite rather than duplicate. IfNoneMatch: '*' makes the write idempotent-safe.
AzureStorageServiceProvider, vendor:publish, $blob->generateSasToken(...), setBlobServiceProperties, or DeleteRetentionPolicy — none of those exist in this package. SAS is BlobSasBuilder; lifecycle/soft-delete policy is an Azure portal/ARM concern, not this SDK's. Verify every call against the README or the docs site.>=2.2.1. 2.2.1 fixed SAS state leaking between a reused builder's blob and container calls, and replaced raw typed-property errors with UnableToGenerateSasException. 2.3.0 added Guzzle 8; if your app's guzzlehttp/guzzle constraint is ^7.0, widen it to ^7.0|^8.0 or stay on 2.2.x deliberately.config:cache + env(): only read the connection string through config(). Calling env() at runtime returns null in production.azure-oss/identity + DefaultAzureCredential; connection strings are account keys with full access.now()->utc() (or the shared date helper the SDK expects) — a config/app.php timezone of Europe/Amsterdam plus naive now() silently produces expired or long-lived tokens.BlobStorageException + BlobErrorCode (BlobNotFound, ContainerAlreadyExists, BlobAlreadyExists, LeaseIdMismatch, TagsTooLarge). Prefer *IfExists / createIfNotExists over try/catch to avoid create races.getBlobs() pages behind the scenes but each page holds full models; don't accumulate them in an array for a million-blob container. And never buffer a download you intend to stream.finally — leaked handles in a resident worker will exhaust FDs.Storage::fake() equivalent. Wrap the client behind a small BlobStorage interface, bind a fake in tests, and keep one Azurite-backed integration suite that creates a uniquely-named container per test and deletes it in tearDown. Storage::fake() on the Flysystem driver does not touch Azure either — it swaps the adapter.How can I help you explore Laravel packages today?