Gestire i file su Amazon S3 in Laravel con Filament
Amazon S3 (Simple Storage Service) è uno dei servizi di archiviazione oggetti più diffusi e affidabili disponibili oggi. Integrato in un'applicazione Laravel con pannello amministrativo Filament, permette di gestire upload, download e cancellazione di file in modo scalabile e sicuro, senza dover dipendere dallo storage locale del server. In questo articolo vedremo come configurare S3 come driver di filesystem in Laravel, come collegarlo ai componenti di Filament e come costruire un flusso completo di gestione dei file dall'interfaccia amministrativa.
Prerequisiti
Prima di iniziare è necessario disporre di un'applicazione Laravel 10 o superiore con Filament v3 installato e configurato. Occorre inoltre un account AWS attivo con accesso alla console, dove andremo a creare un bucket S3 e le relative credenziali IAM. Sul lato PHP è richiesto il pacchetto league/flysystem-aws-s3-v3, che funge da adattatore tra il filesystem astratto di Laravel e le API di S3.
Installiamo il pacchetto necessario tramite Composer:
composer require league/flysystem-aws-s3-v3 "^3.0"
Creazione del bucket S3 su AWS
Accedendo alla console AWS, navighiamo nel servizio S3 e creiamo un nuovo bucket. Il nome del bucket deve essere globalmente univoco. Durante la creazione è importante disattivare il blocco degli accessi pubblici solo se si desidera rendere pubblicamente accessibili i file caricati; in caso contrario, è preferibile mantenere il blocco attivo e gestire gli accessi tramite URL pre-firmati.
Una volta creato il bucket, è necessario configurare una policy CORS per consentire le richieste provenienti dal dominio dell'applicazione. Dalla scheda "Autorizzazioni" del bucket, aggiungiamo la seguente configurazione CORS:
[
{
"AllowedHeaders": ["*"],
"AllowedMethods": ["GET", "PUT", "POST", "DELETE", "HEAD"],
"AllowedOrigins": ["https://example.com"],
"ExposeHeaders": ["ETag"]
}
]
Passiamo poi al servizio IAM per creare un utente dedicato all'applicazione. Assegniamo all'utente una policy inline con i permessi minimi necessari:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject",
"s3:DeleteObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::nome-del-bucket",
"arn:aws:s3:::nome-del-bucket/*"
]
}
]
}
Dopo aver creato l'utente, generiamo le chiavi di accesso (Access Key ID e Secret Access Key) che useremo nella configurazione di Laravel.
Configurazione del filesystem in Laravel
Aggiungiamo le credenziali S3 al file .env del progetto:
AWS_ACCESS_KEY_ID=la-tua-access-key
AWS_SECRET_ACCESS_KEY=la-tua-secret-key
AWS_DEFAULT_REGION=eu-west-1
AWS_BUCKET=nome-del-bucket
AWS_URL=https://nome-del-bucket.s3.eu-west-1.amazonaws.com
AWS_ENDPOINT=
AWS_USE_PATH_STYLE_ENDPOINT=false
Laravel legge queste variabili nel file config/filesystems.php. Verifichiamo che il disco S3 sia definito correttamente:
<?php
return [
// Disco predefinito per l'applicazione
'default' => env('FILESYSTEM_DISK', 'local'),
'disks' => [
'local' => [
'driver' => 'local',
'root' => storage_path('app'),
'throw' => false,
],
'public' => [
'driver' => 'local',
'root' => storage_path('app/public'),
'url' => env('APP_URL') . '/storage',
'visibility' => 'public',
'throw' => false,
],
// Configurazione del disco S3
's3' => [
'driver' => 's3',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION'),
'bucket' => env('AWS_BUCKET'),
'url' => env('AWS_URL'),
'endpoint' => env('AWS_ENDPOINT'),
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
'throw' => false,
// Visibilità predefinita per i file caricati
'visibility' => 'private',
],
],
'links' => [
public_path('storage') => storage_path('app/public'),
],
];
Se vogliamo impostare S3 come disco di default per tutta l'applicazione, è sufficiente cambiare la variabile FILESYSTEM_DISK nel file .env:
FILESYSTEM_DISK=s3
Modello e migrazione
Per illustrare un caso d'uso concreto, creiamo una risorsa per la gestione di documenti aziendali. Ogni documento avrà un nome, una descrizione e il percorso del file su S3. Generiamo il modello e la migrazione:
php artisan make:model Document -m
Definiamo la migrazione:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('documents', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->text('description')->nullable();
// Percorso del file all'interno del bucket S3
$table->string('file_path');
// Dimensione del file in byte
$table->unsignedBigInteger('file_size')->nullable();
// Tipo MIME del file
$table->string('mime_type')->nullable();
$table->string('disk')->default('s3');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('documents');
}
};
Eseguiamo la migrazione:
php artisan migrate
Definiamo il modello Eloquent:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Storage;
class Document extends Model
{
protected $fillable = [
'name',
'description',
'file_path',
'file_size',
'mime_type',
'disk',
];
// Restituisce l'URL temporaneo del file su S3 (valido 60 minuti)
public function getTemporaryUrlAttribute(): string
{
return Storage::disk($this->disk)
->temporaryUrl($this->file_path, now()->addMinutes(60));
}
// Restituisce la dimensione del file in formato leggibile
public function getHumanFileSizeAttribute(): string
{
$bytes = $this->file_size ?? 0;
if ($bytes < 1024) {
return $bytes . ' B';
} elseif ($bytes < 1048576) {
return round($bytes / 1024, 2) . ' KB';
} else {
return round($bytes / 1048576, 2) . ' MB';
}
}
}
Creazione della risorsa Filament
Generiamo la risorsa Filament per i documenti:
php artisan make:filament-resource Document --generate
Questo comando crea automaticamente i file DocumentResource.php, ListDocuments.php, CreateDocument.php e EditDocument.php all'interno di app/Filament/Resources/. Modifichiamo DocumentResource.php per definire il form e la tabella:
<?php
namespace App\Filament\Resources;
use App\Filament\Resources\DocumentResource\Pages;
use App\Models\Document;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Support\Facades\Storage;
class DocumentResource extends Resource
{
protected static ?string $model = Document::class;
protected static ?string $navigationIcon = 'heroicon-o-document';
protected static ?string $navigationLabel = 'Documenti';
protected static ?string $modelLabel = 'Documento';
protected static ?string $pluralModelLabel = 'Documenti';
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('name')
->label('Nome')
->required()
->maxLength(255),
Forms\Components\Textarea::make('description')
->label('Descrizione')
->rows(3)
->columnSpanFull(),
// Componente di upload collegato al disco S3
Forms\Components\FileUpload::make('file_path')
->label('File')
->required()
->disk('s3')
->directory('documents')
->visibility('private')
->preserveFilenames()
->maxSize(51200) // 50 MB in kilobyte
->acceptedFileTypes([
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'image/jpeg',
'image/png',
])
->afterStateUpdated(function ($state, callable $set) {
// Aggiorniamo i metadati del file dopo il caricamento
if ($state) {
$path = is_array($state) ? array_key_first($state) : $state;
$set('disk', 's3');
}
})
->columnSpanFull(),
Forms\Components\Hidden::make('disk')
->default('s3'),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('name')
->label('Nome')
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('description')
->label('Descrizione')
->limit(50)
->toggleable(),
Tables\Columns\TextColumn::make('mime_type')
->label('Tipo')
->badge()
->toggleable(),
Tables\Columns\TextColumn::make('human_file_size')
->label('Dimensione')
->toggleable(),
Tables\Columns\TextColumn::make('created_at')
->label('Caricato il')
->dateTime('d/m/Y H:i')
->sortable()
->toggleable(),
])
->filters([])
->actions([
// Azione per scaricare il file tramite URL pre-firmato
Tables\Actions\Action::make('download')
->label('Scarica')
->icon('heroicon-o-arrow-down-tray')
->url(fn (Document $record): string => $record->temporary_url)
->openUrlInNewTab(),
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make()
->after(function (Document $record) {
// Eliminiamo il file da S3 dopo la cancellazione del record
Storage::disk($record->disk)->delete($record->file_path);
}),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make()
->after(function ($records) {
// Eliminiamo tutti i file selezionati da S3
foreach ($records as $record) {
Storage::disk($record->disk)->delete($record->file_path);
}
}),
]),
]);
}
public static function getRelations(): array
{
return [];
}
public static function getPages(): array
{
return [
'index' => Pages\ListDocuments::route('/'),
'create' => Pages\CreateDocument::route('/create'),
'edit' => Pages\EditDocument::route('/{record}/edit'),
];
}
}
Gestione dei metadati dopo l'upload
Il componente FileUpload di Filament salva automaticamente il percorso del file nel campo del database, ma non popolerà automaticamente i campi file_size e mime_type. Per farlo, sovrascriviamo il metodo handleRecordCreation nella pagina di creazione e il metodo handleRecordUpdate nella pagina di modifica.
Modifichiamo app/Filament/Resources/DocumentResource/Pages/CreateDocument.php:
<?php
namespace App\Filament\Resources\DocumentResource\Pages;
use App\Filament\Resources\DocumentResource;
use App\Models\Document;
use Filament\Resources\Pages\CreateRecord;
use Illuminate\Support\Facades\Storage;
class CreateDocument extends CreateRecord
{
protected static string $resource = DocumentResource::class;
// Intercettiamo la creazione per aggiungere i metadati del file
protected function handleRecordCreation(array $data): Document
{
$filePath = $data['file_path'];
$disk = $data['disk'] ?? 's3';
// Recuperiamo dimensione e tipo MIME del file da S3
if ($filePath && Storage::disk($disk)->exists($filePath)) {
$data['file_size'] = Storage::disk($disk)->size($filePath);
$data['mime_type'] = Storage::disk($disk)->mimeType($filePath);
}
return Document::create($data);
}
protected function getRedirectUrl(): string
{
return $this->getResource()::getUrl('index');
}
}
Modifichiamo app/Filament/Resources/DocumentResource/Pages/EditDocument.php:
<?php
namespace App\Filament\Resources\DocumentResource\Pages;
use App\Filament\Resources\DocumentResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Storage;
class EditDocument extends EditRecord
{
protected static string $resource = DocumentResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
// Aggiorniamo i metadati se il file viene sostituito
protected function handleRecordUpdate(Model $record, array $data): Model
{
$newFilePath = $data['file_path'];
$disk = $data['disk'] ?? 's3';
// Se il percorso del file è cambiato, aggiorniamo i metadati e puliamo il vecchio file
if ($newFilePath !== $record->file_path) {
if ($record->file_path && Storage::disk($disk)->exists($record->file_path)) {
Storage::disk($disk)->delete($record->file_path);
}
if ($newFilePath && Storage::disk($disk)->exists($newFilePath)) {
$data['file_size'] = Storage::disk($disk)->size($newFilePath);
$data['mime_type'] = Storage::disk($disk)->mimeType($newFilePath);
}
}
$record->update($data);
return $record;
}
protected function getRedirectUrl(): string
{
return $this->getResource()::getUrl('index');
}
}
URL pre-firmati e visibilità dei file
Quando i file sono archiviati con visibilità private su S3, non è possibile accedervi tramite URL pubblico diretto. La soluzione è l'utilizzo degli URL pre-firmati (presigned URL), che sono URL temporanei firmati crittograficamente dalle credenziali AWS e validi per un periodo di tempo definito.
Laravel mette a disposizione il metodo Storage::disk('s3')->temporaryUrl() per generarli. Abbiamo già utilizzato questo approccio nell'attributo getTemporaryUrlAttribute del modello. Possiamo anche generare URL pre-firmati direttamente nei controller o nei service:
<?php
namespace App\Services;
use Illuminate\Support\Facades\Storage;
class FileService
{
// Genera un URL pre-firmato con scadenza personalizzabile
public function getPresignedUrl(string $filePath, int $expirationMinutes = 30): string
{
return Storage::disk('s3')->temporaryUrl(
$filePath,
now()->addMinutes($expirationMinutes)
);
}
// Carica un file su S3 e restituisce il percorso
public function upload(
\Illuminate\Http\UploadedFile $file,
string $directory = 'uploads',
string $visibility = 'private'
): string {
return Storage::disk('s3')->putFile(
$directory,
$file,
$visibility
);
}
// Elimina un file da S3
public function delete(string $filePath): bool
{
if (Storage::disk('s3')->exists($filePath)) {
return Storage::disk('s3')->delete($filePath);
}
return false;
}
// Verifica l'esistenza del file su S3
public function exists(string $filePath): bool
{
return Storage::disk('s3')->exists($filePath);
}
// Restituisce i metadati del file
public function getMetadata(string $filePath): array
{
$disk = Storage::disk('s3');
return [
'size' => $disk->size($filePath),
'mime_type' => $disk->mimeType($filePath),
'last_modified' => $disk->lastModified($filePath),
];
}
}
Aggiungere un'azione di anteprima per le immagini
Per i file di tipo immagine è utile mostrare un'anteprima direttamente nel pannello Filament. Possiamo farlo aggiungendo un'azione condizionale nella tabella:
<?php
// Aggiungiamo questo metodo all'interno della classe DocumentResource
use Filament\Tables\Actions\Action;
use Filament\Support\Enums\MaxWidth;
// Azione di anteprima per le immagini
Action::make('preview')
->label('Anteprima')
->icon('heroicon-o-eye')
->visible(fn (Document $record): bool => str_starts_with($record->mime_type ?? '', 'image/'))
->modalContent(fn (Document $record) => view(
'filament.modals.image-preview',
['url' => $record->temporary_url]
))
->modalWidth(MaxWidth::ExtraLarge)
->modalSubmitAction(false)
->modalCancelActionLabel('Chiudi'),
Creiamo la vista del modale in resources/views/filament/modals/image-preview.blade.php:
<!-- Vista per l'anteprima dell'immagine nel modale -->
<div class="flex items-center justify-center p-4">
<img
src="{{ $url }}"
alt="Anteprima"
class="max-w-full max-h-96 object-contain rounded-lg shadow"
>
</div>
Widget per le statistiche dello storage
Filament permette di creare widget personalizzati per la dashboard. Creiamo un widget che mostra alcune statistiche sui file caricati:
php artisan make:filament-widget StorageStatsWidget --stats-overview
Modifichiamo il file generato in app/Filament/Widgets/StorageStatsWidget.php:
<?php
namespace App\Filament\Widgets;
use App\Models\Document;
use Filament\Widgets\StatsOverviewWidget as BaseWidget;
use Filament\Widgets\StatsOverviewWidget\Stat;
class StorageStatsWidget extends BaseWidget
{
protected function getStats(): array
{
// Calcoliamo le statistiche dei documenti
$totalDocuments = Document::count();
$totalSizeBytes = Document::sum('file_size');
$recentUploads = Document::where('created_at', '>=', now()->subDays(7))->count();
// Convertiamo la dimensione totale in un formato leggibile
$totalSizeFormatted = $this->formatBytes($totalSizeBytes);
return [
Stat::make('Documenti totali', $totalDocuments)
->description('File archiviati su S3')
->color('primary')
->icon('heroicon-o-document'),
Stat::make('Spazio utilizzato', $totalSizeFormatted)
->description('Dimensione totale dei file')
->color('warning')
->icon('heroicon-o-server'),
Stat::make('Caricamenti recenti', $recentUploads)
->description('Ultimi 7 giorni')
->color('success')
->icon('heroicon-o-arrow-up-tray'),
];
}
// Formatta i byte in un formato leggibile dall'utente
private function formatBytes(int $bytes): string
{
if ($bytes < 1024) {
return $bytes . ' B';
} elseif ($bytes < 1048576) {
return round($bytes / 1024, 2) . ' KB';
} elseif ($bytes < 1073741824) {
return round($bytes / 1048576, 2) . ' MB';
} else {
return round($bytes / 1073741824, 2) . ' GB';
}
}
}
Registriamo il widget nel pannello Filament in app/Providers/Filament/AdminPanelProvider.php:
<?php
namespace App\Providers\Filament;
use App\Filament\Widgets\StorageStatsWidget;
use Filament\Http\Middleware\Authenticate;
use Filament\Http\Middleware\DisableBladeIconComponents;
use Filament\Http\Middleware\DispatchServletEvents;
use Filament\Panel;
use Filament\PanelProvider;
use Filament\Support\Colors\Color;
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
use Illuminate\Cookie\Middleware\EncryptCookies;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
use Illuminate\Routing\Middleware\SubstituteBindings;
use Illuminate\Session\Middleware\AuthenticateSession;
use Illuminate\Session\Middleware\StartSession;
use Illuminate\View\Middleware\ShareErrorsFromSession;
class AdminPanelProvider extends PanelProvider
{
public function panel(Panel $panel): Panel
{
return $panel
->default()
->id('admin')
->path('admin')
->colors(['primary' => Color::Amber])
// Registriamo il widget delle statistiche
->widgets([
StorageStatsWidget::class,
])
->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources')
->discoverPages(in: app_path('Filament/Pages'), for: 'App\\Filament\\Pages')
->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets')
->middleware([
EncryptCookies::class,
AddQueuedCookiesToResponse::class,
StartSession::class,
AuthenticateSession::class,
ShareErrorsFromSession::class,
VerifyCsrfToken::class,
SubstituteBindings::class,
DisableBladeIconComponents::class,
DispatchServletEvents::class,
])
->authMiddleware([
Authenticate::class,
]);
}
}
Pulizia automatica dei file orfani
Quando un record viene eliminato senza passare attraverso l'interfaccia Filament (ad esempio tramite comandi Artisan o eliminazioni in massa a livello di database), il file corrispondente su S3 potrebbe rimanere senza essere rimosso. La soluzione è agganciare la cancellazione del file agli eventi Eloquent nel modello:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Storage;
class Document extends Model
{
protected $fillable = [
'name',
'description',
'file_path',
'file_size',
'mime_type',
'disk',
];
// Registriamo il listener per la cancellazione automatica del file
protected static function booted(): void
{
static::deleting(function (Document $document) {
// Eliminiamo il file da S3 prima di cancellare il record
if ($document->file_path) {
Storage::disk($document->disk ?? 's3')->delete($document->file_path);
}
});
}
public function getTemporaryUrlAttribute(): string
{
return Storage::disk($this->disk)
->temporaryUrl($this->file_path, now()->addMinutes(60));
}
public function getHumanFileSizeAttribute(): string
{
$bytes = $this->file_size ?? 0;
if ($bytes < 1024) {
return $bytes . ' B';
} elseif ($bytes < 1048576) {
return round($bytes / 1024, 2) . ' KB';
} else {
return round($bytes / 1048576, 2) . ' MB';
}
}
}
In alternativa, è possibile creare un comando Artisan schedulato che confronta periodicamente i file presenti su S3 con i record del database e rimuove quelli orfani:
<?php
namespace App\Console\Commands;
use App\Models\Document;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
class CleanOrphanedS3FilesCommand extends Command
{
protected $signature = 'storage:clean-orphaned';
protected $description = 'Rimuove da S3 i file non associati ad alcun record nel database';
public function handle(): int
{
// Recuperiamo tutti i percorsi registrati nel database
$databasePaths = Document::pluck('file_path')->toArray();
// Recuperiamo tutti i file presenti nella directory principale su S3
$s3Files = Storage::disk('s3')->allFiles('documents');
$orphanedCount = 0;
foreach ($s3Files as $s3File) {
if (!in_array($s3File, $databasePaths)) {
// Il file non ha un record corrispondente, lo eliminiamo
Storage::disk('s3')->delete($s3File);
$this->line("Eliminato: {$s3File}");
$orphanedCount++;
}
}
$this->info("Pulizia completata. File orfani eliminati: {$orphanedCount}");
return self::SUCCESS;
}
}
Registriamo il comando nello scheduler in app/Console/Kernel.php o nel file routes/console.php per le versioni più recenti di Laravel:
<?php
use Illuminate\Foundation\Inspiring;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Schedule;
// Pulizia dei file orfani ogni domenica a mezzanotte
Schedule::command('storage:clean-orphaned')->weekly()->sundays()->at('00:00');
Test con filesystem fittizio
Per i test automatici è fondamentale non effettuare chiamate reali ad AWS. Laravel mette a disposizione il metodo Storage::fake() che sostituisce il disco specificato con un filesystem in memoria, evitando qualsiasi interazione con S3 durante l'esecuzione dei test.
<?php
namespace Tests\Feature;
use App\Models\Document;
use App\Models\User;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;
class DocumentManagementTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
// Sostituiamo il disco S3 con un filesystem fittizio in memoria
Storage::fake('s3');
}
public function test_document_is_deleted_from_s3_when_record_is_deleted(): void
{
// Creiamo un file fittizio e lo carichiamo nel disco finto
$file = UploadedFile::fake()->create('report.pdf', 100, 'application/pdf');
$path = Storage::disk('s3')->putFile('documents', $file);
// Creiamo il record nel database
$document = Document::factory()->create([
'file_path' => $path,
'disk' => 's3',
]);
// Verifichiamo che il file esista prima della cancellazione
Storage::disk('s3')->assertExists($path);
// Eliminiamo il record
$document->delete();
// Verifichiamo che il file sia stato rimosso da S3
Storage::disk('s3')->assertMissing($path);
}
public function test_document_can_be_uploaded_through_filament(): void
{
$user = User::factory()->create();
$file = UploadedFile::fake()->create('document.pdf', 200, 'application/pdf');
// Simuliamo la creazione tramite il pannello Filament
$this->actingAs($user)
->post('/admin/documents', [
'name' => 'Documento di test',
'description' => 'Descrizione di prova',
'file_path' => $file,
'disk' => 's3',
]);
// Verifichiamo che il record esista nel database
$this->assertDatabaseHas('documents', [
'name' => 'Documento di test',
'disk' => 's3',
]);
}
}
Considerazioni sulla sicurezza
L'integrazione con S3 introduce alcune responsabilità in termini di sicurezza che è importante considerare. In primo luogo, le credenziali AWS non devono mai essere incluse nel codice sorgente né nei file di configurazione versionati; devono essere gestite esclusivamente tramite variabili d'ambiente e, in ambienti di produzione, tramite IAM Role associati all'istanza EC2 o al container.
In secondo luogo, gli URL pre-firmati hanno una scadenza e devono essere generati al momento della richiesta, non memorizzati nel database. Memorizzare un URL pre-firmato significherebbe avere un link valido solo per un periodo limitato e poi inutilizzabile, oltre a rappresentare un potenziale rischio se il database venisse compromesso.
Infine, è opportuno limitare i tipi di file accettati lato server, non solo lato client. Anche se Filament consente di filtrare i file tramite MIME type nel componente FileUpload, è buona pratica aggiungere una validazione esplicita nella request o nel controller:
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreDocumentRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'description' => ['nullable', 'string'],
// Validazione lato server del tipo di file
'file_path' => [
'required',
'file',
'max:51200',
'mimes:pdf,doc,docx,xls,xlsx,jpg,jpeg,png',
],
];
}
}
Conclusioni
L'integrazione tra Amazon S3, Laravel e Filament offre una soluzione robusta e scalabile per la gestione dei file nelle applicazioni web. Grazie all'astrazione fornita dal filesystem di Laravel, è possibile passare da un disco locale a S3 modificando poche righe di configurazione, mentre Filament si occupa di rendere l'interfaccia amministrativa intuitiva e funzionale.
I punti chiave dell'implementazione che abbiamo affrontato comprendono la configurazione del disco S3 tramite le variabili d'ambiente, l'utilizzo del componente FileUpload di Filament con visibilità privata, la generazione di URL pre-firmati per l'accesso controllato ai file, la pulizia automatica dei file tramite gli eventi Eloquent e l'isolamento dei test tramite Storage::fake(). Questo approccio garantisce che l'archiviazione dei file sia separata dall'applicazione stessa, rendendo il sistema più resiliente, più facile da scalare orizzontalmente e più semplice da mantenere nel tempo.