Anexos e mídia
O filament/spatie-laravel-media-library-plugin entrega a camada de mídia — upload, coleções e
conversões — nos componentes de formulário, tabela e infolist do Filament. A model de demonstração
App\Models\Projeto mostra o desenho completo:
use Spatie\MediaLibrary\HasMedia;use Spatie\MediaLibrary\InteractsWithMedia;use Spatie\MediaLibrary\MediaCollections\Models\Media;
class Projeto extends Model implements HasMedia{ use InteractsWithMedia;
public function registerMediaCollections(): void { $this->addMediaCollection('anexos'); }
public function registerMediaConversions(?Media $media = null): void { $this->addMediaConversion('miniatura') ->nonQueued() // sem garantia de worker no ar: enfileirada, a ->width(200) // conversão só existiria com um worker de pé, e a coluna ->height(200); // ficaria vazia sem erro nenhum }}E o ProjetoResource consome as duas pontas:
SpatieMediaLibraryFileUpload::make('anexos'), // no formulário
SpatieMediaLibraryImageColumn::make('anexos') // na tabela ->simpleLightbox(),O ->simpleLightbox() funciona sem cola porque SpatieMediaLibraryImageColumn estende
ImageColumn, que é exatamente onde o macro do lightbox é registrado.
Repare nas miniaturas empilhadas na linha do registro: cada uma é servida por URL assinada, porque o disco é privado — o mesmo arquivo pedido sem a assinatura responde 403.
O escopo por organização vem de graça — e é o ponto. A tabela media do Spatie é polimórfica:
o arquivo pertence ao registro, e o registro já é escopado por BelongsToTenant. Quem não alcança
o projeto não alcança o anexo, sem coluna de tenant em media e sem configuração para lembrar de
ligar.
⚠️ O disco default da mídia é
local, e é privado de propósito. ComMEDIA_DISK=publico arquivo cai emstorage/app/public, servido pelo symlinkpublic/storage: caminho/storage/{id}/{arquivo}, ID sequencial, alcançável sem sessão — a multi-organização do Filament não chega ao sistema de arquivos. Usepublicsó para avatar e logo, que aparecem na tela de login.Duas consequências práticas do disco privado:
Media::getUrl()responde 403. É falha fechada, e é o que se espera. Quem publica link de mídia privada usagetTemporaryUrl(), que assina a URL.- Quem tem o link entra, durante a validade da assinatura, sem sessão. A rota
storage.localdo Laravel valida a assinatura, não o usuário: compartilhar o link é compartilhar o arquivo até ele expirar. Para anexo que precise de autorização por organização, sirva por rota própria que consulte a policy antes de entregar.Já tem instalação rodando com
MEDIA_DISK=public? A config nova protege só o arquivo NOVO. Rodephp artisan kit:midia-privada(aceita--dry-run) para mover o que já foi gravado — sem ele, a mídia antiga continua servida pelo symlink.