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