optimizations
This commit is contained in:
334
app/Services/CollectionSavedLibraryService.php
Normal file
334
app/Services/CollectionSavedLibraryService.php
Normal file
@@ -0,0 +1,334 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Collection;
|
||||
use App\Models\CollectionSave;
|
||||
use App\Models\CollectionSavedNote;
|
||||
use App\Models\CollectionSavedList;
|
||||
use App\Models\CollectionSavedListItem;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Collection as SupportCollection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class CollectionSavedLibraryService
|
||||
{
|
||||
/**
|
||||
* @param array<int, int> $collectionIds
|
||||
* @return array<int, array{saved_because:?string,last_viewed_at:?string}>
|
||||
*/
|
||||
public function saveMetadataFor(User $user, array $collectionIds): array
|
||||
{
|
||||
if ($collectionIds === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return CollectionSave::query()
|
||||
->where('user_id', $user->id)
|
||||
->whereIn('collection_id', $collectionIds)
|
||||
->get(['collection_id', 'save_context', 'save_context_meta_json', 'last_viewed_at'])
|
||||
->mapWithKeys(function (CollectionSave $save): array {
|
||||
return [
|
||||
(int) $save->collection_id => [
|
||||
'saved_because' => $this->savedBecauseLabel($save),
|
||||
'last_viewed_at' => $save->last_viewed_at?->toIso8601String(),
|
||||
],
|
||||
];
|
||||
})
|
||||
->all();
|
||||
}
|
||||
|
||||
public function recentlyRevisited(User $user, int $limit = 6): SupportCollection
|
||||
{
|
||||
$savedIds = CollectionSave::query()
|
||||
->where('user_id', $user->id)
|
||||
->whereNotNull('last_viewed_at')
|
||||
->orderByDesc('last_viewed_at')
|
||||
->limit(max(1, min($limit, 12)))
|
||||
->pluck('collection_id')
|
||||
->map(static fn ($id): int => (int) $id)
|
||||
->all();
|
||||
|
||||
if ($savedIds === []) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
$collections = Collection::query()
|
||||
->public()
|
||||
->with([
|
||||
'user:id,username,name',
|
||||
'coverArtwork:id,user_id,title,slug,hash,thumb_ext,published_at,is_public,is_approved,deleted_at',
|
||||
])
|
||||
->whereIn('id', $savedIds)
|
||||
->get()
|
||||
->keyBy('id');
|
||||
|
||||
return collect($savedIds)
|
||||
->map(fn (int $collectionId) => $collections->get($collectionId))
|
||||
->filter()
|
||||
->values();
|
||||
}
|
||||
|
||||
public function listsFor(User $user): array
|
||||
{
|
||||
return CollectionSavedList::query()
|
||||
->withCount('items')
|
||||
->where('user_id', $user->id)
|
||||
->orderBy('title')
|
||||
->get()
|
||||
->map(fn (CollectionSavedList $list) => [
|
||||
'id' => (int) $list->id,
|
||||
'title' => $list->title,
|
||||
'slug' => $list->slug,
|
||||
'items_count' => (int) $list->items_count,
|
||||
])
|
||||
->all();
|
||||
}
|
||||
|
||||
public function findListBySlugForUser(User $user, string $slug): CollectionSavedList
|
||||
{
|
||||
return CollectionSavedList::query()
|
||||
->withCount('items')
|
||||
->where('user_id', $user->id)
|
||||
->where('slug', $slug)
|
||||
->firstOrFail();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, int> $collectionIds
|
||||
* @return array<int, array<int, int>>
|
||||
*/
|
||||
public function membershipsFor(User $user, array $collectionIds): array
|
||||
{
|
||||
if ($collectionIds === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return DB::table('collection_saved_list_items as items')
|
||||
->join('collection_saved_lists as lists', 'lists.id', '=', 'items.saved_list_id')
|
||||
->where('lists.user_id', $user->id)
|
||||
->whereIn('items.collection_id', $collectionIds)
|
||||
->orderBy('items.saved_list_id')
|
||||
->get(['items.collection_id', 'items.saved_list_id'])
|
||||
->groupBy('collection_id')
|
||||
->map(fn ($rows) => collect($rows)->pluck('saved_list_id')->map(static fn ($id) => (int) $id)->values()->all())
|
||||
->mapWithKeys(fn ($listIds, $collectionId) => [(int) $collectionId => $listIds])
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, int> $collectionIds
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function notesFor(User $user, array $collectionIds): array
|
||||
{
|
||||
if ($collectionIds === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return CollectionSavedNote::query()
|
||||
->where('user_id', $user->id)
|
||||
->whereIn('collection_id', $collectionIds)
|
||||
->pluck('note', 'collection_id')
|
||||
->mapWithKeys(fn ($note, $collectionId) => [(int) $collectionId => (string) $note])
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, int>
|
||||
*/
|
||||
public function collectionIdsForList(User $user, CollectionSavedList $list): array
|
||||
{
|
||||
abort_unless((int) $list->user_id === (int) $user->id, 403);
|
||||
|
||||
return CollectionSavedListItem::query()
|
||||
->where('saved_list_id', $list->id)
|
||||
->orderBy('order_num')
|
||||
->pluck('collection_id')
|
||||
->map(static fn ($id) => (int) $id)
|
||||
->all();
|
||||
}
|
||||
|
||||
public function createList(User $user, string $title): CollectionSavedList
|
||||
{
|
||||
$slug = $this->uniqueSlug($user, $title);
|
||||
|
||||
return CollectionSavedList::query()->create([
|
||||
'user_id' => $user->id,
|
||||
'title' => $title,
|
||||
'slug' => $slug,
|
||||
]);
|
||||
}
|
||||
|
||||
public function addToList(User $user, CollectionSavedList $list, Collection $collection): CollectionSavedListItem
|
||||
{
|
||||
abort_unless((int) $list->user_id === (int) $user->id, 403);
|
||||
|
||||
$nextOrder = (int) (CollectionSavedListItem::query()->where('saved_list_id', $list->id)->max('order_num') ?? -1) + 1;
|
||||
|
||||
return CollectionSavedListItem::query()->firstOrCreate(
|
||||
[
|
||||
'saved_list_id' => $list->id,
|
||||
'collection_id' => $collection->id,
|
||||
],
|
||||
[
|
||||
'order_num' => $nextOrder,
|
||||
'created_at' => now(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function removeFromList(User $user, CollectionSavedList $list, Collection $collection): bool
|
||||
{
|
||||
abort_unless((int) $list->user_id === (int) $user->id, 403);
|
||||
|
||||
$deleted = CollectionSavedListItem::query()
|
||||
->where('saved_list_id', $list->id)
|
||||
->where('collection_id', $collection->id)
|
||||
->delete();
|
||||
|
||||
if ($deleted > 0) {
|
||||
$this->normalizeOrder($list);
|
||||
}
|
||||
|
||||
return $deleted > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, int|string> $orderedCollectionIds
|
||||
*/
|
||||
public function reorderList(User $user, CollectionSavedList $list, array $orderedCollectionIds): void
|
||||
{
|
||||
abort_unless((int) $list->user_id === (int) $user->id, 403);
|
||||
|
||||
$normalizedIds = collect($orderedCollectionIds)
|
||||
->map(static fn ($id) => (int) $id)
|
||||
->filter(static fn (int $id) => $id > 0)
|
||||
->values();
|
||||
|
||||
$currentIds = collect($this->collectionIdsForList($user, $list))->values();
|
||||
|
||||
if ($normalizedIds->count() !== $currentIds->count() || $normalizedIds->diff($currentIds)->isNotEmpty() || $currentIds->diff($normalizedIds)->isNotEmpty()) {
|
||||
throw ValidationException::withMessages([
|
||||
'collection_ids' => 'The submitted saved-list order is invalid.',
|
||||
]);
|
||||
}
|
||||
|
||||
DB::transaction(function () use ($list, $normalizedIds): void {
|
||||
/** @var SupportCollection<int, int> $itemIds */
|
||||
$itemIds = CollectionSavedListItem::query()
|
||||
->where('saved_list_id', $list->id)
|
||||
->whereIn('collection_id', $normalizedIds->all())
|
||||
->pluck('id', 'collection_id')
|
||||
->mapWithKeys(static fn ($id, $collectionId) => [(int) $collectionId => (int) $id]);
|
||||
|
||||
foreach ($normalizedIds as $index => $collectionId) {
|
||||
$itemId = $itemIds->get($collectionId);
|
||||
if (! $itemId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
CollectionSavedListItem::query()
|
||||
->whereKey($itemId)
|
||||
->update(['order_num' => $index]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function itemsCount(CollectionSavedList $list): int
|
||||
{
|
||||
return (int) CollectionSavedListItem::query()
|
||||
->where('saved_list_id', $list->id)
|
||||
->count();
|
||||
}
|
||||
|
||||
public function upsertNote(User $user, Collection $collection, ?string $note): ?CollectionSavedNote
|
||||
{
|
||||
$hasSavedCollection = DB::table('collection_saves')
|
||||
->where('user_id', $user->id)
|
||||
->where('collection_id', $collection->id)
|
||||
->exists();
|
||||
|
||||
if (! $hasSavedCollection) {
|
||||
throw ValidationException::withMessages([
|
||||
'collection' => 'You can only add notes to collections saved in your library.',
|
||||
]);
|
||||
}
|
||||
|
||||
$normalizedNote = trim((string) ($note ?? ''));
|
||||
|
||||
if ($normalizedNote === '') {
|
||||
CollectionSavedNote::query()
|
||||
->where('user_id', $user->id)
|
||||
->where('collection_id', $collection->id)
|
||||
->delete();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return CollectionSavedNote::query()->updateOrCreate(
|
||||
[
|
||||
'user_id' => $user->id,
|
||||
'collection_id' => $collection->id,
|
||||
],
|
||||
[
|
||||
'note' => $normalizedNote,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
private function uniqueSlug(User $user, string $title): string
|
||||
{
|
||||
$base = Str::slug(Str::limit($title, 80, '')) ?: 'saved-list';
|
||||
$slug = $base;
|
||||
$suffix = 2;
|
||||
|
||||
while (CollectionSavedList::query()->where('user_id', $user->id)->where('slug', $slug)->exists()) {
|
||||
$slug = $base . '-' . $suffix;
|
||||
$suffix++;
|
||||
}
|
||||
|
||||
return $slug;
|
||||
}
|
||||
|
||||
private function normalizeOrder(CollectionSavedList $list): void
|
||||
{
|
||||
$itemIds = CollectionSavedListItem::query()
|
||||
->where('saved_list_id', $list->id)
|
||||
->orderBy('order_num')
|
||||
->orderBy('id')
|
||||
->pluck('id');
|
||||
|
||||
foreach ($itemIds as $index => $itemId) {
|
||||
CollectionSavedListItem::query()
|
||||
->whereKey($itemId)
|
||||
->update(['order_num' => $index]);
|
||||
}
|
||||
}
|
||||
|
||||
private function savedBecauseLabel(CollectionSave $save): ?string
|
||||
{
|
||||
$context = trim((string) ($save->save_context ?? ''));
|
||||
$meta = is_array($save->save_context_meta_json) ? $save->save_context_meta_json : [];
|
||||
|
||||
return match ($context) {
|
||||
'collection_detail' => 'Saved from the collection page',
|
||||
'featured_collections' => 'Saved from featured collections',
|
||||
'featured_landing' => 'Saved from featured collections',
|
||||
'recommended_landing' => 'Saved from recommended collections',
|
||||
'trending_landing' => 'Saved from trending collections',
|
||||
'community_landing' => 'Saved from community collections',
|
||||
'editorial_landing' => 'Saved from editorial collections',
|
||||
'seasonal_landing' => 'Saved from seasonal collections',
|
||||
'collection_search' => ! empty($meta['query']) ? sprintf('Saved from search for "%s"', (string) $meta['query']) : 'Saved from collection search',
|
||||
'community_row', 'trending_row', 'editorial_row', 'seasonal_row', 'recent_row' => ! empty($meta['surface_label']) ? sprintf('Saved from %s', (string) $meta['surface_label']) : 'Saved from a collection rail',
|
||||
'program_landing' => ! empty($meta['program_label']) ? sprintf('Saved from the %s program', (string) $meta['program_label']) : (! empty($meta['program_key']) ? sprintf('Saved from the %s program', (string) $meta['program_key']) : 'Saved from a program landing'),
|
||||
'campaign_landing' => ! empty($meta['campaign_label']) ? sprintf('Saved during %s', (string) $meta['campaign_label']) : (! empty($meta['campaign_key']) ? sprintf('Saved during %s', (string) $meta['campaign_key']) : 'Saved from a campaign landing'),
|
||||
default => $context !== '' ? str_replace('_', ' ', ucfirst($context)) : null,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user