Local storage vs cloud: Flysystem e adapters
1. Introdução ao Flysystem e ao conceito de abstração de armazenamento
Desenvolvedores PHP frequentemente enfrentam um dilema: começar com armazenamento local por simplicidade e depois migrar para a nuvem quando a aplicação cresce. Essa decisão, quando tomada sem planejamento, resulta em código fortemente acoplado a APIs específicas — seja file_put_contents, SDKs da AWS ou bibliotecas do Google Cloud.
O Flysystem resolve esse problema oferecendo uma camada de abstração unificada para sistemas de arquivos. Criado por Frank de Jonge e mantido pela comunidade PHP League, ele permite que você escreva código que funciona igualmente bem com disco local, Amazon S3, Google Cloud Storage, Azure Blob, FTP, SFTP e dezenas de outros provedores.
Os benefícios são claros: portabilidade entre ambientes, testabilidade com adapters em memória e manutenção simplificada — você troca o adapter, não a lógica de negócio.
2. Arquitetura do Flysystem: Filesystem, Adapters e Drivers
A arquitetura do Flysystem é elegante em sua simplicidade. A classe central é League\Flysystem\Filesystem, que expõe métodos como:
$filesystem->write('path/file.txt', 'conteúdo');
$filesystem->read('path/file.txt');
$filesystem->listContents('path', true);
$filesystem->delete('path/file.txt');
$filesystem->has('path/file.txt');
O adapter atua como ponte entre essa interface uniforme e o armazenamento real. Cada adapter implementa League\Flysystem\FilesystemAdapter e traduz chamadas genéricas para operações específicas do provedor.
Entre os drivers mais populares estão:
- league/flysystem-local — sistema de arquivos local
- league/flysystem-aws-s3-v3 — Amazon S3
- league/flysystem-google-cloud-storage — Google Cloud Storage
- league/flysystem-azure-blob-storage — Azure Blob
- league/flysystem-sftp — SFTP via phpseclib
3. Configuração e uso do adapter Local
Comece instalando o adapter local:
composer require league/flysystem-local
A configuração é direta:
use League\Flysystem\Filesystem;
use League\Flysystem\Local\LocalFilesystemAdapter;
$adapter = new LocalFilesystemAdapter('/var/www/uploads');
$filesystem = new Filesystem($adapter);
// Escrita
$filesystem->write('users/avatar.jpg', file_get_contents('temp.jpg'));
// Leitura
$conteudo = $filesystem->read('users/avatar.jpg');
// Listagem
$arquivos = $filesystem->listContents('users', true);
// Exclusão
$filesystem->delete('users/avatar.jpg');
// Verificação
if ($filesystem->has('users/avatar.jpg')) {
echo 'Arquivo existe';
}
O adapter local é rápido e simples, mas tem limitações significativas: não escala horizontalmente, não oferece replicação automática e backups precisam ser gerenciados manualmente. Para aplicações com múltiplos servidores, ele se torna um gargalo.
4. Configuração e uso de adapters cloud (S3, GCS, Azure)
Vamos configurar o Amazon S3:
composer require league/flysystem-aws-s3-v3
use Aws\S3\S3Client;
use League\Flysystem\AwsS3V3\AwsS3V3Adapter;
use League\Flysystem\Filesystem;
$client = new S3Client([
'credentials' => [
'key' => $_ENV['AWS_ACCESS_KEY_ID'],
'secret' => $_ENV['AWS_SECRET_ACCESS_KEY'],
],
'region' => 'us-east-1',
'version' => 'latest',
]);
$adapter = new AwsS3V3Adapter($client, 'meu-bucket', 'prefixo-opcional');
$filesystem = new Filesystem($adapter);
// Upload
$filesystem->write('documentos/relatorio.pdf', fopen('relatorio.pdf', 'r'));
// Download
$conteudo = $filesystem->read('documentos/relatorio.pdf');
// URL pública
$url = $client->getObjectUrl('meu-bucket', 'documentos/relatorio.pdf');
Para Google Cloud Storage:
composer require league/flysystem-google-cloud-storage
use Google\Cloud\Storage\StorageClient;
use League\Flysystem\GoogleCloudStorage\GoogleCloudStorageAdapter;
use League\Flysystem\Filesystem;
$storageClient = new StorageClient([
'projectId' => $_ENV['GCP_PROJECT_ID'],
'keyFilePath' => $_ENV['GCP_KEY_FILE'],
]);
$bucket = $storageClient->bucket($_ENV['GCS_BUCKET']);
$adapter = new GoogleCloudStorageAdapter($bucket);
$filesystem = new Filesystem($adapter);
Em ambientes cloud, trate exceções e timeouts adequadamente:
use League\Flysystem\UnableToWriteFile;
try {
$filesystem->write('arquivo.mp4', $stream, ['timeout' => 300]);
} catch (UnableToWriteFile $e) {
Log::error('Falha ao enviar para cloud: ' . $e->getMessage());
// Fallback para armazenamento local
}
5. Estratégias de migração entre local e cloud
A transição entre ambientes deve ser indolor. Use variáveis de ambiente para alternar adapters:
function criarFilesystem(): Filesystem
{
$driver = $_ENV['FILESYSTEM_DRIVER'] ?? 'local';
return match ($driver) {
's3' => criarFilesystemS3(),
'gcs' => criarFilesystemGCS(),
default => criarFilesystemLocal(),
};
}
function criarFilesystemLocal(): Filesystem
{
$adapter = new LocalFilesystemAdapter($_ENV['LOCAL_PATH']);
return new Filesystem($adapter);
}
function criarFilesystemS3(): Filesystem
{
$client = new S3Client([
'credentials' => [
'key' => $_ENV['AWS_KEY'],
'secret' => $_ENV['AWS_SECRET'],
],
'region' => $_ENV['AWS_REGION'],
'version' => 'latest',
]);
$adapter = new AwsS3V3Adapter($client, $_ENV['AWS_BUCKET']);
return new Filesystem($adapter);
}
Para sincronizar arquivos entre armazenamentos, considere ferramentas como aws s3 sync ou scripts personalizados que iteram sobre diretórios e transferem arquivos usando o próprio Flysystem.
Cuidados importantes:
- Paths relativos vs absolutos: sempre use paths relativos no Flysystem
- Permissões: adapters cloud geralmente ignoram permissões de arquivo
- Metadados: nem todos os adapters suportam os mesmos metadados (mime type, visibilidade)
6. Performance, cache e boas práticas
Adapters remotos introduzem latência de rede. Estratégias para mitigar:
Cache de metadados — evita chamadas repetidas à API para listar diretórios:
use League\Flysystem\CachedStorage\Adapter;
use League\Flysystem\CachedStorage\Storage\Psr6Cache;
$cacheStorage = new Psr6Cache($cacheItemPool);
$cachedAdapter = new Adapter($s3Adapter, $cacheStorage);
$filesystem = new Filesystem($cachedAdapter);
Uso de streams para arquivos grandes — evita carregar tudo em memória:
// Ruim: carrega o arquivo inteiro na memória
$filesystem->write('grande.mp4', file_get_contents('grande.mp4'));
// Bom: usa stream
$stream = fopen('grande.mp4', 'r');
$filesystem->writeStream('grande.mp4', $stream);
fclose($stream);
Boas práticas:
- Configure retry policies para adapters cloud
- Use conexões persistentes (reutilize clientes HTTP)
- Implemente lazy loading para instanciar Filesystem apenas quando necessário
- Monitore custos de API calls em provedores cloud
7. Flysystem no ecossistema Laravel
O Laravel já inclui Flysystem como dependência e oferece integração nativa via config/filesystems.php:
// config/filesystems.php
'disks' => [
'local' => [
'driver' => 'local',
'root' => storage_path('app'),
],
's3' => [
'driver' => 's3',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION'),
'bucket' => env('AWS_BUCKET'),
],
'gcs' => [
'driver' => 'gcs',
'project_id' => env('GCS_PROJECT_ID'),
'key_file' => env('GCS_KEY_FILE'),
'bucket' => env('GCS_BUCKET'),
],
],
Use o facade Storage para alternar entre discos:
use Illuminate\Support\Facades\Storage;
// Upload com fallback
try {
Storage::disk('s3')->put('avatars/'.$user->id.'.jpg', $foto);
} catch (\Exception $e) {
Storage::disk('local')->put('avatars/'.$user->id.'.jpg', $foto);
}
// URL temporária
$url = Storage::disk('s3')->temporaryUrl('documentos/contrato.pdf', now()->addHours(2));
8. Testes unitários com Flysystem e mocks de adapters
Testar armazenamento sem depender de serviços externos é crucial. O Flysystem oferece o MemoryFilesystemAdapter:
composer require --dev league/flysystem-memory
use League\Flysystem\Filesystem;
use League\Flysystem\Memory\MemoryFilesystemAdapter;
use PHPUnit\Framework\TestCase;
class ArmazenamentoTest extends TestCase
{
private Filesystem $filesystem;
protected function setUp(): void
{
$this->filesystem = new Filesystem(new MemoryFilesystemAdapter());
}
public function testEscritaELeituraDeArquivo(): void
{
$this->filesystem->write('teste.txt', 'conteúdo');
$this->assertTrue($this->filesystem->has('teste.txt'));
$this->assertEquals('conteúdo', $this->filesystem->read('teste.txt'));
}
public function testExclusaoDeArquivo(): void
{
$this->filesystem->write('temp.txt', 'dados');
$this->filesystem->delete('temp.txt');
$this->assertFalse($this->filesystem->has('temp.txt'));
}
public function testListagemDeDiretorio(): void
{
$this->filesystem->write('pasta/a.txt', '1');
$this->filesystem->write('pasta/b.txt', '2');
$itens = $this->filesystem->listContents('pasta', false);
$this->assertCount(2, $itens);
}
}
Para mockar adapters cloud em testes de integração, use PHPUnit com mocks de clientes HTTP:
public function testUploadComS3Mock()
{
$s3Mock = $this->createMock(S3Client::class);
$s3Mock->method('putObject')->willReturn(new Result([]));
$adapter = new AwsS3V3Adapter($s3Mock, 'bucket');
$filesystem = new Filesystem($adapter);
$filesystem->write('arquivo.txt', 'conteúdo');
$this->addToAssertionCount(1);
}
Conclusão
Flysystem transforma a complexidade de múltiplos sistemas de armazenamento em uma interface coesa e testável. Seja você um desenvolvedor solo começando com armazenamento local ou uma equipe escalando para milhões de arquivos na nuvem, a abstração oferecida pelo Flysystem simplifica decisões de infraestrutura e mantém seu código limpo e portável.
Comece com o adapter local, teste com memória e, quando chegar a hora, migre para S3, GCS ou Azure com uma única linha de configuração alterada. Essa é a beleza da abstração bem feita.
Referências
- Documentação oficial do Flysystem (The PHP League) — Guia completo com todos os adapters, instalação e exemplos de uso
- Laravel Filesystem Documentation — Documentação oficial do Laravel sobre discos Flysystem, drivers cloud e facade Storage
- AWS SDK for PHP - S3 — Exemplos práticos de operações S3 com o SDK oficial da AWS
- Google Cloud Storage PHP Client — Documentação do cliente PHP para Google Cloud Storage, usado pelo adapter GCS
- Flysystem Memory Adapter no GitHub — Repositório oficial do adapter em memória para testes unitários
- PHP League: The Flysystem Story — Artigo do criador Frank de Jonge sobre a motivação e arquitetura do Flysystem
- Flysystem vs Gaufrette: Comparação de abstrações de armazenamento — Artigo técnico comparando Flysystem com outras bibliotecas de abstração