Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
96.45% |
136 / 141 |
|
83.33% |
15 / 18 |
CRAP | |
0.00% |
0 / 1 |
NewsUtils | |
96.45% |
136 / 141 |
|
83.33% |
15 / 18 |
54 | |
0.00% |
0 / 1 |
getDefaultFilter | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
isValidFilter | |
100.00% |
23 / 23 |
|
100.00% |
1 / 1 |
6 | |||
getValidFilter | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
getAllValidFiltersForSitemap | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
3 | |||
getUiFormatFilterOptions | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
1 | |||
getUiDateRangeFilterOptions | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
1 | |||
hasArchiveAccess | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getDateRangeOptions | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
getSqlFromFilter | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
2.03 | |||
getSqlDateRangeFilter | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
getSqlFormatFilter | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
7 | |||
getTitleFromFilter | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
4 | |||
getPresentFormatFilterTitle | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
6 | |||
getPastFormatFilterTitle | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
6 | |||
getIsNotArchivedCriteria | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getUrl | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getNewsFormatIcon | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
4 | |||
fromEnv | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace Olz\News\Utils; |
4 | |
5 | use Doctrine\Common\Collections\Criteria; |
6 | use Doctrine\Common\Collections\Expr\Comparison; |
7 | use Olz\Entity\News\NewsEntry; |
8 | use Olz\Utils\DateUtils; |
9 | use Olz\Utils\WithUtilsTrait; |
10 | |
11 | /** |
12 | * @phpstan-type Option array{ident: string, name: string, icon?: string} |
13 | * @phpstan-type UiOption array{selected: bool, new_filter: FullFilter, name: string, icon?: ?string, ident: string} |
14 | * @phpstan-type FullFilter array{format: string, datum: string} |
15 | * @phpstan-type PartialFilter array{format?: string, datum?: string} |
16 | */ |
17 | class NewsUtils { |
18 | use WithUtilsTrait; |
19 | |
20 | public const ALL_FORMAT_OPTIONS = [ |
21 | ['ident' => 'alle', 'name' => "Alle"], |
22 | ['ident' => 'aktuell', 'name' => "Aktuell", 'icon' => 'entry_type_aktuell_20.svg'], |
23 | ['ident' => 'kaderblog', 'name' => "Kaderblog", 'icon' => 'entry_type_kaderblog_20.svg'], |
24 | ['ident' => 'forum', 'name' => "Forum", 'icon' => 'entry_type_forum_20.svg'], |
25 | ['ident' => 'galerie', 'name' => "Galerien", 'icon' => 'entry_type_gallery_20.svg'], |
26 | ['ident' => 'video', 'name' => "Videos", 'icon' => 'entry_type_movie_20.svg'], |
27 | ]; |
28 | |
29 | /** @return FullFilter */ |
30 | public function getDefaultFilter(): array { |
31 | $current_year = intval($this->dateUtils()->getCurrentDateInFormat('Y')); |
32 | return [ |
33 | 'format' => 'alle', |
34 | 'datum' => strval($current_year), |
35 | ]; |
36 | } |
37 | |
38 | /** @param ?array<string, string> $filter */ |
39 | public function isValidFilter(?array $filter): bool { |
40 | $has_correct_format = ( |
41 | isset($filter['format']) |
42 | && array_filter( |
43 | NewsUtils::ALL_FORMAT_OPTIONS, |
44 | function ($format_option) use ($filter) { |
45 | return $format_option['ident'] === $filter['format']; |
46 | } |
47 | ) |
48 | ); |
49 | $has_correct_date_range = ( |
50 | isset($filter['datum']) |
51 | && array_filter( |
52 | $this->getDateRangeOptions(), |
53 | function ($date_option) use ($filter) { |
54 | return $date_option['ident'] === $filter['datum']; |
55 | } |
56 | ) |
57 | ); |
58 | $has_no_other_keys = !array_filter( |
59 | array_keys($filter ?? []), |
60 | fn ($key) => $key !== 'format' && $key !== 'datum', |
61 | ); |
62 | return $has_correct_format && $has_correct_date_range && $has_no_other_keys; |
63 | } |
64 | |
65 | /** |
66 | * @param ?PartialFilter $filter |
67 | * |
68 | * @return FullFilter |
69 | */ |
70 | public function getValidFilter(?array $filter): array { |
71 | $default_filter = $this->getDefaultFilter(); |
72 | if (!$filter) { |
73 | return $default_filter; |
74 | } |
75 | $merged_filter = [ |
76 | 'format' => $filter['format'] ?? $default_filter['format'], |
77 | 'datum' => $filter['datum'] ?? $default_filter['datum'], |
78 | ]; |
79 | return $this->isValidFilter($merged_filter) ? $merged_filter : $default_filter; |
80 | } |
81 | |
82 | /** @return array<FullFilter> */ |
83 | public function getAllValidFiltersForSitemap(): array { |
84 | $all_valid_filters = []; |
85 | foreach (NewsUtils::ALL_FORMAT_OPTIONS as $format_option) { |
86 | $date_range_options = $this->getDateRangeOptions(); |
87 | foreach ($date_range_options as $date_range_option) { |
88 | $all_valid_filters[] = [ |
89 | 'format' => $format_option['ident'], |
90 | 'datum' => $date_range_option['ident'], |
91 | ]; |
92 | } |
93 | } |
94 | return $all_valid_filters; |
95 | } |
96 | |
97 | /** |
98 | * @param FullFilter $filter |
99 | * |
100 | * @return array<UiOption> |
101 | */ |
102 | public function getUiFormatFilterOptions(array $filter): array { |
103 | return array_map(function ($format_option) use ($filter) { |
104 | $new_filter = $filter; |
105 | $new_filter['format'] = $format_option['ident']; |
106 | return [ |
107 | 'selected' => $format_option['ident'] === $filter['format'], |
108 | 'new_filter' => $new_filter, |
109 | 'name' => $format_option['name'], |
110 | 'icon' => $format_option['icon'] ?? null, |
111 | 'ident' => $format_option['ident'], |
112 | ]; |
113 | }, NewsUtils::ALL_FORMAT_OPTIONS); |
114 | } |
115 | |
116 | /** |
117 | * @param FullFilter $filter |
118 | * |
119 | * @return array<UiOption> |
120 | */ |
121 | public function getUiDateRangeFilterOptions(array $filter): array { |
122 | return array_map(function ($date_range_option) use ($filter) { |
123 | $new_filter = $filter; |
124 | $new_filter['datum'] = $date_range_option['ident']; |
125 | return [ |
126 | 'selected' => $date_range_option['ident'] === $filter['datum'], |
127 | 'new_filter' => $new_filter, |
128 | 'name' => $date_range_option['name'], |
129 | 'ident' => $date_range_option['ident'], |
130 | ]; |
131 | }, $this->getDateRangeOptions()); |
132 | } |
133 | |
134 | public function hasArchiveAccess(): bool { |
135 | return $this->authUtils()->hasPermission('verified_email'); |
136 | } |
137 | |
138 | /** |
139 | * @return array<Option> |
140 | */ |
141 | public function getDateRangeOptions(): array { |
142 | $include_archive = $this->hasArchiveAccess(); |
143 | $current_year = intval($this->dateUtils()->getCurrentDateInFormat('Y')); |
144 | $first_year = $include_archive ? 2006 : $current_year - DateUtils::ARCHIVE_YEARS_THRESHOLD; |
145 | $options = []; |
146 | for ($year = $current_year; $year >= $first_year; $year--) { |
147 | $year_ident = strval($year); |
148 | $options[] = ['ident' => $year_ident, 'name' => $year_ident]; |
149 | } |
150 | return $options; |
151 | } |
152 | |
153 | /** @param FullFilter $filter */ |
154 | public function getSqlFromFilter(array $filter): string { |
155 | if (!$this->isValidFilter($filter)) { |
156 | return "'1'='0'"; |
157 | } |
158 | $date_range_filter = $this->getSqlDateRangeFilter($filter); |
159 | $format_filter = $this->getSqlFormatFilter($filter); |
160 | return "({$date_range_filter}) AND ({$format_filter})"; |
161 | } |
162 | |
163 | /** @param FullFilter $filter */ |
164 | private function getSqlDateRangeFilter(array $filter): string { |
165 | $today = $this->dateUtils()->getIsoToday(); |
166 | if (intval($filter['datum']) > 2000) { |
167 | $sane_year = strval(intval($filter['datum'])); |
168 | return "YEAR(n.published_date) = '{$sane_year}'"; |
169 | } |
170 | // @codeCoverageIgnoreStart |
171 | // Reason: Should not be reached. |
172 | return "'1' = '0'"; // invalid => show nothing |
173 | // @codeCoverageIgnoreEnd |
174 | } |
175 | |
176 | /** @param FullFilter $filter */ |
177 | private function getSqlFormatFilter(array $filter): string { |
178 | if ($filter['format'] === 'alle') { |
179 | return "'1' = '1'"; |
180 | } |
181 | if ($filter['format'] === 'aktuell') { |
182 | return "n.format LIKE '%aktuell%'"; |
183 | } |
184 | if ($filter['format'] === 'kaderblog') { |
185 | return "n.format LIKE '%kaderblog%'"; |
186 | } |
187 | if ($filter['format'] === 'forum') { |
188 | return "n.format LIKE '%forum%'"; |
189 | } |
190 | if ($filter['format'] === 'galerie') { |
191 | return "n.format LIKE '%galerie%'"; |
192 | } |
193 | if ($filter['format'] === 'video') { |
194 | return "n.format LIKE '%video%'"; |
195 | } |
196 | // @codeCoverageIgnoreStart |
197 | // Reason: Should not be reached. |
198 | return "'1' = '0'"; // invalid => show nothing |
199 | // @codeCoverageIgnoreEnd |
200 | } |
201 | |
202 | /** @param FullFilter $filter */ |
203 | public function getTitleFromFilter(array $filter): string { |
204 | if (!$this->isValidFilter($filter)) { |
205 | return "News"; |
206 | } |
207 | $this_year = $this->dateUtils()->getCurrentDateInFormat('Y'); |
208 | if ($filter['datum'] === $this_year) { |
209 | $format_title = $this->getPresentFormatFilterTitle($filter); |
210 | return "{$format_title}"; |
211 | } |
212 | $format_title = $this->getPastFormatFilterTitle($filter); |
213 | if (intval($filter['datum']) > 2000) { |
214 | $year = $filter['datum']; |
215 | return "{$format_title} {$year}"; |
216 | } |
217 | // @codeCoverageIgnoreStart |
218 | // Reason: Should not be reached. |
219 | return "Aktuell"; |
220 | // @codeCoverageIgnoreEnd |
221 | } |
222 | |
223 | /** @param FullFilter $filter */ |
224 | private function getPresentFormatFilterTitle(array $filter): string { |
225 | if ($filter['format'] === 'aktuell') { |
226 | return "Aktuell"; |
227 | } |
228 | if ($filter['format'] === 'kaderblog') { |
229 | return "Kaderblog"; |
230 | } |
231 | if ($filter['format'] === 'forum') { |
232 | return "Forum"; |
233 | } |
234 | if ($filter['format'] === 'galerie') { |
235 | return "Galerien"; |
236 | } |
237 | if ($filter['format'] === 'video') { |
238 | return "Videos"; |
239 | } |
240 | return "News"; |
241 | } |
242 | |
243 | /** @param FullFilter $filter */ |
244 | private function getPastFormatFilterTitle(array $filter): string { |
245 | if ($filter['format'] === 'aktuell') { |
246 | return "Aktuelles von"; |
247 | } |
248 | if ($filter['format'] === 'kaderblog') { |
249 | return "Kaderblog von"; |
250 | } |
251 | if ($filter['format'] === 'forum') { |
252 | return "Forumseinträge von"; |
253 | } |
254 | if ($filter['format'] === 'galerie') { |
255 | return "Galerien von"; |
256 | } |
257 | if ($filter['format'] === 'video') { |
258 | return "Videos von"; |
259 | } |
260 | return "News von"; |
261 | } |
262 | |
263 | public function getIsNotArchivedCriteria(): Comparison { |
264 | $archive_threshold = $this->dateUtils()->getIsoArchiveThreshold(); |
265 | return Criteria::expr()->gte('published_date', new \DateTime($archive_threshold)); |
266 | } |
267 | |
268 | /** @param PartialFilter $filter */ |
269 | public function getUrl(array $filter = []): string { |
270 | $code_href = $this->envUtils()->getCodeHref(); |
271 | $enc_json_filter = urlencode(json_encode($filter) ?: '{}'); |
272 | return "{$code_href}news?filter={$enc_json_filter}"; |
273 | } |
274 | |
275 | /** @var array<string, string> */ |
276 | protected static $iconBasenameByFormat = [ |
277 | 'aktuell' => 'entry_type_aktuell_20.svg', |
278 | 'forum' => 'entry_type_forum_20.svg', |
279 | 'galerie' => 'entry_type_gallery_20.svg', |
280 | 'kaderblog' => 'entry_type_kaderblog_20.svg', |
281 | 'video' => 'entry_type_movie_20.svg', |
282 | |
283 | 'galerie_white' => 'entry_type_gallery_white_20.svg', |
284 | 'video_white' => 'entry_type_movie_white_20.svg', |
285 | ]; |
286 | |
287 | public function getNewsFormatIcon( |
288 | NewsEntry|string $input, |
289 | ?string $modifier = null, |
290 | ): ?string { |
291 | $format = $input instanceof NewsEntry ? $input->getFormat() : $input; |
292 | $key = $modifier === null ? $format : "{$format}_{$modifier}"; |
293 | $icon = self::$iconBasenameByFormat[$key] ?? null; |
294 | if ($icon === null) { |
295 | return null; |
296 | } |
297 | $code_href = $this->envUtils()->getCodeHref(); |
298 | return "{$code_href}assets/icns/{$icon}"; |
299 | } |
300 | |
301 | public static function fromEnv(): self { |
302 | return new self(); |
303 | } |
304 | } |