Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.18% covered (success)
93.18%
205 / 220
80.77% covered (success)
80.77%
42 / 52
CRAP
0.00% covered (danger)
0.00%
0 / 1
Category
93.18% covered (success)
93.18%
205 / 220
80.77% covered (success)
80.77%
42 / 52
95.74
0.00% covered (danger)
0.00%
0 / 1
 __construct
92.86% covered (success)
92.86%
13 / 14
0.00% covered (danger)
0.00%
0 / 1
5.01
 getCategoryRepository
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getTreeBuilder
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getBreadcrumbsBuilder
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getBreadcrumbsHtmlRenderer
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getAdminCategoryTreePresenter
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getCategoryService
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getTreeFacade
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 setGroups
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 setLanguage
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getUser
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getGroups
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getOrderedCategories
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
3
 getLevelOf
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setUser
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getAllCategories
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 getAllCategoryIds
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCategoriesPaginated
94.74% covered (success)
94.74%
18 / 19
0.00% covered (danger)
0.00%
0 / 1
3.00
 countCategories
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 buildCategoryTree
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 buildAdminCategoryTree
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 transform
88.89% covered (success)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
2.01
 getNodes
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getChildren
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getChildNodes
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 expandTo
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
5
 collapseAll
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
5
 getLineCategory
83.33% covered (success)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
5.12
 expand
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getCategoryData
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getPath
87.50% covered (success)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
3.02
 getPathWithStartPage
92.31% covered (success)
92.31%
12 / 13
0.00% covered (danger)
0.00%
0 / 1
3.00
 getCategoryIdFromFaq
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getCategoryIdsFromFaq
85.71% covered (success)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
3.03
 getCategoryIdFromName
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getCategoriesFromFaq
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 create
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 checkIfCategoryExists
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 update
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 moveOwnership
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasLanguage
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 updateParentCategory
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 delete
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCategoryLanguagesTranslated
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getMissingCategories
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 getOwner
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getModeratorGroupId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCategoryTree
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCategoryName
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getCategoryDescription
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 categoryHasLinkToFaq
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCategoryNames
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3/**
4 * The main category class. Yes, it's huge.
5 *
6 * This Source Code Form is subject to the terms of the Mozilla Public License,
7 * v. 2.0. If a copy of the MPL was not distributed with this file, You can
8 * obtain one at https://mozilla.org/MPL/2.0/.
9 *
10 * @package   phpMyFAQ
11 * @author    Thorsten Rinne <thorsten@phpmyfaq.de>
12 * @author    Lars Tiedemann <larstiedemann@yahoo.de>
13 * @author    Matteo Scaramuccia <matteo@scaramuccia.com>
14 * @author    Rudi Ferrari <bookcrossers@gmx.de>
15 * @copyright 2004-2026 phpMyFAQ Team
16 * @license   https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
17 * @link      https://www.phpmyfaq.de
18 * @since     2004-02-16
19 */
20
21declare(strict_types=1);
22
23namespace phpMyFAQ;
24
25use phpMyFAQ\Category\CategoryCache;
26use phpMyFAQ\Category\CategoryPermissionContext;
27use phpMyFAQ\Category\CategoryRepository;
28use phpMyFAQ\Category\CategoryRepositoryInterface;
29use phpMyFAQ\Category\CategoryService;
30use phpMyFAQ\Category\CategoryTreeFacade;
31use phpMyFAQ\Category\Navigation\BreadcrumbsBuilder;
32use phpMyFAQ\Category\Navigation\BreadcrumbsHtmlRenderer;
33use phpMyFAQ\Category\Presentation\AdminCategoryTreePresenter;
34use phpMyFAQ\Category\Tree\TreeBuilder;
35use phpMyFAQ\Entity\CategoryEntity;
36
37/* @mago-expect lint:too-many-methods - legacy category facade; split into repository/services is in progress */
38class Category
39{
40    /**
41     * The current language.
42     */
43    private ?string $language = null;
44
45    /**
46     * Internal cache for category data.
47     */
48    private CategoryCache $categoryCache;
49
50    /**
51     * Permission context for category access control.
52     */
53    private CategoryPermissionContext $categoryPermissionContext;
54
55    /**
56     * Internal repository for persistence access
57     */
58    private ?CategoryRepositoryInterface $categoryRepository = null;
59
60    /**
61     * Internal service for CRUD operations
62     */
63    private ?CategoryService $categoryService = null;
64
65    /**
66     * Internal tree facade for tree operations
67     */
68    private ?CategoryTreeFacade $categoryTreeFacade = null;
69
70    /**
71     * Internal tree builder
72     */
73    private ?TreeBuilder $treeBuilder = null;
74
75    /**
76     * Internal breadcrumbs builder
77     */
78    private ?BreadcrumbsBuilder $breadcrumbsBuilder = null;
79
80    /**
81     * Internal breadcrumbs HTML renderer
82     */
83    private ?BreadcrumbsHtmlRenderer $breadcrumbsHtmlRenderer = null;
84
85    /**
86     * Internal admin category tree presenter
87     */
88    private ?AdminCategoryTreePresenter $adminCategoryTreePresenter = null;
89
90    public function __construct(
91        private readonly Configuration $configuration,
92        array $groups = [],
93        bool $withPermission = true,
94        ?string $faqLanguage = null,
95    ) {
96        $this->categoryCache = new CategoryCache();
97        $this->categoryPermissionContext = new CategoryPermissionContext($groups);
98        $languageToUse =
99            $faqLanguage !== null && $faqLanguage !== ''
100                ? $faqLanguage
101                : $this->configuration->getLanguage()->getLanguage();
102        $this->setLanguage($languageToUse);
103        $this->getOrderedCategories($withPermission);
104
105        foreach ($this->categoryCache->getCategoryNames() as $categoryName) {
106            if (!array_key_exists('id', $categoryName)) {
107                continue;
108            }
109
110            $id = (int) $categoryName['id'];
111            $level = $this->getLevelOf($id);
112            $this->categoryCache->addCategoryName($id, array_merge($categoryName, ['level' => $level]));
113        }
114    }
115
116    /**
117     * Lazy repository factory.
118     */
119    private function getCategoryRepository(): CategoryRepositoryInterface
120    {
121        if (!$this->categoryRepository instanceof CategoryRepositoryInterface) {
122            $this->categoryRepository = new CategoryRepository($this->configuration);
123        }
124
125        return $this->categoryRepository;
126    }
127
128    /**
129     * Lazy tree builder factory.
130     */
131    private function getTreeBuilder(): TreeBuilder
132    {
133        if (!$this->treeBuilder instanceof TreeBuilder) {
134            $this->treeBuilder = new TreeBuilder();
135        }
136
137        return $this->treeBuilder;
138    }
139
140    /**
141     * Lazy breadcrumbs builder factory.
142     */
143    private function getBreadcrumbsBuilder(): BreadcrumbsBuilder
144    {
145        if (!$this->breadcrumbsBuilder instanceof BreadcrumbsBuilder) {
146            $this->breadcrumbsBuilder = new BreadcrumbsBuilder();
147        }
148
149        return $this->breadcrumbsBuilder;
150    }
151
152    /**
153     * Lazy breadcrumbs HTML renderer factory.
154     */
155    private function getBreadcrumbsHtmlRenderer(): BreadcrumbsHtmlRenderer
156    {
157        if (!$this->breadcrumbsHtmlRenderer instanceof BreadcrumbsHtmlRenderer) {
158            $this->breadcrumbsHtmlRenderer = new BreadcrumbsHtmlRenderer();
159        }
160
161        return $this->breadcrumbsHtmlRenderer;
162    }
163
164    /**
165     * Lazy admin category tree presenter factory.
166     */
167    private function getAdminCategoryTreePresenter(): AdminCategoryTreePresenter
168    {
169        if (!$this->adminCategoryTreePresenter instanceof AdminCategoryTreePresenter) {
170            $this->adminCategoryTreePresenter = new AdminCategoryTreePresenter();
171        }
172
173        return $this->adminCategoryTreePresenter;
174    }
175
176    /**
177     * Lazy category service factory.
178     */
179    private function getCategoryService(): CategoryService
180    {
181        if (!$this->categoryService instanceof CategoryService) {
182            $this->categoryService = new CategoryService($this->getCategoryRepository());
183        }
184
185        return $this->categoryService;
186    }
187
188    /**
189     * Lazy tree facade factory.
190     */
191    private function getTreeFacade(): CategoryTreeFacade
192    {
193        if ($this->categoryTreeFacade === null) {
194            $this->categoryTreeFacade = new CategoryTreeFacade();
195        }
196
197        return $this->categoryTreeFacade;
198    }
199
200    /**
201     * @param int[] $groups
202     */
203    public function setGroups(array $groups): Category
204    {
205        if ([] === $groups) {
206            $groups = [-1];
207        }
208
209        $this->categoryPermissionContext->setGroups($groups);
210        return $this;
211    }
212
213    public function setLanguage(string $language): Category
214    {
215        $this->language = $language;
216        return $this;
217    }
218
219    public function getUser(): int
220    {
221        return $this->categoryPermissionContext->getUser();
222    }
223
224    /**
225     * @return int[]
226     */
227    public function getGroups(): array
228    {
229        return $this->categoryPermissionContext->getGroups();
230    }
231
232    /**
233     * Returns all categories with ordered category IDs according to the user
234     * and group permissions.
235     *
236     * @return array<int, array<string, mixed>>
237     */
238    public function getOrderedCategories(bool $withPermission = true, bool $withInactive = false): array
239    {
240        $categories = [];
241
242        $rows = $this->getCategoryRepository()->findOrderedCategories(
243            $this->categoryPermissionContext->getGroups(),
244            $this->categoryPermissionContext->getUser(),
245            $this->language,
246            $withPermission,
247            $withInactive,
248        );
249
250        foreach ($rows as $row) {
251            $id = (int) $row['id'];
252
253            $this->categoryCache->addCategory($id, $row);
254            $this->categoryCache->addCategoryName($id, $row);
255            $categories[$id] = $row
256            + [
257                'level' => $this->getLevelOf($id),
258            ];
259        }
260
261        foreach ($this->categoryCache->getCategoryNames() as $id => $row) {
262            $parentId = (int) ($row['parent_id'] ?? 0);
263            $categoryRef = $row;
264            $this->categoryCache->addChild($parentId, (int) $id, $categoryRef);
265        }
266
267        return $categories;
268    }
269
270    /**
271     * Get the level of the item id.
272     */
273    public function getLevelOf(int $categoryId): int
274    {
275        return $this->getTreeFacade()->getLevelOf($this->categoryCache->getCategoryNames(), $categoryId);
276    }
277
278    public function setUser(int $userId = -1): Category
279    {
280        $this->categoryPermissionContext->setUser($userId);
281        return $this;
282    }
283
284    /**
285     * Gets all categories and write them in an array.
286     * @return array<int, array<string, mixed>>
287     */
288    public function getAllCategories(): array
289    {
290        $categories = [];
291        $rows = $this->getCategoryRepository()->findAllCategories($this->language);
292        foreach ($rows as $row) {
293            $id = (int) $row['id'];
294            $categories[$id] = $row
295            + [
296                'level' => $this->getLevelOf($id),
297            ];
298        }
299
300        return $categories;
301    }
302
303    /**
304     * Gets all category IDs
305     *
306     * @return int[]
307     */
308    public function getAllCategoryIds(): array
309    {
310        return $this->getCategoryRepository()->findAllCategoryIds($this->language);
311    }
312
313    /**
314     * Gets paginated categories with sorting support for API.
315     *
316     * @param int $limit Number of items per page
317     * @param int $offset Starting offset
318     * @param string $sortField Field to sort by
319     * @param string $sortOrder Sort direction (ASC, DESC)
320     * @return array
321     */
322    public function getCategoriesPaginated(
323        int $limit = 25,
324        int $offset = 0,
325        string $sortField = 'id',
326        string $sortOrder = 'ASC',
327        bool $activeOnly = false,
328    ): array {
329        $categories = [];
330        $rows = $this->getCategoryRepository()->findCategoriesPaginated(
331            $this->language,
332            $limit,
333            $offset,
334            $sortField,
335            $sortOrder,
336            $activeOnly,
337            $this->getGroups(),
338            $this->getUser(),
339        );
340        foreach ($rows as $id => $row) {
341            if (!is_array($row)) {
342                continue;
343            }
344
345            $categories[(int) $id] = $row
346            + [
347                'level' => $this->getLevelOf((int) $id),
348            ];
349        }
350
351        return $categories;
352    }
353
354    /**
355     * Counts total categories for current language.
356     *
357     * @param bool $activeOnly Only count active categories
358     * @return int Total count
359     */
360    public function countCategories(bool $activeOnly = false): int
361    {
362        return $this->getCategoryRepository()->countCategories(
363            $this->language,
364            $activeOnly,
365            $this->getGroups(),
366            $this->getUser(),
367        );
368    }
369
370    /**
371     * Builds the category tree.
372     */
373    public function buildCategoryTree(int $parentId = 0, int $indent = 0): void
374    {
375        $this->categoryCache->setCatTree($this->getTreeFacade()->buildLinearTree(
376            $this->categoryCache->getCategories(),
377            $parentId,
378            $indent,
379        ));
380    }
381
382    /**
383     * Creates the category tree for the admin category overview.
384     */
385    /**
386     * @param array<int, array<string, mixed>> $categories
387     * @return array<int, array<array-key, mixed>>
388     */
389    public function buildAdminCategoryTree(array $categories, int $parentId = 0): array
390    {
391        return $this->getTreeFacade()->buildAdminCategoryTree($categories, $parentId);
392    }
393
394    /**
395     * Transforms the linear array in a 1D array in the order of the tree, with
396     * the info.
397     */
398    public function transform(int $categoryId): void
399    {
400        $adminCategoryTreePresenter = $this->getAdminCategoryTreePresenter();
401        $entries = $adminCategoryTreePresenter->transform(
402            $this->getTreeBuilder(),
403            $this->categoryCache->getCategoryNames(),
404            $this->categoryCache->getChildren(),
405            $categoryId,
406        );
407        foreach ($entries as $entry) {
408            $this->categoryCache->addTreeTabEntry($entry);
409        }
410    }
411
412    /**
413     * List in an array the root, super-root, ... of the $id.
414     *
415     * @return int[]
416     */
417    private function getNodes(int $categoryId): array
418    {
419        return $this->getTreeBuilder()->getNodes($this->categoryCache->getCategoryNames(), $categoryId);
420    }
421
422    /**
423     * List in an array of the $id of the child.
424     */
425    public function getChildren(int $categoryId): array
426    {
427        return $this->getTreeBuilder()->getChildren($this->categoryCache->getChildren(), $categoryId);
428    }
429
430    /**
431     * List in an array of the $id of the child.
432     *
433     * @return int[]
434     */
435    public function getChildNodes(int $categoryId): array
436    {
437        return $this->getTreeBuilder()->getChildNodes($this->categoryCache->getChildren(), $categoryId);
438    }
439
440    /**
441     * Try to expand from the parent_id to the node $id
442     */
443    public function expandTo(int $categoryId): void
444    {
445        $this->collapseAll();
446        $ascendants = $this->getNodes($categoryId);
447        $ascendants[] = $categoryId;
448        $numAscendants = count($ascendants);
449        for ($i = 0; $i < $numAscendants; ++$i) {
450            $ascendantId = (int) $ascendants[$i];
451            $lineIndex = $this->getLineCategory($ascendantId);
452            $entry = $this->categoryCache->getTreeTabEntry($lineIndex);
453            if ($entry !== null && array_key_exists('numChildren', $entry)) {
454                $numChildren = (int) $entry['numChildren'];
455                if ($numChildren > 0) {
456                    $this->expand($ascendantId);
457                    continue;
458                }
459
460                break;
461            }
462        }
463    }
464
465    /**
466     * Collapse the complete category tree.
467     */
468    public function collapseAll(): void
469    {
470        $numTreeTab = $this->categoryCache->countTreeTab();
471        for ($i = 0; $i < $numTreeTab; ++$i) {
472            $entry = $this->categoryCache->getTreeTabEntry($i);
473            if ($entry !== null && array_key_exists('symbol', $entry) && $entry['symbol'] === 'minus') {
474                $this->categoryCache->updateTreeTabEntry($i, ['symbol' => 'plus']);
475            }
476        }
477    }
478
479    /**
480     * Get the line number where to find the node $id in the category tree.
481     */
482    private function getLineCategory(int $categoryId): int
483    {
484        $num = $this->categoryCache->countTreeTab();
485        for ($i = 0; $i < $num; ++$i) {
486            $entry = $this->categoryCache->getTreeTabEntry($i);
487            if ($entry !== null && array_key_exists('id', $entry) && $entry['id'] === $categoryId) {
488                return $i;
489            }
490        }
491
492        return -1;
493    }
494
495    /**
496     * Expand the node $id.
497     */
498    public function expand(int $categoryId): void
499    {
500        $lineIndex = $this->getLineCategory($categoryId);
501        $this->categoryCache->updateTreeTabEntry($lineIndex, ['symbol' => 'minus']);
502    }
503
504    public function getCategoryData(int $categoryId): CategoryEntity
505    {
506        $language = $this->language;
507
508        return $language !== null
509            ? $this->getCategoryService()->getCategoryData($categoryId, $language)
510            : new CategoryEntity();
511    }
512
513    public function getPath(
514        int $catId,
515        string $separator = ' / ',
516        bool $renderAsHtml = false,
517        string $useCssClass = 'breadcrumb',
518    ): string {
519        $ids = $this->getNodes($catId);
520        $segments = $this->getBreadcrumbsBuilder()->buildFromIds($this->categoryCache->getCategoryNames(), $ids);
521
522        if ($segments === []) {
523            return '';
524        }
525
526        if ($renderAsHtml) {
527            return $this->getBreadcrumbsHtmlRenderer()->render($this->configuration, $segments, $useCssClass);
528        }
529
530        $names = array_map(static fn(array $s): string => $s['name'], $segments);
531        return implode($separator, $names);
532    }
533
534    /**
535     * Returns the breadcrumb path with the start page as the first segment.
536     *
537     * @param int $catId Category ID
538     * @param string $separator Separator for text mode
539     * @param bool $renderAsHtml Render as HTML or plain text
540     * @param string $useCssClass CSS class for HTML mode
541     * @param string|null $startPageName Optional start page name (defaults to Translation msgHome)
542     * @param string $startPageDescription Optional start page description
543     */
544    public function getPathWithStartPage(
545        int $catId,
546        string $separator = ' / ',
547        bool $renderAsHtml = false,
548        string $useCssClass = 'breadcrumb',
549        ?string $startPageName = null,
550        string $startPageDescription = '',
551    ): string {
552        $ids = $this->getNodes($catId);
553        $segments = $this->getBreadcrumbsBuilder()->buildFromIdsWithStartPage(
554            $this->categoryCache->getCategoryNames(),
555            $ids,
556            $startPageName,
557            $startPageDescription,
558        );
559
560        if ($segments === []) {
561            return '';
562        }
563
564        if ($renderAsHtml) {
565            return $this->getBreadcrumbsHtmlRenderer()->render($this->configuration, $segments, $useCssClass);
566        }
567
568        $names = array_map(static fn(array $s): string => $s['name'], $segments);
569        return implode($separator, $names);
570    }
571
572    public function getCategoryIdFromFaq(int $faqId): int
573    {
574        $cats = $this->getCategoryIdsFromFaq($faqId);
575        return $cats[0] ?? 0;
576    }
577
578    /**
579     * @return int[]
580     */
581    public function getCategoryIdsFromFaq(int $faqId): array
582    {
583        $categories = $this->getCategoriesFromFaq($faqId);
584        $result = [];
585        foreach ($categories as $category) {
586            if (!array_key_exists('id', $category)) {
587                continue;
588            }
589
590            $result[] = (int) $category['id'];
591        }
592
593        return $result;
594    }
595
596    public function getCategoryIdFromName(string $categoryName): int|bool
597    {
598        $id = $this->getCategoryRepository()->findCategoryIdByName($categoryName);
599        return $id ?? false;
600    }
601
602    /**
603     * @return array<int, array<string, mixed>>
604     */
605    public function getCategoriesFromFaq(int $faqId): array
606    {
607        $rows = $this->getCategoryRepository()->findCategoriesFromFaq($faqId, (string) $this->language);
608        foreach ($rows as $id => $row) {
609            $this->categoryCache->addCategory((int) $id, $row);
610        }
611
612        return $rows;
613    }
614
615    public function create(CategoryEntity $categoryEntity): ?int
616    {
617        return $this->getCategoryRepository()->create($categoryEntity);
618    }
619
620    public function checkIfCategoryExists(CategoryEntity $categoryEntity): int
621    {
622        return $this->getCategoryRepository()->countByNameLangParent(
623            $categoryEntity->getName(),
624            $categoryEntity->getLang(),
625            $categoryEntity->getParentId(),
626        );
627    }
628
629    public function update(CategoryEntity $categoryEntity): bool
630    {
631        return $this->getCategoryRepository()->update($categoryEntity);
632    }
633
634    public function moveOwnership(int $currentOwner, int $newOwner): bool
635    {
636        return $this->getCategoryRepository()->moveOwnership($currentOwner, $newOwner);
637    }
638
639    public function hasLanguage(int $categoryId, string $categoryLanguage): bool
640    {
641        return $this->getCategoryRepository()->hasLanguage($categoryId, $categoryLanguage);
642    }
643
644    public function updateParentCategory(int $categoryId, int $parentId): bool
645    {
646        if ($categoryId === $parentId) {
647            return false;
648        }
649
650        return $this->getCategoryRepository()->updateParentCategory($categoryId, $parentId);
651    }
652
653    public function delete(int $categoryId, string $categoryLang): bool
654    {
655        return $this->getCategoryRepository()->delete($categoryId, $categoryLang);
656    }
657
658    /**
659     * @return array<string, string>
660     */
661    public function getCategoryLanguagesTranslated(int $categoryId): array
662    {
663        return $this->getCategoryRepository()->getCategoryLanguagesTranslated($categoryId);
664    }
665
666    public function getMissingCategories(): void
667    {
668        $rows = $this->getCategoryService()->getMissingCategories($this->language);
669        foreach ($rows as $row) {
670            $id = (int) $row['id'];
671            $parentId = (int) $row['parent_id'];
672
673            $currentNames = $this->categoryCache->getCategoryNames();
674            if (!array_key_exists($id, $currentNames)) {
675                $this->categoryCache->addCategoryName($id, $row);
676                $this->categoryCache->addCategory($id, $row);
677
678                $categoryNameRef = $this->categoryCache->getCategoryName($id);
679                $this->categoryCache->addChild($parentId, $id, $categoryNameRef);
680            }
681        }
682    }
683
684    /**
685     * Returns the user id of the category owner
686     */
687    public function getOwner(?int $categoryId = null): int
688    {
689        return $this->categoryPermissionContext->getOwner($categoryId);
690    }
691
692    /**
693     * Returns the moderator group id for a given category or 0 if none assigned.
694     */
695    public function getModeratorGroupId(int $categoryId): int
696    {
697        return $this->categoryPermissionContext->getModeratorGroupId($categoryId);
698    }
699
700    /**
701     * Returns the category tree as an array.
702     *
703     * @return array<int, array<string, mixed>>
704     */
705    public function getCategoryTree(): array
706    {
707        return $this->categoryCache->getCatTree();
708    }
709
710    public function getCategoryName(int $categoryId): string
711    {
712        $categoryName = $this->categoryCache->getCategoryName($categoryId);
713        return (string) ($categoryName['name'] ?? '');
714    }
715
716    public function getCategoryDescription(int $categoryId): string
717    {
718        $categoryName = $this->categoryCache->getCategoryName($categoryId);
719        return (string) ($categoryName['description'] ?? '');
720    }
721
722    /**
723     * Checks if a category has a link to a specific FAQ.
724     */
725    public function categoryHasLinkToFaq(int $faqId, int $categoryId): bool
726    {
727        return $this->getCategoryRepository()->hasLinkToFaq($faqId, $categoryId);
728    }
729
730    /**
731     * Returns all category names from the internal cache.
732     * This method replaces the deprecated public property $categoryNames.
733     *
734     * @return array<int, array<string, mixed>>
735     * @since 4.2.0
736     */
737    public function getCategoryNames(): array
738    {
739        return $this->categoryCache->getCategoryNames();
740    }
741}