diff --git a/.env b/.env index 300177db..114aa336 100644 --- a/.env +++ b/.env @@ -101,9 +101,5 @@ CALENDAR_API_FEED_SOURCE_CUSTOM_MAPPINGS='{}' CALENDAR_API_FEED_SOURCE_EVENT_MODIFIERS='{}' CALENDAR_API_FEED_SOURCE_DATE_FORMAT= CALENDAR_API_FEED_SOURCE_DATE_TIMEZONE= -# Default: 1 hour -CALENDAR_API_FEED_SOURCE_CACHE_EXPIRE_RESOURCES=3600 -# Defaul: 5 minutes -CALENDAR_API_FEED_SOURCE_CACHE_EXPIRE_EVENTS=300 +CALENDAR_API_FEED_SOURCE_CACHE_EXPIRE_SECONDS=300 ###< Calendar Api Feed Source ### - diff --git a/config/services.yaml b/config/services.yaml index 5dd4abea..b71c181d 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -62,8 +62,7 @@ services: $eventModifiers: '%env(json:CALENDAR_API_FEED_SOURCE_EVENT_MODIFIERS)%' $dateFormat: '%env(string:CALENDAR_API_FEED_SOURCE_DATE_FORMAT)%' $timezone: '%env(string:CALENDAR_API_FEED_SOURCE_DATE_TIMEZONE)%' - $cacheExpireResourcesSeconds: '%env(int:CALENDAR_API_FEED_SOURCE_CACHE_EXPIRE_RESOURCES)%' - $cacheExpireEventsSeconds: '%env(int:CALENDAR_API_FEED_SOURCE_CACHE_EXPIRE_EVENTS)%' + $cacheExpireSeconds: '%env(int:CALENDAR_API_FEED_SOURCE_CACHE_EXPIRE_SECONDS)%' App\Service\KeyVaultService: arguments: diff --git a/src/Feed/CalendarApiFeedType.php b/src/Feed/CalendarApiFeedType.php index 1e085737..5bce5959 100644 --- a/src/Feed/CalendarApiFeedType.php +++ b/src/Feed/CalendarApiFeedType.php @@ -10,6 +10,8 @@ use App\Model\CalendarLocation; use App\Model\CalendarResource; use App\Service\FeedService; +use Faker\Core\DateTime; +use Psr\Cache\CacheItemPoolInterface; use Psr\Cache\InvalidArgumentException; use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\Request; @@ -37,6 +39,7 @@ class CalendarApiFeedType implements FeedTypeInterface private const string CACHE_KEY_LOCATIONS = 'locations'; private const string CACHE_KEY_RESOURCES = 'resources'; private const string CACHE_KEY_EVENTS = 'events'; + private const string CACHE_LATEST_REQUEST_SUFFIX = '-latest-request'; private array $mappings; @@ -44,7 +47,7 @@ public function __construct( private readonly FeedService $feedService, private readonly HttpClientInterface $client, private readonly LoggerInterface $logger, - private readonly CacheInterface $calendarApiCache, + private readonly CacheItemPoolInterface $calendarApiCache, private readonly string $locationEndpoint, private readonly string $resourceEndpoint, private readonly string $eventEndpoint, @@ -52,8 +55,7 @@ public function __construct( private readonly array $eventModifiers, private readonly string $dateFormat, private readonly string $timezone, - private readonly int $cacheExpireResourcesSeconds, - private readonly int $cacheExpireEventsSeconds + private readonly int $cacheExpireSeconds, ) { $this->mappings = $this->createMappings($this->customMappings); @@ -158,7 +160,7 @@ public function getAdminFormOptions(FeedSource $feedSource): array 'name' => 'resources', 'label' => 'Vælg resurser', 'helpText' => 'Her vælger du hvilke resurser, der skal hentes indgange fra.', - 'formGroupClasses' => 'col-md-6 mb-3', + 'formGroupClasses' => 'mb-3', ], ]; @@ -180,7 +182,7 @@ public function getAdminFormOptions(FeedSource $feedSource): array 'name' => 'enabledModifiers', 'label' => 'Vælg justeringer af begivenheder', 'helpText' => 'Her kan du aktivere forskellige justeringer af begivenhederne.', - 'formGroupClasses' => 'col-md-6 mb-3', + 'formGroupClasses' => 'mb-3', 'options' => $enableModifierOptions, ]; } @@ -261,106 +263,146 @@ private function getLocationOptions(): array private function getResourceEvents(string $resourceId): array { - return $this->calendarApiCache->get(self::CACHE_KEY_EVENTS.'-'.$resourceId, function (ItemInterface $item) use ($resourceId): array { - $item->expiresAfter($this->cacheExpireEventsSeconds); + $cacheItem = $this->calendarApiCache->getItem(self::CACHE_KEY_EVENTS.'-'.$resourceId); + + if (!$cacheItem->isHit()) { $allEvents = $this->loadEvents(); - return array_filter($allEvents, fn(CalendarEvent $item) => $item->resourceId === $resourceId); - }); + $items = array_filter($allEvents, fn(CalendarEvent $item) => $item->resourceId === $resourceId); + + $cacheItem->set($items); + $cacheItem->expiresAfter($this->cacheExpireSeconds); + $this->calendarApiCache->save($cacheItem); + } + + return $cacheItem->get() ?? []; } private function getLocationResources(string $locationId): array { - return $this->calendarApiCache->get(self::CACHE_KEY_RESOURCES.'-'.$locationId, function (ItemInterface $item) use ($locationId): array { - $item->expiresAfter($this->cacheExpireResourcesSeconds); + $cacheItem = $this->calendarApiCache->getItem(self::CACHE_KEY_RESOURCES.'-'.$locationId); + + if (!$cacheItem->isHit()) { $allResources = $this->loadResources(); - return array_filter($allResources, fn(CalendarResource $item) => $item->locationId === $locationId); - }); + $items = array_filter($allResources, fn(CalendarResource $item) => $item->locationId === $locationId); + + $cacheItem->set($items); + $cacheItem->expiresAfter($this->cacheExpireSeconds); + $this->calendarApiCache->save($cacheItem); + } + + return $cacheItem->get() ?? []; } private function loadLocations(): array { - return $this->calendarApiCache->get(self::CACHE_KEY_LOCATIONS, function (ItemInterface $item): array { - $item->expiresAfter($this->cacheExpireResourcesSeconds); + $cacheItem = $this->calendarApiCache->getItem(self::CACHE_KEY_LOCATIONS); - $response = $this->client->request('GET', $this->locationEndpoint); + if (!$cacheItem->isHit() || $this->shouldFetchNewData(self::CACHE_KEY_LOCATIONS)) { + try { + $response = $this->client->request('GET', $this->locationEndpoint); - $LocationEntries = $response->toArray(); + $LocationEntries = $response->toArray(); - return array_map(function (array $entry) { - return new CalendarLocation( - $entry[$this->getMapping('locationId')], - $entry[$this->getMapping('locationDisplayName')], - ); - }, $LocationEntries); - }); + $locations = array_map(function (array $entry) { + return new CalendarLocation( + $entry[$this->getMapping('locationId')], + $entry[$this->getMapping('locationDisplayName')], + ); + }, $LocationEntries); + + $cacheItem->set($locations); + $this->calendarApiCache->save($cacheItem); + } catch (\Throwable $throwable) { + $this->logger->error('Error fetching locations data. {code}: {message}', ['code' => $throwable->getCode(), 'message' => $throwable->getMessage()]); + } + } + + return $cacheItem->get() ?? []; } private function loadResources(): array { - return $this->calendarApiCache->get(self::CACHE_KEY_RESOURCES, function (ItemInterface $item): array { - $item->expiresAfter($this->cacheExpireResourcesSeconds); - - $response = $this->client->request('GET', $this->resourceEndpoint); + $cacheItem = $this->calendarApiCache->getItem(self::CACHE_KEY_RESOURCES); - $resourceEntries = $response->toArray(); + if (!$cacheItem->isHit() || $this->shouldFetchNewData(self::CACHE_KEY_RESOURCES)) { + try { + $response = $this->client->request('GET', $this->resourceEndpoint); - $resources = []; + $resourceEntries = $response->toArray(); - foreach ($resourceEntries as $resourceEntry) { - // Only include resources that are marked as included in events. Defaults to true, if the resourceEntry - // does not have the property defined by the mapping resourceIncludedInEvents. - $resourceIncludedInEvents = $resourceEntry[$this->getMapping('resourceIncludedInEvents')] ?? true; - $includeValue = $this->parseBool($resourceIncludedInEvents); - - // Only include resources that are included in events endpoint. - if ($includeValue) { - $resource = new CalendarResource( - $resourceEntry[$this->getMapping('resourceId')], - $resourceEntry[$this->getMapping('resourceLocationId')], - $resourceEntry[$this->getMapping('resourceDisplayName')], - ); + $resources = []; - $resources[] = $resource; + foreach ($resourceEntries as $resourceEntry) { + // Only include resources that are marked as included in events. Defaults to true, if the resourceEntry + // does not have the property defined by the mapping resourceIncludedInEvents. + $resourceIncludedInEvents = $resourceEntry[$this->getMapping('resourceIncludedInEvents')] ?? true; + $includeValue = $this->parseBool($resourceIncludedInEvents); + + // Only include resources that are included in events endpoint. + if ($includeValue) { + $resource = new CalendarResource( + $resourceEntry[$this->getMapping('resourceId')], + $resourceEntry[$this->getMapping('resourceLocationId')], + $resourceEntry[$this->getMapping('resourceDisplayName')], + ); + + $resources[] = $resource; + } } + + $cacheItem->set($resources); + $this->calendarApiCache->save($cacheItem); + } catch (\Throwable $throwable) { + $this->logger->error('Error fetching resources data. {code}: {message}', ['code' => $throwable->getCode(), 'message' => $throwable->getMessage()]); } + } - return $resources; - }); + return $cacheItem->get() ?? []; } private function loadEvents(): array { - return $this->calendarApiCache->get(self::CACHE_KEY_EVENTS, function (ItemInterface $item): array { - $item->expiresAfter($this->cacheExpireEventsSeconds); - $response = $this->client->request('GET', $this->eventEndpoint); - - $eventEntries = $response->toArray(); - - return array_reduce($eventEntries, function (array $carry, array $entry) { - $newEntry = new CalendarEvent( - Ulid::generate(), - $entry[$this->getMapping('eventTitle')], - $this->stringToUnixTimestamp($entry[$this->getMapping('eventStartTime')]), - $this->stringToUnixTimestamp($entry[$this->getMapping('eventEndTime')]), - $entry[$this->getMapping('eventResourceId')], - $entry[$this->getMapping('eventResourceDisplayName')], - ); - - // Filter out entries if they do not supply required data. - if ( - !empty($newEntry->startTimeTimestamp) && - !empty($newEntry->endTimeTimestamp) && - !empty($newEntry->resourceId) && - !empty($newEntry->resourceDisplayName) - ) { - $carry[] = $newEntry; - } + $cacheItem = $this->calendarApiCache->getItem(self::CACHE_KEY_EVENTS); + + if (!$cacheItem->isHit() || $this->shouldFetchNewData(self::CACHE_KEY_EVENTS)) { + try { + $response = $this->client->request('GET', $this->eventEndpoint); + + $eventEntries = $response->toArray(); + + $events = array_reduce($eventEntries, function (array $carry, array $entry) { + $newEntry = new CalendarEvent( + Ulid::generate(), + $entry[$this->getMapping('eventTitle')], + $this->stringToUnixTimestamp($entry[$this->getMapping('eventStartTime')]), + $this->stringToUnixTimestamp($entry[$this->getMapping('eventEndTime')]), + $entry[$this->getMapping('eventResourceId')], + $entry[$this->getMapping('eventResourceDisplayName')], + ); + + // Filter out entries if they do not supply required data. + if ( + !empty($newEntry->startTimeTimestamp) && + !empty($newEntry->endTimeTimestamp) && + !empty($newEntry->resourceId) && + !empty($newEntry->resourceDisplayName) + ) { + $carry[] = $newEntry; + } - return $carry; - }, []); - }); + return $carry; + }, []); + + $cacheItem->set($events); + $this->calendarApiCache->save($cacheItem); + } catch (\Throwable $throwable) { + $this->logger->error('Error fetching events data. {code}: {message}', ['code' => $throwable->getCode(), 'message' => $throwable->getMessage()]); + } + } + + return $cacheItem->get() ?? []; } private function stringToUnixTimestamp(string $dateTimeString): int @@ -399,6 +441,13 @@ private function getMapping(string $key): string return $this->mappings[$key]; } + private function shouldFetchNewData(string $cacheKey): bool + { + $latestRequestCacheItem = $this->calendarApiCache->getItem($cacheKey.self::CACHE_LATEST_REQUEST_SUFFIX); + $latestRequest = $latestRequestCacheItem->get(); + return $latestRequest === null || $latestRequest <= time() - $this->cacheExpireSeconds; + } + private function createMappings(array $customMappings): array { return [ diff --git a/src/Feed/SupportedFeedOutputs.php b/src/Feed/SupportedFeedOutputs.php index 40511439..284325c0 100644 --- a/src/Feed/SupportedFeedOutputs.php +++ b/src/Feed/SupportedFeedOutputs.php @@ -27,7 +27,23 @@ class SupportedFeedOutputs final public const string POSTER_OUTPUT = 'poster'; /** - * TODO: Describe data structure. + * Data example: + * [ + * { + * "textMarkup": "
Sed nulla lorem, varius sodales justo ac, ultrices placerat nunc.
\n
#mountains #horizon Lorem ipsum ...
", + * "mediaUrl": "https://raw.githubusercontent.com/os2display/display-templates/refs/heads/develop/src/fixtures/images/mountain1.jpeg", + * "videoUrl": null, + * "username": "username", + * "createdTime": "2022-02-03T08:50:07", + * }, + * { + * "textMarkup": "
Sed nulla lorem, varius sodales justo ac, ultrices placerat nunc.
\n
#mountains #video Lorem ipsum ...
", + * "mediaUrl"": null, + * "videoUrl": "https://github.com/os2display/display-templates/raw/refs/heads/develop/src/fixtures/videos/test.mp4", + * "username": "username2", + * "createdTime": "2022-01-03T08:50:07", + * } + * ] */ final public const string INSTAGRAM_OUTPUT = 'instagram';