ILIAS  trunk Revision v12.0_alpha-399-g579a087ced2
NewsCollection.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
21namespace ILIAS\News\Data;
22
23use ArrayIterator;
24
29class NewsCollection implements \Countable, \IteratorAggregate, \JsonSerializable
30{
32 protected array $news_items = [];
33
35 protected array $context_map = [];
36
38 protected array $type_map = [];
39
41 protected array $user_read_status = [];
43 protected array $grouped_items_map = [];
44
45 public function __construct(array $news_items = [])
46 {
47 $this->addNewsItems($news_items);
48 }
49
53 public function addNewsItems(array $news_items): static
54 {
55 foreach ($news_items as $item) {
56 $this->addNewsItem($item);
57 }
58 return $this;
59 }
60
64 public function addNewsItem(NewsItem $item): static
65 {
66 $id = $item->getId();
67 $this->news_items[$id] = $item;
68
69 // Build context index for fast context-based lookups
70 $context_key = $item->getContextObjId() . '_' . $item->getContextObjType();
71 $this->context_map[$context_key][] = $id;
72
73 // Build type index for fast type-based filtering
74 $this->type_map[$item->getContextObjType()][] = $id;
75
76 return $this;
77 }
78
79 public function getNewsItems(): array
80 {
81 return $this->news_items;
82 }
83
84 public function getNewsForContext(int $context_obj_id, string $context_obj_type): array
85 {
86 $context_key = $context_obj_id . '_' . $context_obj_type;
87
88 if (!isset($this->context_map[$context_key])) {
89 return [];
90 }
91
92 return array_map(
93 fn($id) => $this->news_items[$id],
94 $this->context_map[$context_key]
95 );
96 }
97
98 public function getNewsByType(string $obj_type): array
99 {
100 if (!isset($this->type_map[$obj_type])) {
101 return [];
102 }
103
104 return array_map(
105 fn($id) => $this->news_items[$id],
106 $this->type_map[$obj_type]
107 );
108 }
109
113 public function setUserReadStatus(int $user_id, array $read_news_ids): static
114 {
115 $this->user_read_status[$user_id] = array_filter($read_news_ids);
116 return $this;
117 }
118
119 public function isReadByUser(int $user_id, int $news_id): bool
120 {
121 return isset($this->user_read_status[$user_id][$news_id]);
122 }
123
127 public function getUserReadStatus(int $user_id): array
128 {
129 $result = [];
130 foreach (array_keys($this->news_items) as $news_id) {
131 $result[$news_id] = $this->isReadByUser($user_id, $news_id);
132 }
133 return $result;
134 }
135
136 /*
137 Grouping
138 */
139
140 public function groupFiles(): static
141 {
142 foreach (array_filter($this->news_items) as $item) {
143 if ($item->getContextObjType() === 'file') {
144 if (isset($this->grouped_items_map[$item->getContextObjId()])) {
145 $this->grouped_items_map[$item->getContextObjId()]['aggregation'][] = $item->getId();
146 } else {
147 $this->grouped_items_map[$item->getContextObjId()] = [
148 'first' => $item->getId(),
149 'aggregation' => []
150 ];
151 }
152 }
153 }
154 return $this;
155 }
156
157 public function groupForums(bool $group_posting_sequence): static
158 {
159 $last_forum = 0;
160
161 foreach (array_filter($this->news_items) as $item) {
162 // If we are grouping by sequence, we need to reset the entry in the aggregation map when switching
163 if ($group_posting_sequence && $last_forum !== $item->getContextObjType() && $last_forum !== 0) {
164 $this->grouped_items_map[$last_forum] = null;
165 }
166
167 if ($item->getContextObjType() === 'frm') {
168 if (isset($this->grouped_items_map[$item->getContextObjId()])) {
169 $this->grouped_items_map[$item->getContextObjId()]['aggregation'][] = $item->getId();
170 } else {
171 $this->grouped_items_map[$item->getContextObjId()] = [
172 'first' => $item->getId(),
173 'aggregation' => []
174 ];
175 $last_forum = $item->getContextObjId();
176 }
177 }
178 }
179
180 return $this;
181 }
182
189 public function getGroupingFor(NewsItem $item): ?array
190 {
191 if (!isset($this->grouped_items_map[$item->getContextObjId()])) {
192 return null;
193 }
194
195 $aggregation = $this->grouped_items_map[$item->getContextObjId()];
196 if ($aggregation['first'] !== $item->getId()) {
197 return null;
198 }
199
200 if ($item->getContextObjType() === 'frm') {
201 $item = $item->withContent('')->withContentLong('');
202 }
203
204 return [
205 'parent' => $item,
206 'aggregation' => [$item, ...array_map(fn($id) => $this->news_items[$id], $aggregation['aggregation'])],
207 'agg_ref_id' => $item->getContextRefId(),
208 'no_context_title' => $item->getContextObjType() === 'frm'
209 ];
210 }
211
212 /*
213 Legacy Adapter
214 */
215
224 public function getAggregatedNews(
225 bool $aggregate_files = false,
226 bool $aggregate_forums = false,
227 bool $group_posting_sequence = false
228 ): array {
229 $items = [];
230 $file_aggregation_map = [];
231 $forum_aggregation_map = [];
232 $last_forum = 0;
233
234 foreach ($this->news_items as $item) {
235 $entry = [
236 'id' => $item->getId(),
237 'priority' => $item->getPriority(),
238 'title' => $item->getTitle(),
239 'content' => $item->getContent(),
240 'context_obj_id' => $item->getContextObjId(),
241 'context_obj_type' => $item->getContextObjType(),
242 'context_sub_obj_id' => $item->getContextSubObjId(),
243 'context_sub_obj_type' => $item->getContextSubObjType(),
244 'content_type' => $item->getContentType(),
245 'creation_date' => $item->getCreationDate()->format('Y-m-d H:i:s'),
246 'user_id' => $item->getUserId(),
247 'visibility' => $item->getVisibility(),
248 'content_long' => $item->getContentLong(),
249 'content_is_lang_var' => $item->isContentIsLangVar(),
250 'mob_id' => $item->getMobId(),
251 'playtime' => $item->getPlaytime(),
252 'start_date' => null, //it seems like this is not used anymore
253 'end_date' => null, //it seems like this is not used anymore
254 'content_text_is_lang_var' => $item->isContentTextIsLangVar(),
255 'mob_cnt_download' => $item->getMobCntDownload(),
256 'mob_cnt_play' => $item->getMobCntPlay(),
257 'content_html' => $item->isContentHtml(),
258 'update_user_id' => $item->getUpdateUserId(),
259 'user_read' => (int) $this->isReadByUser($item->getUserId(), $item->getId()),
260 'ref_id' => $item->getContextRefId()
261 ];
262
263 if ($aggregate_files && $item->getContextObjType() === 'file') {
264 if (isset($file_aggregation_map[$item->getContextObjId()])) {
265 // If this file already has an aggregation entry, add it there and prevent adding it to the main list
266 $idx = $file_aggregation_map[$item->getContextObjId()];
267 $items[$idx]['aggregation'][$item->getId()] = $entry;
268 continue;
269 }
270
271 // If this is the first news for this file, set the aggregation array
272 $entry['aggregation'] = [];
273 $entry['agg_ref_id'] = $item->getContextRefId();
274 $file_aggregation_map[$item->getContextObjId()] = $item->getId();
275
276 }
277
278 if ($aggregate_forums) {
279 // If we are grouping by sequence, we need to reset the entry in the aggregation map when switching
280 if ($group_posting_sequence && $last_forum !== 0 && $last_forum !== $item->getContextObjType()) {
281 $forum_aggregation_map[$last_forum] = null;
282 }
283
284 if ($item->getContextObjType() === 'frm') {
285 $entry['no_context_title'] = true;
286
287 if (isset($forum_aggregation_map[$item->getContextObjId()])) {
288 // If this form already has an aggregation entry, add it there and prevent adding it to the main list
289 $idx = $forum_aggregation_map[$item->getContextObjId()];
290 $items[$idx]['aggregation'][$item->getId()] = $entry;
291 continue;
292 }
293
294 // If this is the first news for this forum, set the aggregation array
295 $entry['agg_ref_id'] = $item->getContextRefId();
296 $entry['content'] = '';
297 $entry['content_long'] = '';
298
299 $forum_aggregation_map[$item->getContextObjId()] = $item->getId();
300 $last_forum = $item->getContextObjType();
301
302 }
303 }
304
305 $items[$item->getId()] = $entry;
306 }
307 return $items;
308 }
309
310 /*
311 Interface Methods & Additional Accessors
312 */
313
314 public function jsonSerialize(): array
315 {
316 return array_values($this->news_items);
317 }
318
319 public function getIterator(): ArrayIterator
320 {
321 return new ArrayIterator($this->news_items);
322 }
323
324 public function count(): int
325 {
326 return count($this->news_items);
327 }
328
329 public function isEmpty(): bool
330 {
331 return empty($this->news_items);
332 }
333
334 public function first(): ?NewsItem
335 {
336 return reset($this->news_items) ?: null;
337 }
338
339 public function last(): ?NewsItem
340 {
341 return end($this->news_items) ?: null;
342 }
343
344 public function contains(int $news_id): bool
345 {
346 return isset($this->news_items[$news_id]);
347 }
348
349 public function getById(int $news_id): ?NewsItem
350 {
351 return $this->news_items[$news_id] ?? null;
352 }
353
354 public function getPageFor(int $news_id): int
355 {
356 $pages = array_keys($this->news_items);
357 return (int) array_search($news_id, $pages);
358 }
359
360 public function pick(int $offset): ?NewsItem
361 {
362 $index = max(0, $offset);
363 return array_values($this->news_items)[$index] ?? null;
364 }
365
366 public function pluck(string $key, bool $wrap = false): array
367 {
368 $arr = array_column($this->toArray(), $key);
369 return $wrap ? array_map(fn($item) => [$item], $arr) : $arr;
370 }
371
375 public function toArray(): array
376 {
377 return array_map(fn($item) => $item->toArray(), $this->news_items);
378 }
379
380
384 public function merge(NewsCollection $other): static
385 {
386 $merged = new static();
387 $merged->addNewsItems($this->news_items);
388 $merged->addNewsItems($other->getNewsItems());
389
390 // Merge user read status
391 foreach ($other->user_read_status as $user_id => $read_ids) {
392 $merged->user_read_status[$user_id] = isset($this->user_read_status[$user_id])
393 ? array_merge($this->user_read_status[$user_id], $read_ids)
394 : $read_ids;
395 }
396
397 return $merged;
398 }
399
403 public function limit(?int $limit): static
404 {
405 if ($limit === null || $limit >= count($this->news_items)) {
406 return $this;
407 }
408
409 $limited = new static();
410 $items = array_slice($this->news_items, 0, $limit, true);
411 $limited->addNewsItems($items);
412
413 return $limited;
414 }
415
421 public function exclude(array $news_ids): static
422 {
423 if (empty($news_ids)) {
424 return $this;
425 }
426
427 $filtered = new static();
428 $filtered->addNewsItems(array_filter(
429 $this->news_items,
430 fn($item) => !in_array($item->getId(), $news_ids)
431 ));
432 return $filtered;
433 }
434
435 public function load(array $news_ids = []): static
436 {
437 return $this;
438 }
439}
$id
plugin.php for ilComponentBuildPluginInfoObjectiveTest::testAddPlugins
Definition: plugin.php:23
Optimized News Collection with memory-efficient data structures to support large news feeds.
getNewsForContext(int $context_obj_id, string $context_obj_type)
limit(?int $limit)
Limit the number of news items and returns it as a new collection.
addNewsItems(array $news_items)
Add multiple news items efficiently.
pluck(string $key, bool $wrap=false)
isReadByUser(int $user_id, int $news_id)
addNewsItem(NewsItem $item)
Add a single news item with indexing.
setUserReadStatus(int $user_id, array $read_news_ids)
getGroupingFor(NewsItem $item)
Returns the grouping for a given news item.
__construct(array $news_items=[])
getAggregatedNews(bool $aggregate_files=false, bool $aggregate_forums=false, bool $group_posting_sequence=false)
Get news items in a format compatible with the legacy rendering implementation.
merge(NewsCollection $other)
Merge with another collection and returns it as a new collection.
exclude(array $news_ids)
Returns a new collection with only the news items that are not in the provided list.
groupForums(bool $group_posting_sequence)
News Item DTO for transfer of news items.
Definition: NewsItem.php:29
withContent(string $content)
Definition: NewsItem.php:180