243 lines
10 KiB
PHP
243 lines
10 KiB
PHP
<?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;
|
|
}
|
|
} |