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 Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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

  1. The README Quickstart — it is the only guaranteed-correct API sample.
  2. https://php-oss-for-azure.github.io for the full docs.
  3. changelog/ + UPGRADE.md before any minor bump; 2.x changed SAS signing, lease headers, and list-response shapes.
  4. The migration guide if you are porting from 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.

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.

Gotchas and Tips

  • Ignore Laravel-looking snippets that mention 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.
  • Pin >=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.
  • Lock the Storage API version per client via the client options. It propagates to children, and it changes SAS defaults — a container client created without it can sign a SAS the blob client then rejects with 400.
  • config:cache + env(): only read the connection string through config(). Calling env() at runtime returns null in production.
  • Never commit the connection string. In production prefer Entra ID via azure-oss/identity + DefaultAzureCredential; connection strings are account keys with full access.
  • Azurite is not Azure. Path-style URLs and a default account mean container names must be lowercase, 3–63 chars, alphanumeric + hyphens. Soft delete, versions, and index tags are unreliable or unsupported locally — test those against a real storage account in a throwaway container.
  • Clock skew and timezones: SAS start/expiry are UTC. Use 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.
  • Don't match on exception strings. Branch on BlobStorageException + BlobErrorCode (BlobNotFound, ContainerAlreadyExists, BlobAlreadyExists, LeaseIdMismatch, TagsTooLarge). Prefer *IfExists / createIfNotExists over try/catch to avoid create races.
  • Memory: iterating 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.
  • Octane / long-lived workers: the SDK client is safe to reuse as a singleton (Guzzle pools connections), but always close detached streams in finally — leaked handles in a resident worker will exhaust FDs.
  • Testing: there is no 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.
  • Extension points are thin by design: a custom Guzzle client/handler (retries, logging middleware, tracing) and a custom credential are the two real seams. Put retry/backoff for 500/503 there rather than in every call site.
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.
gmostafa/php-graphql-client
xeviant/paystack
snowsoft/faker
meditationsteps/blogetc
animalyoung/laravel-influxdb
onix-solutions/sms-telera
lonely-lights/prosetta
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log