Upload beautify
This commit is contained in:
169
app/Services/AvatarService.php
Normal file
169
app/Services/AvatarService.php
Normal file
@@ -0,0 +1,169 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user