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.