32, 'sm' => 64, 'md' => 128, 'lg' => 256, 'xl' => 512, ]; protected $quality = 85; public function __construct() { // Guard: if Intervention Image is not installed, defer error until actual use if (class_exists(\Intervention\Image\ImageManagerStatic::class)) { try { Image::configure(['driver' => extension_loaded('gd') ? 'gd' : 'imagick']); $this->imageAvailable = true; } catch (\Throwable $e) { // If configuration fails, treat as unavailable and log for diagnostics logger()->warning('Intervention Image present but configuration failed: '.$e->getMessage()); $this->imageAvailable = false; } } else { $this->imageAvailable = false; } } /** * Process an uploaded file for a user and store webp sizes. * Returns the computed sha1 hash. * * @param int $userId * @param UploadedFile $file * @return string sha1 hash */ public function storeFromUploadedFile(int $userId, UploadedFile $file): string { if (! $this->imageAvailable) { throw new RuntimeException('Intervention Image is not available. If you just installed the package, restart your PHP process (php artisan serve or PHP-FPM) and run `composer dump-autoload -o`.'); } // Load image and re-encode to webp after validating try { $img = Image::make($file->getRealPath()); } catch (\Throwable $e) { throw new RuntimeException('Failed to read uploaded image: '.$e->getMessage()); } // Ensure square center crop per spec $max = max($img->width(), $img->height()); $img->fit($max, $max); $basePath = "avatars/{$userId}"; Storage::disk('public')->makeDirectory($basePath); // Save original as webp $originalData = (string) $img->encode('webp', $this->quality); Storage::disk('public')->put($basePath . '/original.webp', $originalData); // Generate sizes foreach ($this->sizes as $name => $size) { $resized = $img->resize($size, $size, function ($constraint) { $constraint->upsize(); })->encode('webp', $this->quality); Storage::disk('public')->put("{$basePath}/{$size}.webp", (string)$resized); } $hash = sha1($originalData); $mime = 'image/webp'; // Persist metadata to user_profiles if exists, otherwise users table fallbacks if (SchemaHasTable('user_profiles')) { DB::table('user_profiles')->where('user_id', $userId)->update([ 'avatar_hash' => $hash, 'avatar_updated_at' => Carbon::now(), 'avatar_mime' => $mime, ]); } else { DB::table('users')->where('id', $userId)->update([ 'avatar_hash' => $hash, 'avatar_updated_at' => Carbon::now(), 'avatar_mime' => $mime, ]); } return $hash; } /** * Process a legacy file path for a user (path-to-file). * Returns sha1 or null when missing. * * @param int $userId * @param string $path Absolute filesystem path * @return string|null */ public function storeFromLegacyFile(int $userId, string $path): ?string { if (!file_exists($path) || !is_readable($path)) { return null; } try { $img = Image::make($path); } catch (\Exception $e) { return null; } $max = max($img->width(), $img->height()); $img->fit($max, $max); $basePath = "avatars/{$userId}"; Storage::disk('public')->makeDirectory($basePath); $originalData = (string) $img->encode('webp', $this->quality); Storage::disk('public')->put($basePath . '/original.webp', $originalData); foreach ($this->sizes as $name => $size) { $resized = $img->resize($size, $size, function ($constraint) { $constraint->upsize(); })->encode('webp', $this->quality); Storage::disk('public')->put("{$basePath}/{$size}.webp", (string)$resized); } $hash = sha1($originalData); $mime = 'image/webp'; if (SchemaHasTable('user_profiles')) { DB::table('user_profiles')->where('user_id', $userId)->update([ 'avatar_hash' => $hash, 'avatar_updated_at' => Carbon::now(), 'avatar_mime' => $mime, ]); } else { DB::table('users')->where('id', $userId)->update([ 'avatar_hash' => $hash, 'avatar_updated_at' => Carbon::now(), 'avatar_mime' => $mime, ]); } return $hash; } } /** * Helper: check for table existence without importing Schema facade repeatedly */ function SchemaHasTable(string $name): bool { try { return \Illuminate\Support\Facades\Schema::hasTable($name); } catch (\Throwable $e) { return false; } }