Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.45% covered (success)
96.45%
136 / 141
83.33% covered (warning)
83.33%
15 / 18
CRAP
0.00% covered (danger)
0.00%
0 / 1
NewsUtils
96.45% covered (success)
96.45%
136 / 141
83.33% covered (warning)
83.33%
15 / 18
54
0.00% covered (danger)
0.00%
0 / 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
6
 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    /** @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}