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.

Listagem de Projetos no /app com a coluna de anexos: miniaturas circulares empilhadas na linha de cada registro

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. Com MEDIA_DISK=public o arquivo cai em storage/app/public, servido pelo symlink public/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. Use public só para avatar e logo, que aparecem na tela de login.

Duas consequências práticas do disco privado:

  1. Media::getUrl() responde 403. É falha fechada, e é o que se espera. Quem publica link de mídia privada usa getTemporaryUrl(), que assina a URL.
  2. Quem tem o link entra, durante a validade da assinatura, sem sessão. A rota storage.local do 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. Rode php artisan kit:midia-privada (aceita --dry-run) para mover o que já foi gravado — sem ele, a mídia antiga continua servida pelo symlink.


This site uses Just the Docs, a documentation theme for Jekyll.