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