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