diff --git a/CHANGELOG.md b/CHANGELOG.md index 887bb730..13444a36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +- [#235](https://github.com/os2display/display-api-service/pull/234) + - Fixed Eventdatabasen v2 subscription data order by using occurrences endpoint. - [#236](https://github.com/os2display/display-api-service/pull/236) - Fixed bug where no media url made Notified feed crash. - [#231](https://github.com/os2display/display-api-service/pull/231) diff --git a/src/Feed/EventDatabaseApiV2FeedType.php b/src/Feed/EventDatabaseApiV2FeedType.php index ee62cc58..49f7a977 100644 --- a/src/Feed/EventDatabaseApiV2FeedType.php +++ b/src/Feed/EventDatabaseApiV2FeedType.php @@ -14,6 +14,7 @@ use Symfony\Component\HttpClient\Exception\ClientException; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; /** * @see https://github.com/itk-dev/event-database-api @@ -23,6 +24,10 @@ class EventDatabaseApiV2FeedType implements FeedTypeInterface { final public const string SUPPORTED_FEED_TYPE = SupportedFeedOutputs::POSTER_OUTPUT; + private const string CACHE_OPTIONS_PREFIX = 'options_'; + private const string CACHE_EXPIRE_SUFFIX = '_expire'; + private const int CACHE_TTL = 60 * 60; // An hour. + public function __construct( private readonly FeedService $feedService, private readonly LoggerInterface $logger, @@ -68,37 +73,21 @@ public function getData(Feed $feed): array $locations = $configuration['subscriptionPlaceValue'] ?? null; $organizers = $configuration['subscriptionOrganizerValue'] ?? null; $tags = $configuration['subscriptionTagValue'] ?? null; - $numberOfItems = $configuration['subscriptionNumberValue'] ?? 5; + $numberOfItems = isset($configuration['subscriptionNumberValue']) ? (int) $configuration['subscriptionNumberValue'] : 5; - $queryParams = [ - 'itemsPerPage' => $numberOfItems, - ]; + $queryParams = []; if (is_array($locations) && count($locations) > 0) { - $queryParams['location.entityId'] = implode(',', array_map(static fn ($location) => (int) $location['value'], $locations)); + $queryParams['event.location.entityId'] = implode(',', array_map(static fn ($location) => (int) $location['value'], $locations)); } if (is_array($organizers) && count($organizers) > 0) { - $queryParams['organizer.entityId'] = implode(',', array_map(static fn ($organizer) => (int) $organizer['value'], $organizers)); + $queryParams['event.organizer.entityId'] = implode(',', array_map(static fn ($organizer) => (int) $organizer['value'], $organizers)); } if (is_array($tags) && count($tags) > 0) { - $queryParams['tags'] = implode(',', array_map(static fn ($tag) => (string) $tag['value'], $tags)); + $queryParams['event.tags'] = implode(',', array_map(static fn ($tag) => (string) $tag['value'], $tags)); } - $queryParams['occurrences.start'] = date('c'); - // TODO: Should be based on (end >= now) instead. But not supported by the API. - // $queryParams['occurrences.end'] = date('c'); - // @see https://github.com/itk-dev/event-database-api/blob/develop/src/Api/Dto/Event.php - - $members = $this->helper->request($feedSource, 'events', $queryParams); - - $result = []; - - foreach ($members as $member) { - $poster = $this->helper->mapFirstOccurrenceToOutput((object) $member); - if (null !== $poster) { - $result[] = $poster; - } - } + $result = $this->getSubscriptionData($feedSource, $queryParams, $numberOfItems); $posterOutput = (new PosterOutput($result))->toArray(); @@ -112,7 +101,8 @@ public function getData(Feed $feed): array if (isset($configuration['singleSelectedOccurrence'])) { $occurrenceId = $configuration['singleSelectedOccurrence']; - $members = $this->helper->request($feedSource, 'occurrences', null, $occurrenceId); + $responseData = $this->helper->request($feedSource, 'occurrences', null, $occurrenceId); + $members = $responseData->{'hydra:member'}; if (empty($members)) { return []; @@ -188,6 +178,7 @@ public function getAdminFormOptions(FeedSource $feedSource): array $searchEndpoint = $this->feedService->getFeedSourceConfigUrl($feedSource, 'search'); $entityEndpoint = $this->feedService->getFeedSourceConfigUrl($feedSource, 'entity'); $optionsEndpoint = $this->feedService->getFeedSourceConfigUrl($feedSource, 'options'); + $subscriptionEndpoint = $this->feedService->getFeedSourceConfigUrl($feedSource, 'subscription'); return [ [ @@ -196,6 +187,7 @@ public function getAdminFormOptions(FeedSource $feedSource): array 'endpointSearch' => $searchEndpoint, 'endpointEntity' => $entityEndpoint, 'endpointOption' => $optionsEndpoint, + 'endpointSubscription' => $subscriptionEndpoint, 'name' => 'resources', 'label' => 'Vælg resurser', 'helpText' => 'Her vælger du hvilke resurser der skal hentes indgange fra.', @@ -218,7 +210,8 @@ public function getConfigOptions(Request $request, FeedSource $feedSource, strin throw new \Exception('entityType and entityId must not be null'); } - $members = $this->helper->request($feedSource, $entityType, null, (int) $entityId); + $responseData = $this->helper->request($feedSource, $entityType, null, (int) $entityId); + $members = $responseData->{'hydra:member'}; $result = []; @@ -231,24 +224,90 @@ public function getConfigOptions(Request $request, FeedSource $feedSource, strin } elseif ('options' === $name) { $entityType = $request->query->get('entityType'); - $query = [ - 'itemsPerPage' => 50, - 'name' => $request->query->get('search') ?? '', - ]; - if (null === $entityType) { throw new \Exception('entityType must not be null'); } - $members = $this->helper->request($feedSource, $entityType, $query); + if (!in_array($entityType, ['tags', 'organizations', 'locations'])) { + throw new BadRequestHttpException('Unsupported entityType: '.$entityType); + } - $result = []; + $expireCacheItem = $this->feedWithoutExpireCache->getItem($this::CACHE_OPTIONS_PREFIX.$entityType.$this::CACHE_EXPIRE_SUFFIX); + $cacheItem = $this->feedWithoutExpireCache->getItem($this::CACHE_OPTIONS_PREFIX.$entityType); - foreach ($members as $member) { - $result[] = $this->helper->toPosterOption($member, $entityType); + if ($expireCacheItem->isHit()) { + $result = $expireCacheItem->get(); + + if ($result > time()) { + if ($cacheItem->isHit()) { + return $cacheItem->get(); + } + } } - return $result; + try { + $page = 1; + $results = []; + $itemsPerPage = 50; + + do { + $query = [ + 'itemsPerPage' => $itemsPerPage, + 'page' => $page, + ]; + + $responseData = $this->helper->request($feedSource, $entityType, $query); + $members = $responseData->{'hydra:member'}; + + foreach ($members as $member) { + $results[] = $this->helper->toPosterOption($member, $entityType); + } + + if ($responseData->{'hydra:totalItems'} > $page * $itemsPerPage) { + $fetchMore = true; + $page = $page + 1; + } else { + $fetchMore = false; + } + } while ($fetchMore); + + $cacheItem->set($results); + $this->feedWithoutExpireCache->save($cacheItem); + + $expireCacheItem->set(time() + $this::CACHE_TTL); + $this->feedWithoutExpireCache->save($expireCacheItem); + + return $results; + } catch (\Exception) { + if ($cacheItem->isHit()) { + return $cacheItem->get(); + } else { + return []; + } + } + } elseif ('subscription' === $name) { + $query = $request->query->all(); + + $queryParams = []; + + if (isset($query['tag'])) { + $tag = $query['tag']; + $queryParams['event.tags'] = implode(',', $tag); + } + + if (isset($query['organization'])) { + $organizer = $query['organization']; + $queryParams['event.organizer.entityId'] = implode(',', $organizer); + } + + if (isset($query['location'])) { + $location = $query['location']; + $queryParams['event.location.entityId'] = implode(',', $location); + } + + $numberOfItems = isset($query['numberOfItems']) ? (int) $query['numberOfItems'] : 10; + + return $this->getSubscriptionData($feedSource, $queryParams, $numberOfItems); } elseif ('search' === $name) { $query = $request->query->all(); $queryParams = []; @@ -267,23 +326,23 @@ public function getConfigOptions(Request $request, FeedSource $feedSource, strin if (isset($query['organization'])) { $organizer = $query['organization']; - $queryParams['organizer.entityId'] = (int) $organizer; + $queryParams['organizer.entityId'] = $organizer; } if (isset($query['location'])) { $location = $query['location']; - $queryParams['location.entityId'] = (int) $location; + $queryParams['location.entityId'] = $location; } - $queryParams['occurrences.start'] = date('c'); - // TODO: Should be based on (end >= now) instead. But not supported by the API. - // $queryParams['occurrences.end'] = date('c'); - // @see https://github.com/itk-dev/event-database-api/blob/develop/src/Api/Dto/Event.php + $queryParams['occurrences.end'] = [ + 'gt' => date('c'), + ]; } $queryParams['itemsPerPage'] = $query['itemsPerPage'] ?? 10; - $members = $this->helper->request($feedSource, $type, $queryParams); + $responseData = $this->helper->request($feedSource, $type, $queryParams); + $members = $responseData->{'hydra:member'}; $result = []; @@ -351,4 +410,57 @@ public function getSchema(): array 'required' => ['host', 'apikey'], ]; } + + private function getSubscriptionData(FeedSource $feedSource, array $queryParams = [], int $numberOfItems = 10): array + { + $itemsPerPage = 20; + $page = 1; + + $result = []; + $addedEventIds = []; + + $queryParams['itemsPerPage'] = $itemsPerPage; + + $queryParams['end'] = [ + 'gt' => date('c'), + ]; + + do { + $queryParams['page'] = $page; + + $responseData = $this->helper->request($feedSource, 'occurrences', $queryParams); + $members = $responseData->{'hydra:member'}; + + foreach ($members as $member) { + // If occurrence.event has not been added already, add it to the result array. + $occurrence = $this->helper->mapOccurrenceToOutput((object) $member); + + if (null == $occurrence) { + continue; + } + + if (!in_array($occurrence->eventId, $addedEventIds)) { + $addedEventIds[] = $occurrence->eventId; + $result[] = $occurrence; + } + + if (count($result) >= $numberOfItems) { + break; + } + } + + if (count($result) < $numberOfItems) { + if ($responseData->{'hydra:totalItems'} > $page * $itemsPerPage) { + $fetchMore = true; + $page = $page + 1; + } else { + $fetchMore = false; + } + } else { + $fetchMore = false; + } + } while ($fetchMore); + + return $result; + } } diff --git a/src/Feed/EventDatabaseApiV2Helper.php b/src/Feed/EventDatabaseApiV2Helper.php index d25f2909..db811da8 100644 --- a/src/Feed/EventDatabaseApiV2Helper.php +++ b/src/Feed/EventDatabaseApiV2Helper.php @@ -22,12 +22,12 @@ public function __construct( private readonly HttpClientInterface $client, ) {} - public function request(FeedSource $feedSource, string $entityType, ?array $queryParams = null, ?int $entityId = null): array + public function request(FeedSource $feedSource, string $entityType, ?array $queryParams = null, ?int $entityId = null): object { $secrets = $feedSource->getSecrets(); if (!isset($secrets['host']) || !isset($secrets['apikey'])) { - return []; + throw new \Exception('Missing host or apikey'); } $host = $secrets['host']; @@ -57,9 +57,8 @@ public function request(FeedSource $feedSource, string $entityType, ?array $quer ); $content = $response->getContent(); - $decoded = json_decode($content, null, 512, JSON_THROW_ON_ERROR); - return $decoded->{'hydra:member'}; + return json_decode($content, null, 512, JSON_THROW_ON_ERROR); } public function toEntityResult(string $entityType, object $entity): ?object @@ -106,6 +105,7 @@ public function createPoster(?object $event = null, ?object $occurrence = null): $event->url ?? null, $baseUrl, $imageUrls->large ?? null, + $imageUrls->small ?? null, $occurrence->start ?? null, $occurrence->end ?? null, $occurrence->ticketPriceRange ?? null, diff --git a/src/Feed/OutputModel/Poster/Poster.php b/src/Feed/OutputModel/Poster/Poster.php index 3d442305..34500689 100644 --- a/src/Feed/OutputModel/Poster/Poster.php +++ b/src/Feed/OutputModel/Poster/Poster.php @@ -16,6 +16,7 @@ public function __construct( public ?string $url, public ?string $baseUrl, public ?string $image, + public ?string $imageThumbnail, public ?string $startDate, public ?string $endDate, public ?string $ticketPriceRange,