joshua-barbosa / azure-blob-sdk
SDK PHP para o Azure Blob Storage, integrado ao Laravel: API fluente, driver de Storage, comandos Artisan e múltiplas conexões (SAS URL, connection string ou conta+chave).
Fund package maintenance!
Requires
- php: ^8.0
- ext-hash: *
- ext-json: *
- ext-simplexml: *
- guzzlehttp/guzzle: ^7.5
- guzzlehttp/psr7: ^2.4
- illuminate/console: ^8.0|^9.0|^10.0|^11.0|^12.0|^13.0
- illuminate/contracts: ^8.0|^9.0|^10.0|^11.0|^12.0|^13.0
- illuminate/filesystem: ^8.0|^9.0|^10.0|^11.0|^12.0|^13.0
- illuminate/http: ^8.0|^9.0|^10.0|^11.0|^12.0|^13.0
- illuminate/support: ^8.0|^9.0|^10.0|^11.0|^12.0|^13.0
- psr/log: ^1.1|^2.0|^3.0
Requires (Dev)
- laravel/pint: ^1.13
- orchestra/testbench: ^6.0|^7.0|^8.0|^9.0|^10.0|^11.0
- phpunit/phpunit: ^9.5|^10.5|^11.0|^12.0
Suggests
- league/flysystem: Necessário apenas para o driver de Storage (Storage::disk('azure')). O Laravel já o traz quando o filesystem é usado.
- symfony/mime: Melhora a detecção de Content-Type no upload. Sem ele o pacote usa uma tabela interna de extensões comuns.
This package is auto-updated.
Last update: 2026-08-13 23:47:48 UTC
README
SDK PHP para o Azure Blob Storage, integrado ao Laravel.
Suporta Laravel 8 a 13 / PHP 8.0+.
A REST API do Azure é implementada direto sobre o cliente HTTP do Laravel
(Illuminate\Http), com assinatura Shared Key (HMAC-SHA256) e geração de
Service SAS nativas. Não depende do microsoft/azure-storage-blob, que
está abandonado desde 2021 e não suporta PHP 8.2+ — é ele que costuma travar a
atualização de projetos que falam com o Azure.
use AzureBlob\Facades\AzureBlob; AzureBlob::list('apostilas/2026/'); AzureBlob::download('apostilas/2026/matematica.pdf')->saveTo('/tmp/mat.pdf'); AzureBlob::upload('apostilas/2026/nova.pdf', $conteudo); AzureBlob::temporaryUrl('apostilas/2026/matematica.pdf', 2); AzureBlob::connection('contratos')->uploadJson('metadados.json', $dados);
Compatibilidade
| Laravel | PHP | Flysystem | Testado no CI | Situação |
|---|---|---|---|---|
| 13.x | 8.3 · 8.4 | 3.x | ✅ | Recomendado |
| 12.x | 8.3 | 3.x | ✅ | Recomendado |
| 11.x | 8.2 | 3.x | ✅ | ⚠️ Suporte a ser removido |
| 10.x | 8.1 | 3.x | ✅ | ⚠️ Suporte a ser removido |
| 9.x | 8.1 | 3.x | ✅ | ⚠️ Suporte a ser removido |
| 8.x | 8.0 | 1.x | ✅ | ⚠️ Suporte a ser removido |
⚠️ Laravel 8, 9, 10 e 11 serão descontinuados
O suporte a essas versões existe como medida paliativa, para que projetos legados consigam usar o SDK enquanto a migração não acontece. Ele será removido numa versão futura, sem prazo definido mas sem cerimônia — provavelmente na primeira versão que precisar de um recurso mais novo do framework.
Todas as quatro já estão fora do suporte oficial da Laravel e carregam advisories de segurança conhecidas. Na prática isso significa que o Composer 2.10+ recusa instalá-las por padrão — se o seu projeto está numa delas, você já convive com esse bloqueio, independentemente deste pacote.
O Laravel 8 custa mais caro aqui do que nos outros pacotes: ele traz o Flysystem 1, cujo contrato de adaptador é incompatível com o do 3, então o driver de Storage existe em duas implementações. Ver docs/filesystem.md.
Migre para o Laravel 12 ou 13. Enquanto isso não é viável, fixe a versão do SDK para não ser surpreendido quando o suporte cair:
composer require joshua-barbosa/azure-blob-sdk:~0.1.0O
~0.1.0aceita correções (0.1.1,0.1.2) mas não sobe para0.2.0, que é onde a remoção pode acontecer.
O piso de PHP 8.0 custou alguns recursos de 8.1+ (readonly, enum).
O que foi cedido e como reverter está em docs/php-8.0-compat.md.
Instalação
composer require joshua-barbosa/azure-blob-sdk
O ServiceProvider e a facade Blob são descobertos automaticamente — nada a registrar manualmente.
O pacote está em
0.x: a API pública ainda pode mudar. O SemVer permite alterações incompatíveis entre versões0.xdiferentes, então fixe em~0.1.0se quiser proteção contra quebras — sobretudo se você depende do suporte a Laravel 8–11.
Desenvolvimento local
Para editar o pacote e ver o efeito imediato na aplicação, use um repositório path:
{
"repositories": [
{ "type": "path", "url": "../azure-blob-sdk", "options": { "symlink": true } }
],
"require": { "joshua-barbosa/azure-blob-sdk": "@dev" }
}
Com symlink: true o Composer aponta o vendor/ para a sua cópia local: você edita o pacote e o
efeito é imediato, sem composer update.
Publicar a configuração
php artisan vendor:publish --tag=azure-blob-config
Isso cria config/azure-blob.php na aplicação. O pacote funciona sem publicar — os padrões vêm do
próprio arquivo interno —, mas publicar é o caminho para declarar múltiplas contas e versionar
ajustes de timeout, proxy e log.
Configuração
Há três modos de autenticação, resolvidos nesta ordem de precedência.
Modo 1 — SAS URL (recomendado)
AZURE_STORAGE_SAS_URL="https://minhaconta.blob.core.windows.net/meu-container?sp=racwdl&st=...&se=...&sv=2022-11-02&sr=c&sig=..."
A URL já carrega o endpoint da conta, o container e o token — AZURE_STORAGE_CONTAINER
é opcional e, quando definido, sobrescreve o container embutido na URL. É o modo mais
seguro: a chave da conta nunca entra na aplicação.
Modo 2 — Connection string
AZURE_STORAGE_CONNECTION_STRING="DefaultEndpointsProtocol=https;AccountName=minhaconta;AccountKey=...;EndpointSuffix=core.windows.net" AZURE_STORAGE_CONTAINER=meu-container
Modo 3 — Conta + chave
AZURE_STORAGE_NAME=minhaconta AZURE_STORAGE_KEY=base64DaChaveDaConta== AZURE_STORAGE_CONTAINER=meu-container
Só os modos 2 e 3 conseguem assinar SAS novos. No modo 1 o pacote reaproveita o
token do container, então a validade e as permissões de temporaryUrl() são as do
token configurado. Detalhes em docs/authentication.md.
Múltiplas contas
Declare quantas conexões precisar em config/azure-blob.php e selecione pelo nome:
'connections' => [ 'default' => [ 'sas_url' => env('AZURE_STORAGE_SAS_URL'), 'container' => env('AZURE_STORAGE_CONTAINER'), ], 'apostilas' => [ 'sas_url' => env('APOSTILAS_AZURE_STORAGE_SAS_URL'), 'readonly' => true, ], 'contratos' => [ 'name' => env('CONTRATOS_AZURE_STORAGE_NAME'), 'key' => env('CONTRATOS_AZURE_STORAGE_KEY'), 'container' => 'contratos-assinados', ], ],
AzureBlob::connection('apostilas')->list();
Credenciais que não vêm do arquivo de config (uma conta por cliente, guardada no banco) podem ser montadas na hora:
$blob = app(BlobManager::class)->build(['sas_url' => $cliente->azure_sas_url]);
Proxy
Ambientes corporativos costumam exigir saída por proxy. Basta definir:
AZURE_PROXY=http://usuario:senha@proxy.empresa.com.br:8080 AZURE_PROXY_NO=localhost,.interno.empresa.com.br
Use AZURE_PROXY_HTTP e AZURE_PROXY_HTTPS quando precisar separá-los.
verify aceita false ou o caminho de um CA bundle customizado — um "false"
vindo do .env como string é convertido para booleano, senão o Guzzle o
interpretaria como caminho de arquivo.
Endpoint
Derivado automaticamente na maioria dos casos. Preencha para domínio próprio, nuvem soberana ou Azurite:
AZURE_STORAGE_URL=https://cdn.exemplo.com.br # endpoint da conta AZURE_STORAGE_ENDPOINT_SUFFIX=core.chinacloudapi.cn # padrão: core.windows.net AZURE_BLOB_CONNECTION=default # conexão usada quando nenhuma é informada
Timeouts e limites
AZURE_TIMEOUT=60 # resposta completa (s); por bloco em uploads fatiados AZURE_CONNECT_TIMEOUT=10 # conexão TCP (s) AZURE_VERIFY_SSL=true # false ou caminho de um CA bundle AZURE_MAX_DOWNLOAD_SIZE=5242880 # teto do download() em memória AZURE_BLOCK_SIZE=4194304 # acima disso o upload vai em blocos AZURE_API_VERSION=2022-11-02 # versão da REST API AZURE_READONLY=false # trava local de escrita
Não há retry automático: Put Blob não é idempotente quando combinado com
metadados e condicionais, e uma retentativa cega pode sobrescrever o que outro
processo acabou de gravar.
Log
AZURE_LOG_ENABLED=true AZURE_LOG_CHANNEL=azure-blob # null manda para o canal padrão da aplicação AZURE_LOG_LEVEL=debug AZURE_LOG_DAYS=14
O canal azure-blob é registrado automaticamente em logging.channels, a menos
que a aplicação já tenha um canal com esse nome — nesse caso o dela prevalece.
Chaves de conta, SAS tokens, connection strings e a assinatura (sig=) dentro de
URLs são mascarados antes de qualquer escrita. Um SAS em arquivo de log é uma
credencial vazada: quem lê o arquivo passa a ter o mesmo acesso ao container até
a expiração.
Uso
Facade
use AzureBlob\Facades\AzureBlob; AzureBlob::list('2026/'); // conexão padrão AzureBlob::connection('apostilas')->list(); // conexão nomeada
Injeção de dependência
use AzureBlob\BlobClient; use AzureBlob\BlobManager; public function __construct(private BlobClient $blob) {} // conexão padrão public function __construct(private BlobManager $azure) {} // gerenciador $this->azure->connection('contratos')->upload('a.pdf', $bytes);
O gerenciador se chama
BlobManager, e nãoAzureBlob, justamente para não colidir com a facade — que é registrada com o alias globalAzureBlob. Assim os dois convivem no mesmo arquivo sem precisar deas.
Listar
$lista = AzureBlob::list('2026/', maxResults: 100); foreach ($lista as $item) { echo $item->name, ' ', $item->humanSize(), PHP_EOL; } // Todas as páginas, sem carregar tudo na memória foreach (AzureBlob::listAll('2026/') as $item) { /* ... */ } // Listagem rasa: subpastas agrupadas em vez da árvore inteira $nivel = AzureBlob::directory('2026'); $nivel->directories; // BlobItem[] com isDirectory = true
Baixar
$conteudo = AzureBlob::download('a.pdf'); // BlobContent $conteudo->contents(); $conteudo->saveTo('/tmp/a.pdf'); AzureBlob::downloadJson('dados.json'); // array direto AzureBlob::downloadTo('grande.zip', '/tmp/g.zip'); // stream para disco
download() recusa blobs acima de max_download_size com BlobTooLargeException.
Para arquivos grandes use stream() ou downloadTo(), que mantêm o uso de
memória constante.
Enviar
AzureBlob::upload('a.txt', 'conteúdo'); AzureBlob::uploadJson('dados.json', ['total' => 2]); AzureBlob::uploadFile('destino/a.pdf', '/tmp/local.pdf'); AzureBlob::upload('a.pdf', $bytes, [ 'content_type' => 'application/pdf', // detectado pela extensão se omitido 'overwrite' => false, // falha se já existir 'metadata' => ['origem' => 'web'], ]);
Acima de block_size o envio é fatiado automaticamente em Put Block +
Put Block List, o que contorna o teto de 256 MiB do PUT simples.
Remover, copiar e mover
AzureBlob::delete('a.pdf'); // false se já não existia AzureBlob::deleteDirectory('2026/rascunhos'); // devolve a quantidade removida AzureBlob::copy('a.pdf', 'b.pdf', ['destination_container' => 'processados']); AzureBlob::move('origem.pdf', 'arquivo-morto/origem.pdf');
URLs assinadas
AzureBlob::url('a.pdf'); // pública, sem token AzureBlob::temporaryUrl('a.pdf', 2, 'r'); // 2 horas AzureBlob::temporaryUrl('a.pdf', now()->addDay(), 'rw'); // instante explícito
Storage::disk
// config/filesystems.php 'azure' => ['driver' => 'azure-blob', 'connection' => 'default'],
Storage::disk('azure')->put('pasta/a.txt', $conteudo); Storage::disk('azure')->temporaryUrl('pasta/a.txt', now()->addHour()); $request->file('anexo')->store('anexos', 'azure');
Comandos Artisan
php artisan azure:info --check php artisan azure:list 2026/ --max=50 php artisan azure:upload /tmp/a.pdf destino/a.pdf php artisan azure:sas 2026/a.pdf --hours=2
Referência da API
O README cobre o uso comum. Para o detalhe de cada parte:
| Documento | Conteúdo |
|---|---|
| docs/api.md | AzureBlob\BlobClient e AzureBlob\Results — todos os métodos, suas opções e os objetos de retorno (BlobList, BlobItem, BlobProperties, BlobContent) |
| docs/authentication.md | Os três modos de autenticação, como o Shared Key é assinado e por que a ordem das permissões do SAS importa |
| docs/filesystem.md | O driver azure-blob, os dois adaptadores de Flysystem e as diferenças de comportamento entre Laravel 8 e 9+ |
| docs/commands.md | Os oito comandos azure:*, suas opções e a regra de assinatura que o parser do Laravel 8 impõe |
| docs/mcp-migration.md | Equivalência com a ferramenta MCP em Python que este pacote substitui |
| docs/php-8.0-compat.md | O que o piso de PHP 8.0 custou e como reverter |
Tratamento de erros
Todas as falhas lançam AzureBlob\Exceptions\AzureBlobException, que estende \Exception.
use AzureBlob\Exceptions\AzureBlobException; use AzureBlob\Exceptions\BlobNotFoundException; try { $conteudo = AzureBlob::download('a.pdf'); } catch (BlobNotFoundException $e) { // 404 } catch (AzureBlobException $e) { $e->getMessage(); $e->status(); // 403 $e->errorCode(); // 'AuthorizationPermissionMismatch' $e->context(); // dados já sanitizados }
| Exceção | Quando |
|---|---|
ConfigurationException |
Credencial ausente, container indefinido, conexão inexistente |
BlobNotFoundException |
HTTP 404 do Azure |
BlobTooLargeException |
download() acima de max_download_size |
ReadOnlyException |
Escrita numa conexão travada; lançada antes de a requisição sair |
AzureBlobException |
Demais falhas HTTP e de comunicação |
Todas descendem de AzureBlobException — capture as específicas primeiro se
quiser distinguir os casos.
Testes
composer install
composer test
Desenvolver o pacote exige PHP 8.1+, embora ele rode em 8.0. O motivo é o
laravel/pint, que exige 8.1. Para instalar as dependências de desenvolvimento em PHP 8.0, remova-o antes:composer remove --dev laravel/pint --no-update. É o que a linha de PHP 8.0 do CI faz.
268 testes, 619 asserções. Cobertura: 87,13% de linhas / 75,29% de métodos (medida com Xdebug em PHP 8.3 / Laravel 13).
Rodado localmente em três pontos da matriz antes da publicação:
| PHP | Laravel | Flysystem | PHPUnit | Resultado |
|---|---|---|---|---|
| 8.0.30 | 8.83 | 1.1 | 9.6 | 268 testes, 21 pulados |
| 8.2.33 | 10.50 | 3.35 | 10.5 | 268 testes |
| 8.3.33 | 13.25 | 3.35 | 12.5 | 268 testes |
Os 21 pulados no Laravel 8 são os que instanciam o adaptador de Flysystem 3
diretamente — lá esse caminho é coberto via Storage::disk().
A suíte usa Http::fake() — nenhuma requisição sai para o Azure.
Para cobertura local:
vendor/bin/phpunit --coverage-text --coverage-filter src # requer pcov ou xdebug
Nos testes da sua aplicação, faça o mesmo — o pacote inteiro passa pelo cliente
HTTP do Laravel, então Http::fake() cobre também o driver de Storage e os
comandos Artisan:
Http::fake([ '*/meu-container/a.txt' => Http::response('conteúdo', 200), '*' => Http::response('', 201), ]); AzureBlob::upload('a.txt', 'x'); Http::assertSent(fn ($request) => $request->method() === 'PUT');
Corrigido durante a construção
Bugs reais encontrados pela suíte e pela validação na matriz, não hipóteses:
- Upload em blocos nunca fatiava. Vindo de uma string, o primeiro pedaço chegava vazio e o laço saía de cara: o commit ia com zero blocos e gravava um blob vazio. Só o caminho de stream funcionava.
azure:deleteestava quebrado no Laravel 8. A descrição do argumento continha--recursive, e o parser de assinatura do Laravel 8 casa/-{2,}(.*)/sem âncora — o argumento inteiro virava opção e o comando morria comThe "blob" argument does not exist. Só o Laravel 9+ ancorou a expressão.tests/Unit/CommandSignatureTest.phpguarda a regra.Content-Typeduplicado no Laravel 8.withHeaders()ewithBody()definiam o mesmo cabeçalho e o Guzzle os concatenava (text/plain,text/plain), valor que o Azure grava literal no blob.AzureBlobAdapterV1::copy()erename()devolviam array. O contrato do Flysystem 1 pedeboolnesses dois — sówrite/updatedevolvem array —, eStorage::copy()entregava o array ao chamador em vez detrue.- SAS URL do Azurite era mal interpretada. A heurística "host tem ponto →
conta no subdomínio" dava falso positivo em
127.0.0.1e descartava o endpoint. Passou a testarFILTER_VALIDATE_IP. - Nome do blob corrompido no contexto de erro. Numa falha de operação de
container (o caminho não tem barra),
strpos()devolviafalse, o cast paraintvirava0e osubstrcomia a primeira letra: o log diziaeu-container. Agora o campo fica vazio, que é o correto. - A facade colidia com o gerenciador. O alias global era
Blob— genérico demais para se registrar na aplicação inteira — e trocá-lo porAzureBlobesbarrava na classeAzureBlob\AzureBlob: três arquivos de teste já não conseguiam importar os dois juntos. O gerenciador virouBlobManager, que forma par comBlobCliente libera o nome para a facade. - Dependências erradas no
composer.json.symfony/http-foundationfoi herdado do pacote de referência e nunca usado; em compensaçãoilluminate/console,illuminate/filesystemeguzzlehttp/psr7eram usados sem serem declarados.
Pendências conhecidas
AzureBlobAdapterV1não é medido pela cobertura. Sob o Flysystem 3 a classe sequer carrega, e a linha de PHP 8.0 não tem driver de cobertura disponível. Ela é exercitada por testes reais viaStorage::disk()no CI do Laravel 8, mas não entra no percentual acima.- Não há suporte a
AZURE_PREFIX. A ferramenta MCP descobria variáveis por convenção de prefixo; aqui isso virou conexão nomeada explícita. Ver docs/mcp-migration.md. - Sem suporte a Azure AD / Managed Identity. Só Shared Key e SAS. Uma conta
com
allowSharedKeyAccess=falsenão funciona com este pacote. - Append blobs e page blobs não são suportados — só block blobs, que é o que cobre armazenamento de arquivos.
Contribuindo
Issues e pull requests são bem-vindos. O processo, os comandos e as armadilhas que já quebraram o pacote uma vez estão em CONTRIBUTING.md.
Falha de segurança não vai em issue pública — ver SECURITY.md.
O histórico de versões está em CHANGELOG.md.
Apoie
Este pacote é mantido nas horas vagas. Se ele te poupou algumas horas de briga com a REST API do Azure, me paga um café ☕ — ajuda a manter a compatibilidade em dia conforme o Laravel e a API do Azure evoluem.
Contribuição de código também é bem-vinda: abra uma issue ou um PR.
Licença
MIT.