This commit is contained in:
1 parent
054e970df9
commit
1c98a634c3
50 files changed
+1666
-593
No files matched your search
@@ -0,0 +1,313 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\Dto\AlbumSearchCriteria;
|
||||
use App\Dto\AlbumSearchResult;
|
||||
use App\Entity\Album;
|
||||
use App\Repository\AlbumRepository;
|
||||
use App\Repository\ReviewRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
/**
|
||||
* AlbumSearchService composes Spotify and user albums into reusable payloads.
|
||||
*/
|
||||
class AlbumSearchService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly SpotifyClient $spotify,
|
||||
private readonly AlbumRepository $albumRepository,
|
||||
private readonly ReviewRepository $reviewRepository,
|
||||
private readonly EntityManagerInterface $em,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {
|
||||
}
|
||||
|
||||
public function search(AlbumSearchCriteria $criteria): AlbumSearchResult
|
||||
{
|
||||
$spotifyQuery = $this->buildSpotifyQuery($criteria);
|
||||
$hasUserFilters = $this->hasUserFilters($criteria, $spotifyQuery);
|
||||
|
||||
$stats = [];
|
||||
$savedIds = [];
|
||||
$spotifyPayloads = [];
|
||||
$userPayloads = [];
|
||||
|
||||
if ($criteria->useSpotify() && $spotifyQuery !== '') {
|
||||
$spotifyData = $this->resolveSpotifyAlbums($criteria, $spotifyQuery);
|
||||
$spotifyPayloads = $spotifyData['payloads'];
|
||||
$stats = $this->mergeStats($stats, $spotifyData['stats']);
|
||||
$savedIds = $this->mergeSavedIds($savedIds, $spotifyData['savedIds']);
|
||||
}
|
||||
|
||||
if ($criteria->useUser() && $hasUserFilters) {
|
||||
$userData = $this->resolveUserAlbums($criteria);
|
||||
$userPayloads = $userData['payloads'];
|
||||
$stats = $this->mergeStats($stats, $userData['stats']);
|
||||
}
|
||||
|
||||
$albums = $this->composeAlbumList($criteria->source, $userPayloads, $spotifyPayloads, $criteria->limit);
|
||||
|
||||
return new AlbumSearchResult($criteria, $albums, $stats, $savedIds);
|
||||
}
|
||||
|
||||
private function buildSpotifyQuery(AlbumSearchCriteria $criteria): string
|
||||
{
|
||||
$parts = [];
|
||||
if ($criteria->albumName !== '') {
|
||||
$parts[] = 'album:' . $criteria->albumName;
|
||||
}
|
||||
if ($criteria->artist !== '') {
|
||||
$parts[] = 'artist:' . $criteria->artist;
|
||||
}
|
||||
if ($criteria->yearFrom !== null || $criteria->yearTo !== null) {
|
||||
if ($criteria->yearFrom !== null && $criteria->yearTo !== null && $criteria->yearTo >= $criteria->yearFrom) {
|
||||
$parts[] = 'year:' . $criteria->yearFrom . '-' . $criteria->yearTo;
|
||||
} else {
|
||||
$year = $criteria->yearFrom ?? $criteria->yearTo;
|
||||
if ($year !== null) {
|
||||
$parts[] = 'year:' . $year;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($criteria->query !== '') {
|
||||
$parts[] = $criteria->query;
|
||||
}
|
||||
return implode(' ', $parts);
|
||||
}
|
||||
|
||||
private function hasUserFilters(AlbumSearchCriteria $criteria, string $spotifyQuery): bool
|
||||
{
|
||||
return $spotifyQuery !== ''
|
||||
|| $criteria->albumName !== ''
|
||||
|| $criteria->artist !== ''
|
||||
|| $criteria->yearFrom !== null
|
||||
|| $criteria->yearTo !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{payloads:array<int,array<mixed>>,stats:array<string,array{count:int,avg:float}>,savedIds:array<int,string>}
|
||||
*/
|
||||
private function resolveSpotifyAlbums(AlbumSearchCriteria $criteria, string $spotifyQuery): array
|
||||
{
|
||||
$stored = $this->albumRepository->searchSpotifyAlbums(
|
||||
$spotifyQuery,
|
||||
$criteria->albumName,
|
||||
$criteria->artist,
|
||||
$criteria->yearFrom ?? 0,
|
||||
$criteria->yearTo ?? 0,
|
||||
$criteria->limit
|
||||
);
|
||||
$storedPayloads = array_map(static fn(Album $album) => $album->toTemplateArray(), $stored);
|
||||
$storedIds = $this->collectSpotifyIds($stored);
|
||||
$stats = $storedIds ? $this->reviewRepository->getAggregatesForAlbumIds($storedIds) : [];
|
||||
$savedIds = $storedIds;
|
||||
|
||||
if (count($stored) >= $criteria->limit) {
|
||||
return [
|
||||
'payloads' => array_slice($storedPayloads, 0, $criteria->limit),
|
||||
'stats' => $stats,
|
||||
'savedIds' => $savedIds,
|
||||
];
|
||||
}
|
||||
|
||||
$apiPayloads = $this->fetchSpotifyPayloads($criteria, $spotifyQuery, $storedPayloads);
|
||||
$payloads = $this->mergePayloadLists($apiPayloads['payloads'], $storedPayloads, $criteria->limit);
|
||||
$stats = $this->mergeStats($stats, $apiPayloads['stats']);
|
||||
$savedIds = $this->mergeSavedIds($savedIds, $apiPayloads['savedIds']);
|
||||
|
||||
return ['payloads' => $payloads, 'stats' => $stats, 'savedIds' => $savedIds];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int,array<mixed>> $storedPayloads
|
||||
* @return array{payloads:array<int,array<mixed>>,stats:array<string,array{count:int,avg:float}>,savedIds:array<int,string>}
|
||||
*/
|
||||
private function fetchSpotifyPayloads(AlbumSearchCriteria $criteria, string $spotifyQuery, array $storedPayloads): array
|
||||
{
|
||||
$result = $this->spotify->searchAlbums($spotifyQuery, $criteria->limit);
|
||||
$searchItems = $result['albums']['items'] ?? [];
|
||||
$this->logger->info('Album search results received', [
|
||||
'query' => $spotifyQuery,
|
||||
'items' => is_countable($searchItems) ? count($searchItems) : 0,
|
||||
]);
|
||||
|
||||
if (!$searchItems) {
|
||||
return ['payloads' => [], 'stats' => [], 'savedIds' => []];
|
||||
}
|
||||
|
||||
$ids = $this->extractSpotifyIds($searchItems);
|
||||
if ($ids === []) {
|
||||
return ['payloads' => [], 'stats' => [], 'savedIds' => []];
|
||||
}
|
||||
|
||||
$full = $this->spotify->getAlbums($ids);
|
||||
$albumsPayload = is_array($full) ? ($full['albums'] ?? []) : [];
|
||||
if ($albumsPayload === [] && $searchItems !== []) {
|
||||
$albumsPayload = $searchItems;
|
||||
$this->logger->warning('Spotify getAlbums returned empty; falling back to search items', ['count' => count($albumsPayload)]);
|
||||
}
|
||||
|
||||
$upserted = 0;
|
||||
foreach ($albumsPayload as $payload) {
|
||||
$this->albumRepository->upsertFromSpotifyAlbum((array) $payload);
|
||||
$upserted++;
|
||||
}
|
||||
$this->em->flush();
|
||||
$this->logger->info('Albums upserted to DB', ['upserted' => $upserted]);
|
||||
|
||||
$existing = $this->albumRepository->findBySpotifyIdsKeyed($ids);
|
||||
$payloads = [];
|
||||
foreach ($ids as $sid) {
|
||||
if (isset($existing[$sid])) {
|
||||
$payloads[] = $existing[$sid]->toTemplateArray();
|
||||
}
|
||||
}
|
||||
|
||||
$stats = $this->reviewRepository->getAggregatesForAlbumIds($ids);
|
||||
|
||||
return [
|
||||
'payloads' => $payloads,
|
||||
'stats' => $stats,
|
||||
'savedIds' => array_keys($existing),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{payloads:array<int,array<mixed>>,stats:array<string,array{count:int,avg:float}>}
|
||||
*/
|
||||
private function resolveUserAlbums(AlbumSearchCriteria $criteria): array
|
||||
{
|
||||
$userAlbums = $this->albumRepository->searchUserAlbums(
|
||||
$criteria->query,
|
||||
$criteria->albumName,
|
||||
$criteria->artist,
|
||||
$criteria->yearFrom ?? 0,
|
||||
$criteria->yearTo ?? 0,
|
||||
$criteria->limit
|
||||
);
|
||||
if ($userAlbums === []) {
|
||||
return ['payloads' => [], 'stats' => []];
|
||||
}
|
||||
|
||||
$entityIds = array_values(array_map(static fn(Album $album) => (int) $album->getId(), $userAlbums));
|
||||
$userStats = $this->reviewRepository->getAggregatesForAlbumEntityIds($entityIds);
|
||||
$payloads = array_map(static fn(Album $album) => $album->toTemplateArray(), $userAlbums);
|
||||
|
||||
return [
|
||||
'payloads' => $payloads,
|
||||
'stats' => $this->mapUserStatsToLocalIds($userAlbums, $userStats),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<Album> $userAlbums
|
||||
* @param array<int,array{count:int,avg:float}> $userStats
|
||||
* @return array<string,array{count:int,avg:float}>
|
||||
*/
|
||||
private function mapUserStatsToLocalIds(array $userAlbums, array $userStats): array
|
||||
{
|
||||
$mapped = [];
|
||||
foreach ($userAlbums as $album) {
|
||||
$entityId = (int) $album->getId();
|
||||
$localId = (string) $album->getLocalId();
|
||||
if ($localId !== '' && isset($userStats[$entityId])) {
|
||||
$mapped[$localId] = $userStats[$entityId];
|
||||
}
|
||||
}
|
||||
return $mapped;
|
||||
}
|
||||
|
||||
private function composeAlbumList(string $source, array $userPayloads, array $spotifyPayloads, int $limit): array
|
||||
{
|
||||
if ($source === 'user') {
|
||||
return array_slice($userPayloads, 0, $limit);
|
||||
}
|
||||
if ($source === 'spotify') {
|
||||
return array_slice($spotifyPayloads, 0, $limit);
|
||||
}
|
||||
return array_slice(array_merge($userPayloads, $spotifyPayloads), 0, $limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<Album> $albums
|
||||
* @return list<string>
|
||||
*/
|
||||
private function collectSpotifyIds(array $albums): array
|
||||
{
|
||||
$ids = [];
|
||||
foreach ($albums as $album) {
|
||||
$sid = (string) $album->getSpotifyId();
|
||||
if ($sid !== '') {
|
||||
$ids[] = $sid;
|
||||
}
|
||||
}
|
||||
return array_values(array_unique($ids));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int,mixed> $searchItems
|
||||
* @return list<string>
|
||||
*/
|
||||
private function extractSpotifyIds(array $searchItems): array
|
||||
{
|
||||
$ids = [];
|
||||
foreach ($searchItems as $item) {
|
||||
$id = isset($item['id']) ? (string) $item['id'] : '';
|
||||
if ($id !== '') {
|
||||
$ids[] = $id;
|
||||
}
|
||||
}
|
||||
return array_values(array_unique($ids));
|
||||
}
|
||||
|
||||
private function mergeStats(array $current, array $updates): array
|
||||
{
|
||||
foreach ($updates as $key => $value) {
|
||||
$current[$key] = $value;
|
||||
}
|
||||
return $current;
|
||||
}
|
||||
|
||||
private function mergeSavedIds(array $current, array $updates): array
|
||||
{
|
||||
$merged = array_merge($current, array_filter($updates, static fn($id) => $id !== ''));
|
||||
return array_values(array_unique($merged));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int,array<mixed>> $primary
|
||||
* @param array<int,array<mixed>> $secondary
|
||||
* @return array<int,array<mixed>>
|
||||
*/
|
||||
private function mergePayloadLists(array $primary, array $secondary, int $limit): array
|
||||
{
|
||||
$seen = [];
|
||||
$merged = [];
|
||||
foreach ($primary as $payload) {
|
||||
$merged[] = $payload;
|
||||
if (isset($payload['id'])) {
|
||||
$seen[$payload['id']] = true;
|
||||
}
|
||||
if (count($merged) >= $limit) {
|
||||
return array_slice($merged, 0, $limit);
|
||||
}
|
||||
}
|
||||
foreach ($secondary as $payload) {
|
||||
$id = $payload['id'] ?? null;
|
||||
if ($id !== null && isset($seen[$id])) {
|
||||
continue;
|
||||
}
|
||||
$merged[] = $payload;
|
||||
if ($id !== null) {
|
||||
$seen[$id] = true;
|
||||
}
|
||||
if (count($merged) >= $limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return array_slice($merged, 0, $limit);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user