Commit workspace changes
This commit is contained in:
243
app/Services/GroupAssetService.php
Normal file
243
app/Services/GroupAssetService.php
Normal file
@@ -0,0 +1,243 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Group;
|
||||
use App\Models\GroupAsset;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class GroupAssetService
|
||||
{
|
||||
private const STORAGE_DISK = 'local';
|
||||
|
||||
public function __construct(
|
||||
private readonly GroupHistoryService $history,
|
||||
private readonly GroupActivityService $activity,
|
||||
) {
|
||||
}
|
||||
|
||||
public function store(Group $group, User $actor, array $attributes): GroupAsset
|
||||
{
|
||||
$file = $attributes['file'];
|
||||
if (! $file instanceof UploadedFile) {
|
||||
throw ValidationException::withMessages([
|
||||
'file' => 'A file upload is required for group assets.',
|
||||
]);
|
||||
}
|
||||
|
||||
$extension = strtolower((string) ($file->getClientOriginalExtension() ?: $file->extension() ?: 'bin'));
|
||||
$filename = (string) Str::uuid() . '.' . $extension;
|
||||
$directory = 'group-assets/' . (int) $group->id;
|
||||
$storedPath = $file->storeAs($directory, $filename, self::STORAGE_DISK);
|
||||
$mime = strtolower((string) ($file->getMimeType() ?: 'application/octet-stream'));
|
||||
|
||||
$asset = GroupAsset::query()->create([
|
||||
'group_id' => (int) $group->id,
|
||||
'title' => trim((string) $attributes['title']),
|
||||
'description' => $this->nullableString($attributes['description'] ?? null),
|
||||
'category' => (string) ($attributes['category'] ?? GroupAsset::CATEGORY_MISC),
|
||||
'file_path' => (string) $storedPath,
|
||||
'preview_path' => null,
|
||||
'visibility' => (string) ($attributes['visibility'] ?? GroupAsset::VISIBILITY_MEMBERS_ONLY),
|
||||
'status' => (string) ($attributes['status'] ?? GroupAsset::STATUS_ACTIVE),
|
||||
'linked_project_id' => $this->normalizeProjectId($group, $attributes['linked_project_id'] ?? null),
|
||||
'uploaded_by_user_id' => (int) $actor->id,
|
||||
'approved_by_user_id' => $group->canManageAssets($actor) ? (int) $actor->id : null,
|
||||
'is_featured' => (bool) ($attributes['is_featured'] ?? false),
|
||||
'file_meta_json' => [
|
||||
'original_name' => $file->getClientOriginalName(),
|
||||
'mime_type' => $mime,
|
||||
'size' => (int) $file->getSize(),
|
||||
'extension' => $extension,
|
||||
],
|
||||
]);
|
||||
|
||||
$this->history->record(
|
||||
$group,
|
||||
$actor,
|
||||
'asset_uploaded',
|
||||
sprintf('Uploaded asset "%s".', $asset->title),
|
||||
'group_asset',
|
||||
(int) $asset->id,
|
||||
null,
|
||||
$asset->only(['title', 'category', 'visibility', 'status'])
|
||||
);
|
||||
|
||||
$this->activity->record(
|
||||
$group,
|
||||
$actor,
|
||||
'asset_uploaded',
|
||||
'group_asset',
|
||||
(int) $asset->id,
|
||||
sprintf('%s uploaded a new group asset: %s', $actor->name ?: $actor->username ?: 'A member', $asset->title),
|
||||
$asset->description,
|
||||
$asset->visibility === GroupAsset::VISIBILITY_PUBLIC_DOWNLOAD ? 'public' : 'internal',
|
||||
);
|
||||
|
||||
return $asset->fresh(['group', 'uploader.profile', 'approver.profile', 'linkedProject']);
|
||||
}
|
||||
|
||||
public function update(GroupAsset $asset, User $actor, array $attributes): GroupAsset
|
||||
{
|
||||
$before = $asset->only(['title', 'description', 'category', 'visibility', 'status', 'linked_project_id', 'is_featured']);
|
||||
$wasActive = $asset->status === GroupAsset::STATUS_ACTIVE;
|
||||
|
||||
$asset->fill([
|
||||
'title' => trim((string) ($attributes['title'] ?? $asset->title)),
|
||||
'description' => array_key_exists('description', $attributes) ? $this->nullableString($attributes['description']) : $asset->description,
|
||||
'category' => (string) ($attributes['category'] ?? $asset->category),
|
||||
'visibility' => (string) ($attributes['visibility'] ?? $asset->visibility),
|
||||
'status' => (string) ($attributes['status'] ?? $asset->status),
|
||||
'linked_project_id' => array_key_exists('linked_project_id', $attributes) ? $this->normalizeProjectId($asset->group, $attributes['linked_project_id']) : $asset->linked_project_id,
|
||||
'is_featured' => (bool) ($attributes['is_featured'] ?? $asset->is_featured),
|
||||
'approved_by_user_id' => (string) ($attributes['status'] ?? $asset->status) === GroupAsset::STATUS_ACTIVE ? (int) $actor->id : $asset->approved_by_user_id,
|
||||
])->save();
|
||||
|
||||
if (! $wasActive && $asset->status === GroupAsset::STATUS_ACTIVE && $asset->uploader && (int) $asset->uploader->id !== (int) $actor->id) {
|
||||
app(NotificationService::class)->notifyGroupAssetApproved($asset->uploader, $actor, $asset->group, $asset);
|
||||
}
|
||||
|
||||
$this->history->record(
|
||||
$asset->group,
|
||||
$actor,
|
||||
'asset_updated',
|
||||
sprintf('Updated asset "%s".', $asset->title),
|
||||
'group_asset',
|
||||
(int) $asset->id,
|
||||
$before,
|
||||
$asset->only(['title', 'description', 'category', 'visibility', 'status', 'linked_project_id', 'is_featured'])
|
||||
);
|
||||
|
||||
return $asset->fresh(['group', 'uploader.profile', 'approver.profile', 'linkedProject']);
|
||||
}
|
||||
|
||||
public function studioListing(Group $group, User $viewer, array $filters = []): array
|
||||
{
|
||||
$bucket = (string) ($filters['bucket'] ?? 'all');
|
||||
$category = (string) ($filters['category'] ?? 'all');
|
||||
$search = trim((string) ($filters['q'] ?? ''));
|
||||
$page = max(1, (int) ($filters['page'] ?? 1));
|
||||
$perPage = min(max((int) ($filters['per_page'] ?? 20), 10), 50);
|
||||
|
||||
$query = GroupAsset::query()
|
||||
->with(['uploader.profile', 'approver.profile', 'linkedProject'])
|
||||
->where('group_id', $group->id);
|
||||
|
||||
if ($bucket !== 'all') {
|
||||
$query->where('visibility', $bucket);
|
||||
}
|
||||
|
||||
if ($category !== 'all') {
|
||||
$query->where('category', $category);
|
||||
}
|
||||
|
||||
if ($search !== '') {
|
||||
$query->where(function ($builder) use ($search): void {
|
||||
$builder->where('title', 'like', '%' . $search . '%')
|
||||
->orWhere('description', 'like', '%' . $search . '%')
|
||||
->orWhere('file_meta_json->original_name', 'like', '%' . $search . '%');
|
||||
});
|
||||
}
|
||||
|
||||
if (! $group->canViewInternalAssets($viewer)) {
|
||||
$query->whereIn('visibility', [GroupAsset::VISIBILITY_MEMBERS_ONLY, GroupAsset::VISIBILITY_PUBLIC_DOWNLOAD]);
|
||||
}
|
||||
|
||||
$paginator = $query->latest('updated_at')->paginate($perPage, ['*'], 'page', $page);
|
||||
|
||||
return [
|
||||
'items' => collect($paginator->items())->map(fn (GroupAsset $asset): array => $this->mapStudioAsset($asset))->values()->all(),
|
||||
'meta' => [
|
||||
'current_page' => $paginator->currentPage(),
|
||||
'last_page' => $paginator->lastPage(),
|
||||
'per_page' => $paginator->perPage(),
|
||||
'total' => $paginator->total(),
|
||||
],
|
||||
'filters' => [
|
||||
'bucket' => $bucket,
|
||||
'category' => $category,
|
||||
'q' => $search,
|
||||
],
|
||||
'bucket_options' => [
|
||||
['value' => 'all', 'label' => 'All'],
|
||||
['value' => GroupAsset::VISIBILITY_INTERNAL, 'label' => 'Internal'],
|
||||
['value' => GroupAsset::VISIBILITY_MEMBERS_ONLY, 'label' => 'Members only'],
|
||||
['value' => GroupAsset::VISIBILITY_PUBLIC_DOWNLOAD, 'label' => 'Public download'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function publicListing(Group $group, int $limit = 12): array
|
||||
{
|
||||
return GroupAsset::query()
|
||||
->with(['uploader.profile', 'linkedProject'])
|
||||
->where('group_id', $group->id)
|
||||
->where('status', GroupAsset::STATUS_ACTIVE)
|
||||
->where('visibility', GroupAsset::VISIBILITY_PUBLIC_DOWNLOAD)
|
||||
->latest('updated_at')
|
||||
->limit($limit)
|
||||
->get()
|
||||
->map(fn (GroupAsset $asset): array => $this->mapPublicAsset($asset))
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
public function downloadResponse(GroupAsset $asset): StreamedResponse
|
||||
{
|
||||
$name = (string) ($asset->file_meta_json['original_name'] ?? basename((string) $asset->file_path));
|
||||
$mime = (string) ($asset->file_meta_json['mime_type'] ?? 'application/octet-stream');
|
||||
|
||||
return Storage::disk(self::STORAGE_DISK)->download((string) $asset->file_path, $name, [
|
||||
'Content-Type' => $mime,
|
||||
]);
|
||||
}
|
||||
|
||||
public function mapStudioAsset(GroupAsset $asset): array
|
||||
{
|
||||
return [
|
||||
'id' => (int) $asset->id,
|
||||
'title' => (string) $asset->title,
|
||||
'description' => $asset->description,
|
||||
'category' => (string) $asset->category,
|
||||
'visibility' => (string) $asset->visibility,
|
||||
'status' => (string) $asset->status,
|
||||
'is_featured' => (bool) $asset->is_featured,
|
||||
'file_meta' => $asset->file_meta_json ?? [],
|
||||
'linked_project' => $asset->linkedProject ? ['id' => (int) $asset->linkedProject->id, 'title' => $asset->linkedProject->title] : null,
|
||||
'download_url' => route('groups.assets.download', ['group' => $asset->group, 'asset' => $asset]),
|
||||
'urls' => [
|
||||
'edit' => route('studio.groups.assets.update', ['group' => $asset->group, 'asset' => $asset]),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function mapPublicAsset(GroupAsset $asset): array
|
||||
{
|
||||
return [
|
||||
'id' => (int) $asset->id,
|
||||
'title' => (string) $asset->title,
|
||||
'description' => $asset->description,
|
||||
'category' => (string) $asset->category,
|
||||
'download_url' => route('groups.assets.download', ['group' => $asset->group, 'asset' => $asset]),
|
||||
];
|
||||
}
|
||||
|
||||
private function normalizeProjectId(Group $group, mixed $projectId): ?int
|
||||
{
|
||||
$id = (int) $projectId;
|
||||
return $id > 0 && $group->projects()->where('id', $id)->exists() ? $id : null;
|
||||
}
|
||||
|
||||
private function nullableString(mixed $value): ?string
|
||||
{
|
||||
$trimmed = trim((string) $value);
|
||||
return $trimmed !== '' ? $trimmed : null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user