azure-oss/storage
Azure Blob Storage driver for Laravel. Register Azure as a native filesystem disk and use the familiar Storage facade to upload, retrieve, list, and delete blobs, generate URLs, and manage containers—no custom SDK code required.
azure-oss/storage is the meta package for the community PHP SDK for Azure Storage. It is a subtree split of the AzureOss\Storage\Meta component in php-oss-for-azure/azure-storage-php, so requiring it pulls in the Blob, Queue and Common components under one version constraint.
Minimal steps
composer require azure-oss/storageguzzlehttp/psr7 are the path of least resistance; Symfony HttpClient works too. The package is a thin, PSR-friendly layer, not a Guzzle-specific one..env as AZURE_STORAGE_CONNECTION_STRING.use AzureOss\Storage\Blob\BlobServiceClient;
$this->app->singleton(BlobServiceClient::class, fn () => BlobServiceClient::fromConnectionString(
config('services.azure.connection_string')
));
Where to look first
AzureOss\Storage\Blob\BlobServiceClient — entry point for everything blob-related.AzureOss\Storage\Queue\QueueServiceClient — entry point for queues.AzureOss\Storage\Common\* — StorageSharedKeyCredential, SAS credentials, StorageException, API version handling. This is where you go when auth or error handling misbehaves.First use case that pays for itself: moving user uploads out of local disk into a container with a predictable path convention.
$container = $blobServiceClient->getContainerClient('uploads');
$container->createIfNotExists();
$blob = $container->getBlobClient("users/{$userId}/{$filename}");
$blob->upload(fopen($request->file('avatar')->getRealPath(), 'rb'), overwrite: true);
// later
$stream = $blob->downloadStreaming()->getContentAsString();
Wrap, don't sprinkle. Keep container names, path conventions, content types and retry policy inside one AzureStorageService or repository class. Controllers and jobs should ask for storeAvatar(User $u, UploadedFile $f), never build users/{$id}/... strings.
Choose the upload method by payload shape. Strings for small text/JSON, streams or file paths for anything user-generated. Pass an options object for content type, metadata, overwrite and access tier in one place — this is where you set Content-Type and Content-Disposition so browsers preview or download correctly instead of getting application/octet-stream.
Stream downloads for pass-through. downloadStreaming() gives you a PSR-7 stream you can hand to response()->streamDownload() without buffering the whole blob in PHP memory. Reserve downloadToFile() for things you actually need on disk (PDF processing, image resizing), and avoid getContentAsString() on anything you haven't size-checked.
SAS for browser traffic, never the account key. Generate short-lived, narrowly scoped SAS URIs with the builder and return them to the frontend for direct upload/download. The pattern that works well: the browser asks your API for a URI → API checks authorization → API returns a blob-scoped SAS with write/create and a 5–15 minute expiry → browser PUTs straight to Azure. Your PHP workers never touch the bytes.
$sas = $blobClient->generateSasUri(
BlobSasBuilder::new()
->setContainerName('uploads')
->setBlobName($path)
->setPermissions(BlobSasPermissions::parse('rcw'))
->setExpiresOn(new DateTimeImmutable('+10 minutes'))
);
Keep the account-level SAS generator for admin tooling only; it is effectively a password.
Listing with hierarchy. Azure has no real folders, so emulate them with a prefix plus / delimiter and pass the continuation token through to your UI. If your listing method returns a paged result, treat pagination as mandatory, not optional — a container with 100k blobs will happily time out your admin page otherwise.
Queues as the async backbone. createIfNotExists() on deploy, then send a message per unit of work (sendMessage), receive in batches with a visibility timeout, and delete only after the handler succeeds. Track dequeue count for poison messages; Azure redelivers indefinitely otherwise.
Laravel integration — two tiers. For ordinary file operations (put, get, exists, temporaryUrl, delete) register the Flysystem adapter (azure-oss/flysystem-azure-blob-storage) as a disk in config/filesystems.php and use Storage::disk('azure') like any other disk. Drop to the native BlobServiceClient only for what Flysystem doesn't model well: SAS with precise permissions, access tiers, metadata, block-level control, queue operations. Document which tier each feature team should use, or you'll get both idioms in the same controller.
Testing. Put the native client behind your own small interface so feature tests can bind a fake. Keep a separate integration suite that runs against Azurite (Docker) in CI and a dedicated dev storage account nightly — emulator fidelity is good for blobs but not perfect for SAS and tiering.
azure-oss/* packages to the same version. Because the components are subtree splits of one monorepo, mixing azure-oss/storage with a directly required azure-oss/storage-blob at a different release is the fastest route to a confusing class-signature mismatch. Require the meta package only, then composer update azure-oss/* --with-dependencies.StorageException and branch on the HTTP status: 404 (missing container/blob), 409 BlobAlreadyExists (forgot overwrite), 412 PreconditionFailed (ETag/If-Match race — the correct signal for optimistic concurrency), 403 (bad key, expired SAS, clock skew). Never match on message text.file_put_contents-style APIs.rewind($stream)) before any retry, and never reuse one stream handle for two uploads. If you're wrapping calls in a retry middleware, make sure the stream is rewindable (fopen on a file is; a PSR-7 stream from a request body may not be).DateTimeImmutable in UTC, leave a couple of minutes of skew, and remember that URL-encoding a SAS into a query string can silently break it. Prefer generateSasUri() over hand-assembling.UseDevelopmentStorage=true plus an explicit http://127.0.0.1:10000 endpoint and path-style URLs. Keep a separate .env.testing — do not try to make one connection string do both.If-Match).{type, id}) rather than fat payloads.composer.json, and read the changelog before minor bumps. The upside is that the surface is thin PSR-18/PSR-7 code, so replacing it with microsoft/azure-storage-blob later is a contained refactor rather than a rewrite.php-oss-for-azure/azure-storage-php), not the read-only split. File issues with the component name in the title and expect to supply a minimal Azurite repro.How can I help you explore Laravel packages today?