Files
Twyco afe99ea6d4
CI/CD / test_and_check_app (push) Failing after 7m26s
CI/CD / Publish immutable image (push) Skipped
feat: add containerized deployment workflow
2026-08-22 08:57:18 +02:00

130 lines
3.4 KiB
PHP

<?php
namespace App\Actions\Users;
use App\Models\User;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use RuntimeException;
class UpdateUserAvatar
{
/**
* @var list<string>
*/
private const COLORS = [
'#F97316',
'#22C55E',
'#06B6D4',
'#EC4899',
'#8B5CF6',
'#EAB308',
'#EF4444',
'#14B8A6',
];
public function generate(User $user): User
{
$oldPath = $user->avatar_path;
$color = $this->colorFor($user);
$path = sprintf('avatars/generated/%d-%s.svg', $user->id, (string) Str::uuid());
$stored = Storage::disk($this->avatarDisk())->put(
$path,
$this->svg($this->initials($user->name), $color),
['ContentType' => 'image/svg+xml'],
);
if (! $stored) {
throw new RuntimeException('Generated avatar could not be stored.');
}
$user->forceFill([
'avatar_path' => $path,
'avatar_color' => $color,
'avatar_is_generated' => true,
])->saveQuietly();
$this->deletePath($oldPath);
return $user->refresh();
}
public function upload(User $user, UploadedFile $avatar): User
{
$oldPath = $user->avatar_path;
$path = $avatar->store('avatars/uploads', $this->avatarDisk());
if (! is_string($path)) {
throw new RuntimeException('Uploaded avatar could not be stored.');
}
$user->forceFill([
'avatar_path' => $path,
'avatar_color' => $this->colorFor($user),
'avatar_is_generated' => false,
])->saveQuietly();
$this->deletePath($oldPath);
return $user->refresh();
}
public function delete(User $user): void
{
$this->deletePath($user->avatar_path);
}
private function colorFor(User $user): string
{
if (is_string($user->avatar_color) && in_array($user->avatar_color, self::COLORS, true)) {
return $user->avatar_color;
}
return self::COLORS[random_int(0, count(self::COLORS) - 1)];
}
private function deletePath(?string $path): void
{
if (blank($path)) {
return;
}
Storage::disk($this->avatarDisk())->delete($path);
}
private function avatarDisk(): string
{
$disk = config('filesystems.avatar_disk', 'public');
return is_string($disk) && $disk !== '' ? $disk : 'public';
}
private function initials(string $name): string
{
$parts = preg_split('/\s+/u', trim($name)) ?: [];
if ($parts === []) {
return '?';
}
$first = mb_substr($parts[0], 0, 1);
$last = count($parts) > 1 ? mb_substr($parts[count($parts) - 1], 0, 1) : '';
return mb_strtoupper($first.$last);
}
private function svg(string $initials, string $color): string
{
$escapedInitials = htmlspecialchars($initials, ENT_QUOTES | ENT_XML1, 'UTF-8');
return <<<SVG
<svg xmlns="http://www.w3.org/2000/svg" width="192" height="192" viewBox="0 0 192 192" role="img" aria-label="Profile avatar">
<rect width="192" height="192" rx="42" fill="{$color}"/>
<text x="50%" y="52%" dominant-baseline="middle" text-anchor="middle" fill="#ffffff" font-family="Inter, ui-sans-serif, system-ui, sans-serif" font-size="72" font-weight="700">{$escapedInitials}</text>
</svg>
SVG;
}
}