beberlei/azure-blob-storage
A lightweight PHP client for Azure Blob Storage that lets you create and manage containers and blobs, upload and download data, configure metadata and access policies, and interact with the Azure REST API through a simple object-oriented interface.
Install and configure
composer require beberlei/azure-blob-storage
You need three values from the Azure portal (Storage account → Access keys): the account name, the primary/secondary key, and the blob endpoint (https://<account>.blob.core.windows.net, or a sovereign-cloud/Azurite endpoint). Put them in .env and expose them through a config file — never inline the key.
Look here first
AzureBlobStorage service object and the AzureBlobStorageStreamWrapper.src/AzureBlobStorage.php — scan the public methods (createContainer, deleteContainer, listContainers, putBlob, getBlob, deleteBlob, listBlobs) and note exactly what each returns on failure, because error handling is inconsistent between methods.src/AzureBlobStorageStreamWrapper.php — check which context options it reads (account_name, account_key, possibly container) and which stream operations are actually implemented.First use case: upload a file and read it back
$storage = new AzureBlobStorage($account, $key); // plus host argument if your release supports it
$storage->createContainer('uploads', false); // ignore 409 if it already exists
$storage->putBlob('uploads', 'avatars/42.png', $binary, 'image/png');
$bytes = $storage->getBlob('uploads', 'avatars/42.png');
That is the whole happy path. Everything else in this assessment is about making that path reliable in a Laravel app.
Bind it once in a service provider. Do not construct AzureBlobStorage inside controllers. Register a singleton in AppServiceProvider (or a dedicated AzureStorageServiceProvider) that reads from config/azure.php:
$this->app->singleton(AzureBlobStorage::class, fn () => new AzureBlobStorage(
config('azure.account'),
config('azure.key'),
config('azure.host') // omit if your version has no host argument
));
Hide it behind a small interface. The package is old and unmaintained; wrapping it in App\Contracts\BlobStore (put, get, delete, exists, url) lets you swap in microsoft/azure-storage-blob or a Flysystem adapter later without touching business code. It also gives you a clean fake for tests.
Upload pattern with stable naming. Never use the user's original filename as the blob name. Generate sprintf('%s/%s', $folder, (string) Str::ulid()), keep the original name and MIME type in metadata or in your database, and always pass an explicit content type to putBlob — otherwise downloads come back as application/octet-stream.
Public vs private containers. For static assets (logos, avatars) use a public container and store the constructed URL (https://<account>.blob.core.windows.net/<container>/<blob>) in the DB. For anything sensitive, keep the container private and stream through a controller that checks authorization and forwards Content-Type/Content-Disposition. This package has no SAS generation helper, so either build the shared-access signature yourself or proxy the bytes.
Queue the slow path. Uploads from web requests to Azure are network-bound. Push them to a job, and wrap the call in your own retry loop with exponential backoff — the library has no retry logic at all.
Use the stream wrapper for legacy code. Register it once, in a service provider's boot():
stream_wrapper_register('azure', AzureBlobStorageStreamWrapper::class);
Then code that expects local files (file_get_contents, file_put_contents, copy, unlink, GD/Imagick input) works with azure://container/path/to/blob. Pass per-call credentials via stream_context_create(['azure' => ['account_name' => ..., 'account_key' => ...]]). This is the fastest way to migrate an existing "save uploads to storage/app" feature without rewriting it.
List with prefixes, not scans. Treat Azure's flat namespace as a hierarchy: listBlobs('uploads', 'avatars/') is your directory listing. Pagination in this library is basic — if your release returns everything in one call, be careful with containers holding tens of thousands of blobs.
Local development with Azurite. Point the host at http://127.0.0.1:10000/devstoreaccount1 and use the well-known emulator key. This is the difference between a test suite that hits real Azure on every CI run and one that runs offline. If your pinned release hardcodes blob.core.windows.net, you'll need to fork or subclass it.
Testing. Bind a filesystem-backed fake to BlobStore in TestCase::setUp(). Only a thin integration test should touch Azurite.
composer.json, and budget a migration to microsoft/azure-storage-blob or the Flysystem Azure adapter for new work.Put Blob request, which Azure limits (currently 64 MiB, historically 64 MB). There is no block-level chunking. Larger files will fail — you must either split them yourself or switch SDKs. Related: getBlob returns the whole body as a string, so it will blow your memory limit on large blobs.Date header, so a server clock more than ~15 minutes off from Azure produces 403 AuthenticationFailed — check NTP on the box before debugging your key.http://; override the host with https://... explicitly in production, or your account key travels in plaintext.stream_wrapper_register('azure', ...) emits a warning and returns false. Do it in boot(), guard with in_array('azure', stream_get_wrappers()), and remember the wrapper is global state — in long-running workers (Octane, queue workers) treat its config as immutable.'a') is generally not supported, rename() may degrade to copy-then-delete or fail outright, stat() returns synthetic sizes, and realpath() behavior is odd. Anything using flock() or partial writes will misbehave. Use it for whole-file read/write only.500/503/408 from Azure — and they happen — will surface as exceptions. Wrap every call in a retry with jitter and cap.false returns. Read each method in src/ before depending on it. On failure, log x-ms-request-id and x-ms-error-code from the response; Azure support will ask for them, and they are the only way to correlate a failure with the portal's metrics.createContainer returns 409 if it already exists. Decide whether that is an error in your code path; in provisioning logic it usually isn't.\, and characters that need double URL-encoding, or you'll chase 404s that look like permission bugs.Content-Type set as metadata instead of the dedicated header is a classic mistake: pick putBlob's content-type argument, not ['Content-Type' => ...].AzureBlobStorage and override its internal HTTP sending method (or route through a local proxy) rather than guessing from the exception message. Nine times out of ten a 403 is a signature/Date problem and a 404 is a container that was never created in the target environment.listBlobs makes that trivial.How can I help you explore Laravel packages today?