Skip to content

Commit

Permalink
2321: Fixed how items are cached
Browse files Browse the repository at this point in the history
  • Loading branch information
tuj committed Nov 12, 2024
1 parent dab6e32 commit 76cee7f
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 82 deletions.
6 changes: 1 addition & 5 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -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 ###

3 changes: 1 addition & 2 deletions config/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
197 changes: 123 additions & 74 deletions src/Feed/CalendarApiFeedType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -37,23 +39,23 @@ 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;

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,
private readonly array $customMappings,
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);
Expand Down Expand Up @@ -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',
],
];

Expand All @@ -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,
];
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 [
Expand Down
18 changes: 17 additions & 1 deletion src/Feed/SupportedFeedOutputs.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,23 @@ class SupportedFeedOutputs
final public const string POSTER_OUTPUT = 'poster';

/**
* TODO: Describe data structure.
* Data example:
* [
* {
* "textMarkup": "<div class=\"text\">Sed nulla lorem, varius sodales justo ac, ultrices placerat nunc.</div>\n<div class=\"tags\"><span class=\"tag\">#mountains</span> <span class=\"tag\">#horizon</span> Lorem ipsum ...</div>",
* "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": "<div class=\"text\">Sed nulla lorem, varius sodales justo ac, ultrices placerat nunc.</div>\n<div class=\"tags\"><span class=\"tag\">#mountains</span> <span class=\"tag\">#video</span> Lorem ipsum ...</div>",
* "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';

Expand Down

0 comments on commit 76cee7f

Please sign in to comment.