99use App \Feed \OutputModel \Poster \PosterOutput ;
1010use App \Service \FeedService ;
1111use Doctrine \ORM \EntityManagerInterface ;
12- use Psr \Cache \CacheItemPoolInterface ;
1312use Psr \Log \LoggerInterface ;
1413use Symfony \Component \HttpClient \Exception \ClientException ;
1514use Symfony \Component \HttpFoundation \Request ;
1615use Symfony \Component \HttpFoundation \Response ;
1716use Symfony \Component \HttpKernel \Exception \BadRequestHttpException ;
17+ use Symfony \Contracts \Cache \CacheInterface ;
18+ use Symfony \Contracts \Cache \ItemInterface ;
1819
1920/**
2021 * @see https://github.com/itk-dev/event-database-api
@@ -24,16 +25,14 @@ class EventDatabaseApiV2FeedType implements FeedTypeInterface
2425{
2526 final public const string SUPPORTED_FEED_TYPE = FeedOutputModels::POSTER_OUTPUT ;
2627
27- private const string CACHE_OPTIONS_PREFIX = 'options_ ' ;
28- private const string CACHE_EXPIRE_SUFFIX = '_expire ' ;
29- private const int CACHE_TTL = 60 * 60 ; // An hour.
28+ private const int CACHE_OPTIONS_TTL = 60 * 60 ; // An hour.
3029
3130 public function __construct (
3231 private readonly FeedService $ feedService ,
3332 private readonly LoggerInterface $ logger ,
3433 private readonly EntityManagerInterface $ entityManager ,
3534 private readonly EventDatabaseApiV2Helper $ helper ,
36- private readonly CacheItemPoolInterface $ feedWithoutExpireCache ,
35+ private readonly CacheInterface $ feedWithoutExpireCache ,
3736 private readonly int $ cacheExpire ,
3837 ) {}
3938
@@ -42,104 +41,38 @@ public function __construct(
4241 */
4342 public function getData (Feed $ feed ): array
4443 {
45- $ cacheKey = 'eventdb2- ' .$ feed ->getId ();
46- $ cacheKeyLatestFetch = 'eventdb2-latest-fetch- ' .$ feed ->getId ();
47-
48- $ cacheItem = $ this ->feedWithoutExpireCache ->getItem ($ cacheKey );
49- $ latestFetchCacheItem = $ this ->feedWithoutExpireCache ->getItem ($ cacheKeyLatestFetch );
50-
51- // Serve cached item if latestFetchCacheItem has not expired and feed has not changed.
52- // The expiration is set on latestFetchCacheItem and not cacheItem, so cacheItem can be used as fallback.
53- if ($ latestFetchCacheItem ->isHit ()) {
54- // If feed has not been modified since the item was cached.
55- if ($ feed ->getModifiedAt ()?->format('c ' ) == $ latestFetchCacheItem ->get ()) {
56- if ($ cacheItem ->isHit ()) {
57- return $ cacheItem ->get ();
58- }
59- }
60- }
44+ // Include modifiedAt in cache key so the cache naturally invalidates when feed config changes.
45+ $ modifiedAt = $ feed ->getModifiedAt ()?->getTimestamp() ?? 0 ;
46+ $ cacheKey = 'eventdb2- ' .$ feed ->getId ().'- ' .$ modifiedAt ;
6147
6248 try {
63- $ feedSource = $ feed ->getFeedSource ();
64- $ configuration = $ feed ->getConfiguration ();
65-
66- if (null === $ feedSource ) {
67- throw new \Exception ('Feed source is null ' );
68- }
69-
70- if (isset ($ configuration ['posterType ' ])) {
71- switch ($ configuration ['posterType ' ]) {
72- case 'subscription ' :
73- $ locations = $ configuration ['subscriptionPlaceValue ' ] ?? null ;
74- $ organizers = $ configuration ['subscriptionOrganizerValue ' ] ?? null ;
75- $ tags = $ configuration ['subscriptionTagValue ' ] ?? null ;
76- $ numberOfItems = isset ($ configuration ['subscriptionNumberValue ' ]) ? (int ) $ configuration ['subscriptionNumberValue ' ] : 5 ;
77-
78- $ queryParams = [];
79-
80- if (is_array ($ locations ) && count ($ locations ) > 0 ) {
81- $ queryParams ['event.location.entityId ' ] = implode (', ' , array_map (static fn ($ location ) => (int ) $ location ['value ' ], $ locations ));
82- }
83- if (is_array ($ organizers ) && count ($ organizers ) > 0 ) {
84- $ queryParams ['event.organizer.entityId ' ] = implode (', ' , array_map (static fn ($ organizer ) => (int ) $ organizer ['value ' ], $ organizers ));
85- }
86- if (is_array ($ tags ) && count ($ tags ) > 0 ) {
87- $ queryParams ['event.tags ' ] = implode (', ' , array_map (static fn ($ tag ) => (string ) $ tag ['value ' ], $ tags ));
88- }
89-
90- $ result = $ this ->getSubscriptionData ($ feedSource , $ queryParams , $ numberOfItems );
91-
92- $ posterOutput = (new PosterOutput ($ result ))->toArray ();
93-
94- $ cacheItem ->set ($ posterOutput );
95- $ latestFetchCacheItem ->expiresAfter ($ this ->cacheExpire )->set ($ feed ->getModifiedAt ()?->format('c ' ) ?? '' );
96- $ this ->feedWithoutExpireCache ->save ($ cacheItem );
97- $ this ->feedWithoutExpireCache ->save ($ latestFetchCacheItem );
98-
99- return $ posterOutput ;
100- case 'single ' :
101- if (isset ($ configuration ['singleSelectedOccurrence ' ])) {
102- $ occurrenceId = $ configuration ['singleSelectedOccurrence ' ];
49+ return $ this ->feedWithoutExpireCache ->get ($ cacheKey , function (ItemInterface $ item ) use ($ feed ) {
50+ $ item ->expiresAfter ($ this ->cacheExpire );
10351
104- $ responseData = $ this -> helper -> request ( $ feedSource , ' occurrences ' , null , $ occurrenceId );
105- $ members = $ responseData ->{ ' hydra:member ' } ;
52+ $ feedSource = $ feed -> getFeedSource ( );
53+ $ configuration = $ feed -> getConfiguration () ;
10654
107- if (empty ($ members )) {
108- return [];
109- }
110-
111- $ occurrenceData = $ members [0 ];
112-
113- $ result = [];
114-
115- $ occurrence = $ this ->helper ->mapOccurrenceToOutput ($ occurrenceData );
116-
117- if (null !== $ occurrence ) {
118- $ result [] = $ occurrence ;
119- }
120-
121- $ posterOutput = (new PosterOutput ($ result ))->toArray ();
122-
123- $ cacheItem ->set ($ posterOutput );
124- $ latestFetchCacheItem ->expiresAfter ($ this ->cacheExpire )->set ($ feed ->getModifiedAt ()?->format('c ' ) ?? '' );
125- $ this ->feedWithoutExpireCache ->save ($ cacheItem );
126- $ this ->feedWithoutExpireCache ->save ($ latestFetchCacheItem );
55+ if (null === $ feedSource ) {
56+ throw new \Exception ('Feed source is null ' );
57+ }
12758
128- return $ posterOutput ;
129- }
130- // no break
131- default :
132- throw new \Exception ('Supported posterType: ' .$ configuration ['posterType ' ], 400 );
59+ if (!isset ($ configuration ['posterType ' ])) {
60+ return [];
13361 }
134- }
62+
63+ return match ($ configuration ['posterType ' ]) {
64+ 'subscription ' => $ this ->getSubscriptionPosterOutput ($ feedSource , $ configuration ),
65+ 'single ' => $ this ->getSinglePosterOutput ($ feedSource , $ configuration ),
66+ default => throw new \Exception ('Unsupported posterType: ' .$ configuration ['posterType ' ], 400 ),
67+ };
68+ });
13569 } catch (\Throwable $ throwable ) {
136- // If the content does not exist anymore, unpublished the slide.
70+ // If the content does not exist anymore, unpublish the slide.
13771 if ($ throwable instanceof ClientException && Response::HTTP_NOT_FOUND == $ throwable ->getCode ()) {
13872 try {
13973 $ slide = $ feed ->getSlide ();
14074
14175 if (null !== $ slide ) {
142- // Slide publishedTo is set to now. This will make the slide unpublished from this point on.
14376 $ slide ->setPublishedTo (new \DateTime ('now ' , new \DateTimeZone ('UTC ' )));
14477 $ this ->entityManager ->flush ();
14578
@@ -149,25 +82,70 @@ public function getData(Feed $feed): array
14982 ]);
15083 }
15184 } catch (\Exception $ exception ) {
152- $ this ->logger ->error ('{code }: {message} ' , [
153- 'code ' => $ exception -> getCode (),
85+ $ this ->logger ->error ('EventDatabaseApiV2FeedType: Failed to unpublish slide for feed {feedId }: {message} ' , [
86+ 'feedId ' => $ feed -> getId (),
15487 'message ' => $ exception ->getMessage (),
88+ 'exception ' => $ exception ,
15589 ]);
15690 }
15791 } else {
158- $ this ->logger ->error ('{code }: {message} ' , [
159- 'code ' => $ throwable -> getCode (),
92+ $ this ->logger ->error ('EventDatabaseApiV2FeedType: Failed to get data for feed {feedId }: {message} ' , [
93+ 'feedId ' => $ feed -> getId (),
16094 'message ' => $ throwable ->getMessage (),
95+ 'exception ' => $ throwable ,
16196 ]);
16297 }
16398 }
16499
165- // Fallback option is to return the cached data.
166- if ($ cacheItem ->isHit ()) {
167- return $ cacheItem ->get ();
168- } else {
100+ return [];
101+ }
102+
103+ private function getSubscriptionPosterOutput (FeedSource $ feedSource , array $ configuration ): array
104+ {
105+ $ locations = $ configuration ['subscriptionPlaceValue ' ] ?? null ;
106+ $ organizers = $ configuration ['subscriptionOrganizerValue ' ] ?? null ;
107+ $ tags = $ configuration ['subscriptionTagValue ' ] ?? null ;
108+ $ numberOfItems = isset ($ configuration ['subscriptionNumberValue ' ]) ? (int ) $ configuration ['subscriptionNumberValue ' ] : 5 ;
109+
110+ $ queryParams = [];
111+
112+ if (is_array ($ locations ) && count ($ locations ) > 0 ) {
113+ $ queryParams ['event.location.entityId ' ] = implode (', ' , array_map (static fn ($ location ) => (int ) $ location ['value ' ], $ locations ));
114+ }
115+ if (is_array ($ organizers ) && count ($ organizers ) > 0 ) {
116+ $ queryParams ['event.organizer.entityId ' ] = implode (', ' , array_map (static fn ($ organizer ) => (int ) $ organizer ['value ' ], $ organizers ));
117+ }
118+ if (is_array ($ tags ) && count ($ tags ) > 0 ) {
119+ $ queryParams ['event.tags ' ] = implode (', ' , array_map (static fn ($ tag ) => (string ) $ tag ['value ' ], $ tags ));
120+ }
121+
122+ $ result = $ this ->getSubscriptionData ($ feedSource , $ queryParams , $ numberOfItems );
123+
124+ return (new PosterOutput ($ result ))->toArray ();
125+ }
126+
127+ private function getSinglePosterOutput (FeedSource $ feedSource , array $ configuration ): array
128+ {
129+ if (!isset ($ configuration ['singleSelectedOccurrence ' ])) {
169130 return [];
170131 }
132+
133+ $ occurrenceId = $ configuration ['singleSelectedOccurrence ' ];
134+ $ responseData = $ this ->helper ->request ($ feedSource , 'occurrences ' , null , $ occurrenceId );
135+ $ members = $ responseData ->{'hydra:member ' };
136+
137+ if (empty ($ members )) {
138+ return [];
139+ }
140+
141+ $ result = [];
142+ $ occurrence = $ this ->helper ->mapOccurrenceToOutput ($ members [0 ]);
143+
144+ if (null !== $ occurrence ) {
145+ $ result [] = $ occurrence ;
146+ }
147+
148+ return (new PosterOutput ($ result ))->toArray ();
171149 }
172150
173151 /**
@@ -232,20 +210,9 @@ public function getConfigOptions(Request $request, FeedSource $feedSource, strin
232210 throw new BadRequestHttpException ('Unsupported entityType: ' .$ entityType );
233211 }
234212
235- $ expireCacheItem = $ this ->feedWithoutExpireCache ->getItem ($ this ::CACHE_OPTIONS_PREFIX .$ entityType .$ this ::CACHE_EXPIRE_SUFFIX );
236- $ cacheItem = $ this ->feedWithoutExpireCache ->getItem ($ this ::CACHE_OPTIONS_PREFIX .$ entityType );
237-
238- if ($ expireCacheItem ->isHit ()) {
239- $ result = $ expireCacheItem ->get ();
240-
241- if ($ result > time ()) {
242- if ($ cacheItem ->isHit ()) {
243- return $ cacheItem ->get ();
244- }
245- }
246- }
213+ return $ this ->feedWithoutExpireCache ->get ('options_ ' .$ entityType , function (ItemInterface $ item ) use ($ feedSource , $ entityType ) {
214+ $ item ->expiresAfter (self ::CACHE_OPTIONS_TTL );
247215
248- try {
249216 $ page = 1 ;
250217 $ results = [];
251218 $ itemsPerPage = 50 ;
@@ -271,20 +238,8 @@ public function getConfigOptions(Request $request, FeedSource $feedSource, strin
271238 }
272239 } while ($ fetchMore );
273240
274- $ cacheItem ->set ($ results );
275- $ this ->feedWithoutExpireCache ->save ($ cacheItem );
276-
277- $ expireCacheItem ->set (time () + $ this ::CACHE_TTL );
278- $ this ->feedWithoutExpireCache ->save ($ expireCacheItem );
279-
280241 return $ results ;
281- } catch (\Exception ) {
282- if ($ cacheItem ->isHit ()) {
283- return $ cacheItem ->get ();
284- } else {
285- return [];
286- }
287- }
242+ });
288243 } elseif ('subscription ' === $ name ) {
289244 $ query = $ request ->query ->all ();
290245
0 commit comments