components([ Select::make('type') ->options([ 'article' => 'Article', 'card' => 'Card', 'text' => 'Text', 'image' => 'Image', ]) ->default('article') ->required() ->live(), TextInput::make('title') ->required() ->live(onBlur: true) ->afterStateUpdated(function ($state, $set): void { $set('slug', Str::slug((string) $state)); }), TextInput::make('slug') ->required() ->visible(fn($get) => $get('type') === 'article') ->dehydrated() ->readOnly(), Textarea::make('description') ->visible(fn($get) => $get('type') === 'article') ->columnSpanFull(), SpatieTagsInput::make('tags') ->type('entry-tags') ->visible(fn($get) => $get('type') === 'article') ->columnSpanFull(), SpatieMediaLibraryFileUpload::make('featured_image') ->multiple() // <- force array handling for Filament v4 bug ->visible( fn($get) => $get('type') === 'article' || $get('type') === 'image' ) ->collection('featured-image') ->image() ->imageEditor() ->disk(config('media-library.disk_name', 'public')) ->visibility('public') ->columnSpanFull() ->dehydrated(false) ->saveUploadedFileUsing(function ($file, $record) { if (is_array($file)) { $file = reset($file); } // Validate upload object early if ( ! is_object($file) || ! (method_exists($file, 'getRealPath') || method_exists($file, 'getPathname') || method_exists($file, 'getStream') || method_exists($file, 'store')) ) { Log::error('Invalid upload object', ['type' => gettype($file)]); throw new \Exception('Invalid upload object provided to saveUploadedFileUsing'); } // Use safe variables for further calls $realPath = method_exists($file, 'getRealPath') ? $file->getRealPath() : null; $exists = $realPath ? file_exists($realPath) : false; $name = method_exists($file, 'getClientOriginalName') ? $file->getClientOriginalName() : null; Log::info('TemporaryUploadedFile Debug', [ 'path' => $file->getRealPath(), 'exists' => file_exists($file->getRealPath()), 'name' => $file->getClientOriginalName(), 'temp_dir' => sys_get_temp_dir(), 'disk_root' => config('filesystems.disks.local.root'), 'is_readable' => is_readable($file->getRealPath()), 'is_writable' => is_writable($file->getRealPath()), ]); // Additional debug: Check if the file is being moved to livewire-tmp $livewireTmpPath = storage_path('framework/livewire-tmp'); Log::info('Livewire Temp Directory Debug', [ 'livewire_tmp_path' => $livewireTmpPath, 'exists' => file_exists($livewireTmpPath), 'is_writable' => is_writable($livewireTmpPath), ]); // Check if the file is being moved $tempFilePath = $file->getRealPath(); $newFilePath = $livewireTmpPath . '/' . $file->getClientOriginalName(); if (file_exists($tempFilePath)) { Log::info('File exists in temp directory', ['temp_file_path' => $tempFilePath]); } else { Log::error('File does not exist in temp directory', ['temp_file_path' => $tempFilePath]); } // $diskName = config('media-library.disk_name', 'public'); $diskName = config('media-library.disk_name'); if (config('logging.channels.' . config('logging.default') . '.level') === 'debug') { Log::info('Featured Image Upload Debug', [ 'disk' => $diskName, 'file_name' => $file->getClientOriginalName(), 'file_size' => $file->getSize(), 'file_mime' => $file->getMimeType(), 'file_path' => $file->getRealPath(), 'record_id' => $record?->id, 'aws_config' => [ 'bucket' => config('filesystems.disks.s3.bucket'), 'region' => config('filesystems.disks.s3.region'), 'key_exists' => !empty(config('filesystems.disks.s3.key')), 'secret_exists' => !empty(config('filesystems.disks.s3.secret')), ] ]); } try { if (!$record) { throw new \Exception('Record not found during upload'); } // Test S3 connection if using S3 if ($diskName === 's3') { $disk = \Storage::disk('s3'); // Test basic S3 connectivity $testFile = 'test-' . time() . '.txt'; $disk->put($testFile, 'test content'); $disk->delete($testFile); if (config('logging.channels.' . config('logging.default') . '.level') === 'debug') { Log::info('S3 connectivity test passed'); } } // Use addMedia with the file directly, not addMediaFromRequest // Generate secure filename similar to Livewire temp files $originalName = $file->getClientOriginalName(); $extension = pathinfo($originalName, PATHINFO_EXTENSION); $baseName = pathinfo($originalName, PATHINFO_FILENAME); // Generate secure filename with encoded original name $encodedName = base64_encode($originalName); $secureFileName = Str::random(32) . '-meta' . $encodedName . '-.' . $extension; // Resolve possibly-relative Livewire temp path + safe fallbacks $realPath = $realPath ?: (method_exists($file, 'getRealPath') ? $file->getRealPath() : null); $candidates = []; if ($realPath && str_starts_with($realPath, '/')) { $candidates[] = $realPath; } else { $candidates[] = sys_get_temp_dir() . '/' . ltrim((string)$realPath, '/'); $candidates[] = storage_path('framework/' . ltrim((string)$realPath, '/')); $candidates[] = storage_path(ltrim((string)$realPath, '/')); $candidates[] = base_path(ltrim((string)$realPath, '/')); } if ($realPath) { $candidates[] = storage_path('framework/livewire-tmp/' . basename($realPath)); $candidates[] = sys_get_temp_dir() . '/' . basename($realPath); } // 1) Try storing to local disk (creates an absolute path we control) $stored = null; if (method_exists($file, 'store')) { try { $stored = $file->store('livewire-temp', 'local'); // storage/app/livewire-temp/... if ($stored && \Storage::disk('local')->exists($stored)) { $resolved = \Storage::disk('local')->path($stored); } } catch (\Throwable $e) { Log::debug('store() fallback failed', ['err' => $e->getMessage()]); } } // 2) If not resolved, check candidates if (! isset($resolved)) { foreach ($candidates as $p) { if ($p && file_exists($p)) { $resolved = $p; break; } } } // 3) If still not resolved, try stream -> temp file copy $is_tmp_copy = false; if (! isset($resolved)) { try { $stream = null; if (method_exists($file, 'getStream')) { $stream = $file->getStream(); } elseif (method_exists($file, 'getRealPath') && is_readable($file->getRealPath())) { $stream = fopen($file->getRealPath(), 'r'); } if ($stream) { $tmpPath = tempnam(sys_get_temp_dir(), 'filament-upload-'); $out = fopen($tmpPath, 'w'); stream_copy_to_stream($stream, $out); fclose($out); if (is_resource($stream)) { @fclose($stream); } $resolved = $tmpPath; $is_tmp_copy = true; } } catch (\Throwable $e) { Log::debug('stream fallback failed', ['err' => $e->getMessage()]); } } // 4) Still nothing -> error if (empty($resolved)) { Log::error('Featured Image Upload: could not resolve temp path', [ 'original' => $realPath, 'checked_candidates' => $candidates, 'stored' => $stored, ]); throw new \Exception("File `{$realPath}` does not exist"); } // 5) Use resolved absolute path $media = $record->addMedia($resolved) ->usingName($baseName) ->usingFileName($secureFileName) ->toMediaCollection('featured-image', $diskName); // 6) Cleanup short-lived artifacts if (! empty($is_tmp_copy) && file_exists($resolved)) { @unlink($resolved); } if (! empty($stored) && \Storage::disk('local')->exists($stored)) { \Storage::disk('local')->delete($stored); } Log::info('Featured image resolved', ['resolved' => $resolved, 'media_id' => $media->id ?? null]); if (config('logging.channels.' . config('logging.default') . '.level') === 'debug') { Log::info('Featured Image Upload Success', [ 'media_id' => $media->id, 'media_url' => $media->getUrl(), 'media_path' => $media->getPathRelativeToRoot(), 'disk' => $media->disk ]); } return $media->getUrl(); } catch (\Exception $e) { Log::error('Featured Image Upload Failed', [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString(), 'disk' => $diskName, 'file_name' => $file->getClientOriginalName() ]); // Also show error to user \Filament\Notifications\Notification::make() ->danger() ->title('Upload Failed') ->body($e->getMessage()) ->persistent() ->send(); throw $e; } }) ->hintAction( Action::make('featured_picker') ->label('Featured Image from Gallery') ->icon('heroicon-m-photo') ->extraAttributes(['id' => 'featured-picker-button']) ->schema([ Select::make('image_id') ->label('Select an existing image') ->allowHtml() ->options(function () { $currentDisk = config('media-library.disk_name', 'public'); return Media::where('disk', $currentDisk) ->latest() ->limit(50) ->get(['id', 'file_name', 'name', 'disk']) ->mapWithKeys(function (Media $item) { try { $url = $item->getUrl(); $fileName = e($item->file_name); $name = e($item->name ?? ''); $html = "
" . "{$fileName}" . "
" . "{$name}" . "{$fileName}" . '
'; return [$item->id => $html]; } catch (\Exception $e) { return []; } })->toArray(); }) ->searchable() ->preload() ->required(), ]) ->action(function (array $data, SpatieMediaLibraryFileUpload $component): void { $record = $component->getRecord(); $diskName = config('media-library.disk_name', 'public'); if (config('logging.channels.' . config('logging.default') . '.level') === 'debug') { Log::info('Featured Image Picker Action Debug', [ 'disk' => $diskName, 'record_id' => $record?->id, 'image_id' => $data['image_id'] ?? null ]); } if (! $record) { \Filament\Notifications\Notification::make() ->warning() ->title('Save the entry first') ->send(); return; } if (! $data['image_id']) { return; } $sourceMedia = Media::find($data['image_id']); if (! $sourceMedia) { Log::error('Source media not found', ['image_id' => $data['image_id']]); \Filament\Notifications\Notification::make() ->danger() ->title('Source image not found in database') ->send(); return; } try { // For S3, we need to handle file copying differently if ($sourceMedia->disk === 's3') { if (config('logging.channels.' . config('logging.default') . '.level') === 'debug') { Log::info('Copying S3 media to new collection', [ 'source_disk' => $sourceMedia->disk, 'source_path' => $sourceMedia->getPathRelativeToRoot(), 'target_disk' => $diskName ]); } // Copy from S3 to S3 or download temporarily $sourceDisk = \Storage::disk($sourceMedia->disk); $sourceContent = $sourceDisk->get($sourceMedia->getPathRelativeToRoot()); if (!$sourceContent) { throw new \Exception('Could not read source file from S3'); } $tempCopy = sys_get_temp_dir() . '/' . uniqid() . '_' . $sourceMedia->file_name; file_put_contents($tempCopy, $sourceContent); } else { // Local file handling $sourceFile = $sourceMedia->getPath(); if (! file_exists($sourceFile)) { throw new \Exception('Source file not found on disk'); } $tempCopy = sys_get_temp_dir() . '/' . uniqid() . '_' . $sourceMedia->file_name; copy($sourceFile, $tempCopy); } // Verify record has ID if (! $record->id) { \Filament\Notifications\Notification::make() ->danger() ->title('Entry must be saved first') ->send(); return; } // Add the copy to the entry's featured-image collection $newMedia = $record->addMedia($tempCopy) ->usingName($sourceMedia->name ?: pathinfo($sourceMedia->file_name, PATHINFO_FILENAME)) ->usingFileName($sourceMedia->file_name) ->toMediaCollection('featured-image', $diskName); if (config('logging.channels.' . config('logging.default') . '.level') === 'debug') { Log::info('Featured Image Picker Success', [ 'new_media_id' => $newMedia->id, 'new_media_disk' => $newMedia->disk, 'new_media_url' => $newMedia->getUrl() ]); } // Dispatch event for app.js to handle $component->getLivewire()->dispatch('featured-image-added', ['mediaId' => $newMedia->id]); \Filament\Notifications\Notification::make() ->success() ->title('Image added to featured image') ->send(); } catch (\Exception $e) { Log::error('Featured Image Picker Failed', [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString(), 'source_media_id' => $data['image_id'], 'disk' => $diskName ]); \Filament\Notifications\Notification::make() ->danger() ->title('Error: ' . $e->getMessage()) ->persistent() ->send(); } finally { if (isset($tempCopy) && file_exists($tempCopy)) { unlink($tempCopy); } } }) ), Toggle::make('is_published') ->required(), Toggle::make('is_featured') ->required(), TextInput::make('priority') ->label('Priority') ->numeric() ->default(0) ->required(), DatePicker::make('published_at') ->visible(fn($get) => $get('type') === 'article'), Select::make('category_id') ->label('Category') ->options(function () { return Category::all() ->pluck('name', 'id') ->toArray(); }) ->searchable(), TextInput::make('call_to_action_text') ->label('Call to Action Text') ->visible(fn($get) => $get('type') !== 'article'), TextInput::make('call_to_action_link') ->label('Call to Action URL') ->visible(fn($get) => $get('type') !== 'article'), RichEditor::make('content') ->visible(fn($get) => $get('type') !== 'image') ->columnSpanFull() ->hintAction( Action::make('picker') ->label('Gallery Picker') ->icon('heroicon-m-photo') ->schema([ Select::make('image_url') ->label('Select an existing image') ->allowHtml() ->options(function () { return Media::latest() ->limit(30) // Limit to 30 most recent items for performance ->get(['id', 'file_name', 'name', 'uuid', 'collection_name', 'model_type', 'model_id', 'disk']) ->filter(function (Media $item) { // Only include media items that have a valid disk return $item->disk !== null; }) ->mapWithKeys(function (Media $item) { try { $url = $item->getUrl(); } catch (\Exception $e) { // Skip items that can't generate URLs return []; } $fileName = e($item->file_name); $name = e($item->name ?? ''); // Smaller image preview for better performance $html = "
" . "{$fileName}" . "
" . "{$name}" . "{$fileName}" . '
'; return [$url => $html]; })->toArray(); }) ->searchable() ->preload() ->required(), ]) ->action(function (array $data, RichEditor $component) { // We dispatch the URL to the browser to be inserted into TipTap $component->getLivewire()->dispatch('insert-editor-content', [ 'statePath' => $component->getStatePath(), 'html' => "", ]); }) ), ]); } }