170 lines
5.4 KiB
PHP
170 lines
5.4 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Intervention\Image\ImageManagerStatic as Image;
|
|
use RuntimeException;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Http\UploadedFile;
|
|
|
|
class AvatarService
|
|
{
|
|
protected $sizes = [
|
|
'xs' => 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;
|
|
}
|
|
}
|