Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
85.17% covered (success)
85.17%
270 / 317
61.90% covered (warning)
61.90%
13 / 21
CRAP
0.00% covered (danger)
0.00%
0 / 1
CategoryRepository
85.17% covered (success)
85.17%
270 / 317
61.90% covered (warning)
61.90%
13 / 21
117.99
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 mapRow
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
1
 findOrderedCategories
96.43% covered (success)
96.43%
27 / 28
0.00% covered (danger)
0.00%
0 / 1
11
 findAllCategories
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
8
 findAllCategoryIds
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
8
 findCategoriesPaginated
94.59% covered (success)
94.59%
35 / 37
0.00% covered (danger)
0.00%
0 / 1
11.02
 countCategories
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
30
 findByIdAndLanguage
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
3
 findCategoriesFromFaq
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
6
 findCategoryIdByName
88.89% covered (success)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
2.01
 create
81.82% covered (success)
81.82%
18 / 22
0.00% covered (danger)
0.00%
0 / 1
2.02
 getTenantQuotaEnforcer
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 update
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
1
 moveOwnership
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 hasLanguage
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 updateParentCategory
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 delete
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 getCategoryLanguagesTranslated
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
56
 findMissingCategories
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
1 / 1
8
 countByNameLangParent
92.31% covered (success)
92.31%
12 / 13
0.00% covered (danger)
0.00%
0 / 1
6.02
 hasLinkToFaq
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
6.02
1<?php
2
3/**
4 * Category repository implementation for phpMyFAQ.
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 * @copyright 2025 phpMyFAQ Team
13 * @license   https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
14 * @link      https://www.phpmyfaq.de
15 * @since     2025-10-18
16 */
17
18declare(strict_types=1);
19
20namespace phpMyFAQ\Category;
21
22use phpMyFAQ\Category\Permission\CategoryPermissionService;
23use phpMyFAQ\Configuration;
24use phpMyFAQ\Database;
25use phpMyFAQ\Entity\CategoryEntity;
26use phpMyFAQ\Tenant\TenantQuotaEnforcer;
27
28class CategoryRepository implements CategoryRepositoryInterface
29{
30    private ?TenantQuotaEnforcer $tenantQuotaEnforcer = null;
31
32    public function __construct(
33        private readonly Configuration $configuration,
34    ) {
35    }
36
37    /**
38     * Small mapper to cast DB row to a normalized category array.
39     * @param array<array-key, mixed> $row
40     * @return array<string, mixed>
41     */
42    private function mapRow(array $row): array
43    {
44        return [
45            'id' => (int) ($row['id'] ?? 0),
46            'lang' => (string) ($row['lang'] ?? ''),
47            'parent_id' => (int) ($row['parent_id'] ?? 0),
48            'name' => (string) ($row['name'] ?? ''),
49            'description' => (string) ($row['description'] ?? ''),
50            'user_id' => (int) ($row['user_id'] ?? 0),
51            'group_id' => (int) ($row['group_id'] ?? -1),
52            'active' => (int) ($row['active'] ?? 0),
53            'show_home' => (int) ($row['show_home'] ?? 0),
54            'image' => (string) ($row['image'] ?? ''),
55        ];
56    }
57
58    /**
59     * @inheritDoc
60     */
61    public function findOrderedCategories(
62        array $groups,
63        int $userId,
64        ?string $language,
65        bool $withPermission = true,
66        bool $withInactive = false,
67    ): array {
68        $where = '';
69
70        if ($withPermission) {
71            $categoryPermissionService = new CategoryPermissionService();
72            $where = $withInactive
73                ? $categoryPermissionService->buildWhereClauseWithInactive($groups, $userId)
74                : $categoryPermissionService->buildWhereClause($groups, $userId);
75        }
76
77        if ($language !== null && preg_match(pattern: '/^[a-z\-]{2,}$/', subject: $language)) {
78            $where .= $where === '' ? ' WHERE' : ' AND';
79            $where .= " fc.lang = '" . $this->configuration->getDb()->escape($language) . "'";
80        }
81
82        $prefix = Database::getTablePrefix();
83        $query = <<<SQL
84                SELECT
85                    fc.id AS id,
86                    fc.lang AS lang,
87                    fc.parent_id AS parent_id,
88                    fc.name AS name,
89                    fc.description AS description,
90                    fc.user_id AS user_id,
91                    fc.group_id AS group_id,
92                    fc.active AS active,
93                    fc.image AS image,
94                    fc.show_home AS show_home
95                FROM
96                    {$prefix}faqcategories fc
97                LEFT JOIN {$prefix}faqcategory_group fg
98                    ON fc.id = fg.category_id
99                LEFT JOIN {$prefix}faqcategory_order fco
100                    ON fc.id = fco.category_id
101                LEFT JOIN {$prefix}faqcategory_user fu
102                    ON fc.id = fu.category_id
103                {$where}
104                GROUP BY
105                    fc.id, fc.lang, fc.parent_id, fc.name, fc.description, fc.user_id, fc.group_id, fc.active, fc.image,
106                    fc.show_home, fco.position
107                ORDER BY
108                    fco.position, fc.id ASC
109            SQL;
110
111        $result = $this->configuration->getDb()->query($query);
112        $categories = [];
113
114        if ($result) {
115            while (true) {
116                $row = $this->configuration->getDb()->fetchArray($result);
117                if ($row === false || $row === null || $row === []) {
118                    break;
119                }
120
121                $mapped = $this->mapRow($row);
122                $categories[(int) $mapped['id']] = $mapped;
123            }
124        }
125
126        return $categories;
127    }
128
129    /**
130     * @inheritDoc
131     */
132    public function findAllCategories(?string $language = null): array
133    {
134        $categories = [];
135        $prefix = Database::getTablePrefix();
136        $query = sprintf(
137            'SELECT id, lang, parent_id, name, description, user_id, group_id, active, show_home, image FROM %sfaqcategories',
138            $prefix,
139        );
140        if ($language !== null && preg_match(pattern: '/^[a-z\-]{2,}$/', subject: $language)) {
141            $query .= " WHERE lang = '" . $this->configuration->getDb()->escape($language) . "'";
142        }
143
144        $result = $this->configuration->getDb()->query($query);
145
146        if ($result) {
147            while (true) {
148                $row = $this->configuration->getDb()->fetchArray($result);
149                if ($row === false || $row === null || $row === []) {
150                    break;
151                }
152
153                $mapped = $this->mapRow($row);
154                $categories[(int) $mapped['id']] = $mapped;
155            }
156        }
157
158        return $categories;
159    }
160
161    /**
162     * @inheritDoc
163     */
164    public function findAllCategoryIds(?string $language = null): array
165    {
166        $categories = [];
167
168        $query = sprintf('SELECT id FROM %sfaqcategories', Database::getTablePrefix());
169
170        if ($language !== null && preg_match(pattern: '/^[a-z\-]{2,}$/', subject: $language)) {
171            $query .= sprintf(" WHERE lang = '%s'", $this->configuration->getDb()->escape($language));
172        }
173
174        $result = $this->configuration->getDb()->query($query);
175
176        if ($result) {
177            while (true) {
178                $row = $this->configuration->getDb()->fetchArray($result);
179                if ($row === false || $row === null || $row === []) {
180                    break;
181                }
182
183                $categories[] = (int) $row['id'];
184            }
185        }
186
187        return $categories;
188    }
189
190    /**
191     * Find categories with pagination and sorting support.
192     *
193     * @param string|null $language Language code filter
194     * @param int $limit Number of items per page
195     * @param int $offset Starting offset
196     * @param string $sortField Field to sort by
197     * @param string $sortOrder Sort direction (ASC, DESC)
198     * @return array
199     */
200    /* @mago-expect lint:excessive-parameter-list - pagination, sorting, and permission filters are all query inputs; a criteria object is planned */
201    public function findCategoriesPaginated(
202        ?string $language = null,
203        int $limit = 25,
204        int $offset = 0,
205        string $sortField = 'id',
206        string $sortOrder = 'ASC',
207        bool $activeOnly = false,
208        array $groups = [-1],
209        int $userId = -1,
210    ): array {
211        $categories = [];
212
213        // Whitelist validation for the sort field
214        $allowedSortFields = ['id', 'name', 'parent_id', 'active'];
215        if (!in_array($sortField, $allowedSortFields, strict: true)) {
216            $sortField = 'id';
217        }
218
219        $prefix = Database::getTablePrefix();
220
221        $categoryPermissionService = new CategoryPermissionService();
222        $permissionWhere = $activeOnly
223            ? $categoryPermissionService->buildWhereClause($groups, $userId)
224            : $categoryPermissionService->buildWhereClauseWithInactive($groups, $userId);
225
226        $query = sprintf(
227            'SELECT fc.id, fc.lang, fc.parent_id, fc.name, fc.description, fc.user_id, fc.group_id, fc.active, fc.show_home, fc.image FROM %sfaqcategories fc LEFT JOIN %sfaqcategory_group fg ON fc.id = fg.category_id LEFT JOIN %sfaqcategory_user fu ON fc.id = fu.category_id %s',
228            $prefix,
229            $prefix,
230            $prefix,
231            $permissionWhere,
232        );
233
234        $additionalConditions = [];
235
236        if ($language !== null && preg_match(pattern: '/^[a-z\-]{2,}$/', subject: $language)) {
237            $additionalConditions[] = "fc.lang = '" . $this->configuration->getDb()->escape($language) . "'";
238        }
239
240        if ($additionalConditions !== []) {
241            $query .= ' AND ' . implode(' AND ', $additionalConditions);
242        }
243
244        $query .= sprintf(
245            ' GROUP BY fc.id, fc.lang, fc.parent_id, fc.name, fc.description, fc.user_id, fc.group_id, fc.active, fc.show_home, fc.image ORDER BY fc.%s %s LIMIT %d OFFSET %d',
246            $sortField,
247            $sortOrder,
248            $limit,
249            $offset,
250        );
251
252        $result = $this->configuration->getDb()->query($query);
253
254        if ($result) {
255            while (true) {
256                $row = $this->configuration->getDb()->fetchArray($result);
257                if ($row === false || $row === null || $row === []) {
258                    break;
259                }
260
261                $mapped = $this->mapRow($row);
262                $categories[(int) $mapped['id']] = $mapped;
263            }
264        }
265
266        return $categories;
267    }
268
269    /**
270     * Count total categories for a language.
271     *
272     * @param string|null $language Language code filter
273     * @param bool $activeOnly Only count active categories
274     * @return int Total count
275     */
276    public function countCategories(
277        ?string $language = null,
278        bool $activeOnly = false,
279        array $groups = [-1],
280        int $userId = -1,
281    ): int {
282        $prefix = Database::getTablePrefix();
283
284        $categoryPermissionService = new CategoryPermissionService();
285        $permissionWhere = $activeOnly
286            ? $categoryPermissionService->buildWhereClause($groups, $userId)
287            : $categoryPermissionService->buildWhereClauseWithInactive($groups, $userId);
288
289        $query = sprintf(
290            'SELECT COUNT(DISTINCT fc.id) as total FROM %sfaqcategories fc LEFT JOIN %sfaqcategory_group fg ON fc.id = fg.category_id LEFT JOIN %sfaqcategory_user fu ON fc.id = fu.category_id %s',
291            $prefix,
292            $prefix,
293            $prefix,
294            $permissionWhere,
295        );
296
297        if ($language !== null && preg_match(pattern: '/^[a-z\-]{2,}$/', subject: $language)) {
298            $query .= " AND fc.lang = '" . $this->configuration->getDb()->escape($language) . "'";
299        }
300
301        $result = $this->configuration->getDb()->query($query);
302        $row = $this->configuration->getDb()->fetchObject($result);
303
304        return $row instanceof \stdClass ? (int) ($row->total ?? 0) : 0;
305    }
306
307    public function findByIdAndLanguage(int $categoryId, string $language): ?CategoryEntity
308    {
309        $categoryEntity = null;
310
311        $query = sprintf(
312            "SELECT * FROM %sfaqcategories WHERE id = %d AND lang = '%s'",
313            Database::getTablePrefix(),
314            $categoryId,
315            $this->configuration->getDb()->escape($language),
316        );
317
318        $result = $this->configuration->getDb()->query($query);
319
320        $row = $result ? $this->configuration->getDb()->fetchObject($result) : false;
321        if ($row instanceof \stdClass) {
322            return new CategoryEntity()
323                ->setId((int) $row->id)
324                ->setLang((string) $row->lang)
325                ->setParentId((int) $row->parent_id)
326                ->setName((string) $row->name)
327                ->setDescription((string) ($row->description ?? ''))
328                ->setUserId((int) $row->user_id)
329                ->setGroupId((int) $row->group_id)
330                ->setActive((int) $row->active !== 0)
331                ->setShowHome((int) $row->show_home !== 0)
332                ->setImage((string) ($row->image ?? ''));
333        }
334
335        return $categoryEntity;
336    }
337
338    /**
339     * @inheritDoc
340     */
341    public function findCategoriesFromFaq(int $faqId, string $language): array
342    {
343        $query = sprintf(
344            "
345            SELECT
346                fc.id AS id,
347                fc.lang AS lang,
348                fc.parent_id AS parent_id,
349                fc.name AS name,
350                fc.description AS description
351            FROM
352                %sfaqcategoryrelations fcr,
353                %sfaqcategories fc
354            WHERE
355                fc.id = fcr.category_id
356            AND
357                fcr.record_id = %d
358            AND
359                fcr.category_lang = '%s'
360            AND
361                fc.lang = '%s'",
362            Database::getTablePrefix(),
363            Database::getTablePrefix(),
364            $faqId,
365            $this->configuration->getDb()->escape($language),
366            $this->configuration->getDb()->escape($language),
367        );
368
369        $result = $this->configuration->getDb()->query($query);
370        $categories = [];
371        if ($this->configuration->getDb()->numRows($result) > 0) {
372            while (true) {
373                $row = $this->configuration->getDb()->fetchArray($result);
374                if ($row === false || $row === null || $row === []) {
375                    break;
376                }
377
378                $categories[(int) $row['id']] = [
379                    'id' => (int) $row['id'],
380                    'lang' => (string) $row['lang'],
381                    'parent_id' => (int) $row['parent_id'],
382                    'name' => (string) $row['name'],
383                    'description' => (string) $row['description'],
384                ];
385            }
386        }
387
388        return $categories;
389    }
390
391    public function findCategoryIdByName(string $categoryName): ?int
392    {
393        $query = sprintf(
394            "SELECT id FROM %sfaqcategories WHERE name = '%s'",
395            Database::getTablePrefix(),
396            $this->configuration->getDb()->escape($categoryName),
397        );
398
399        $result = $this->configuration->getDb()->query($query);
400        if ($this->configuration->getDb()->numRows($result) > 0) {
401            return (int) $this->configuration->getDb()->fetchRow($result);
402        }
403
404        return null;
405    }
406
407    public function create(CategoryEntity $categoryEntity): ?int
408    {
409        $this->getTenantQuotaEnforcer()->assertCanCreateCategory();
410
411        if ($categoryEntity->getId() === 0) {
412            $categoryEntity->setId($this->configuration->getDb()->nextId(
413                Database::getTablePrefix() . 'faqcategories',
414                column: 'id',
415            ));
416        }
417
418        $query = sprintf(
419            "INSERT INTO    %sfaqcategories(id, lang, parent_id, name, description, user_id, group_id, active, image, show_home)    VALUES(%d, '%s', %d, '%s', '%s', %d, %d, %d, '%s', %d)",
420            Database::getTablePrefix(),
421            $categoryEntity->getId(),
422            $this->configuration->getDb()->escape($categoryEntity->getLang()),
423            $categoryEntity->getParentId(),
424            $this->configuration->getDb()->escape($categoryEntity->getName()),
425            $this->configuration->getDb()->escape($categoryEntity->getDescription() ?? ''),
426            $categoryEntity->getUserId(),
427            $categoryEntity->getGroupId(),
428            $categoryEntity->getActive(),
429            $this->configuration->getDb()->escape($categoryEntity->getImage() ?? ''),
430            $categoryEntity->getShowHome(),
431        );
432
433        $this->configuration->getDb()->query($query);
434
435        return $categoryEntity->getId();
436    }
437
438    private function getTenantQuotaEnforcer(): TenantQuotaEnforcer
439    {
440        return $this->tenantQuotaEnforcer ??= TenantQuotaEnforcer::createFromDatabaseDriver(
441            $this->configuration->getDb(),
442        );
443    }
444
445    public function update(CategoryEntity $categoryEntity): bool
446    {
447        $query = sprintf(
448            "UPDATE %sfaqcategories SET name = '%s', description = '%s', user_id = %d, group_id = %d, active = %d, show_home = %d, image = '%s' WHERE id = %d AND lang = '%s'",
449            Database::getTablePrefix(),
450            $this->configuration->getDb()->escape($categoryEntity->getName()),
451            $this->configuration->getDb()->escape($categoryEntity->getDescription() ?? ''),
452            $categoryEntity->getUserId(),
453            $categoryEntity->getGroupId(),
454            $categoryEntity->getActive(),
455            $categoryEntity->getShowHome(),
456            $this->configuration->getDb()->escape($categoryEntity->getImage() ?? ''),
457            $categoryEntity->getId(),
458            $this->configuration->getDb()->escape($categoryEntity->getLang()),
459        );
460
461        return (bool) $this->configuration->getDb()->query($query);
462    }
463
464    public function moveOwnership(int $currentOwner, int $newOwner): bool
465    {
466        $query = sprintf(
467            'UPDATE %sfaqcategories SET user_id = %d WHERE user_id = %d',
468            Database::getTablePrefix(),
469            $newOwner,
470            $currentOwner,
471        );
472
473        return (bool) $this->configuration->getDb()->query($query);
474    }
475
476    public function hasLanguage(int $categoryId, string $categoryLanguage): bool
477    {
478        $query = sprintf(
479            "SELECT lang FROM %sfaqcategories WHERE id = %d AND lang = '%s'",
480            Database::getTablePrefix(),
481            $categoryId,
482            $this->configuration->getDb()->escape($categoryLanguage),
483        );
484
485        $result = $this->configuration->getDb()->query($query);
486
487        return $this->configuration->getDb()->numRows($result) > 0;
488    }
489
490    public function updateParentCategory(int $categoryId, int $parentId): bool
491    {
492        $query = sprintf(
493            'UPDATE %sfaqcategories SET parent_id = %d WHERE id = %d',
494            Database::getTablePrefix(),
495            $parentId,
496            $categoryId,
497        );
498
499        return (bool) $this->configuration->getDb()->query($query);
500    }
501
502    public function delete(int $categoryId, string $categoryLang): bool
503    {
504        $query = sprintf(
505            "DELETE FROM %sfaqcategories WHERE id = %d AND lang = '%s'",
506            Database::getTablePrefix(),
507            $categoryId,
508            $this->configuration->getDb()->escape($categoryLang),
509        );
510
511        return (bool) $this->configuration->getDb()->query($query);
512    }
513
514    /**
515     * @inheritDoc
516     */
517    public function getCategoryLanguagesTranslated(int $categoryId): array
518    {
519        $existingLanguages = $this->configuration->getLanguage()->isLanguageAvailable($categoryId, 'faqcategories');
520
521        $translated = [];
522        foreach ($existingLanguages as $existingLanguage) {
523            $whereParts = [];
524            if ($categoryId !== 0) {
525                $whereParts[] = 'id = ' . (int) $categoryId;
526            }
527
528            $whereParts[] = "lang = '" . $this->configuration->getDb()->escape($existingLanguage) . "'";
529            $query = sprintf(
530                'SELECT name, description FROM %sfaqcategories WHERE %s',
531                Database::getTablePrefix(),
532                implode(separator: ' AND ', array: $whereParts),
533            );
534
535            $result = $this->configuration->getDb()->query($query);
536            $row = $result ? $this->configuration->getDb()->fetchArray($result) : false;
537            if (is_array($row) && $row !== []) {
538                $name = (string) ($row['name'] ?? '');
539                $description = (string) ($row['description'] ?? '');
540                $translated[$existingLanguage] = $name . ($description === '' ? '' : '  (' . $description . ')');
541            }
542        }
543
544        ksort($translated);
545
546        return $translated;
547    }
548
549    public function findMissingCategories(?string $language = null): array
550    {
551        $query = sprintf(
552            'SELECT id, lang, parent_id, name, description, user_id, group_id, active FROM %sfaqcategories',
553            Database::getTablePrefix(),
554        );
555        if ($language !== null && preg_match(pattern: '/^[a-z\-]{2,}$/', subject: $language)) {
556            $query .= " WHERE lang != '" . $this->configuration->getDb()->escape($language) . "'";
557        }
558
559        $query .= ' ORDER BY id';
560
561        $result = $this->configuration->getDb()->query($query);
562        $categories = [];
563        if ($result) {
564            while (true) {
565                $row = $this->configuration->getDb()->fetchArray($result);
566                if ($row === false || $row === null || $row === []) {
567                    break;
568                }
569
570                $categories[] = [
571                    'id' => (int) $row['id'],
572                    'lang' => (string) $row['lang'],
573                    'parent_id' => (int) $row['parent_id'],
574                    'name' => (string) $row['name'],
575                    'description' => (string) $row['description'],
576                    'user_id' => (int) $row['user_id'],
577                    'group_id' => (int) $row['group_id'],
578                    'active' => (int) $row['active'],
579                ];
580            }
581        }
582
583        return $categories;
584    }
585
586    public function countByNameLangParent(string $name, string $lang, int $parentId): int
587    {
588        $query = sprintf(
589            "SELECT COUNT(*) AS cnt FROM %sfaqcategories WHERE name = '%s' AND lang = '%s' AND parent_id = %d",
590            Database::getTablePrefix(),
591            $this->configuration->getDb()->escape($name),
592            $this->configuration->getDb()->escape($lang),
593            $parentId,
594        );
595
596        $result = $this->configuration->getDb()->query($query);
597        if ($result) {
598            $row = $this->configuration->getDb()->fetchArray($result);
599            if ($row !== false && $row !== null && $row !== [] && array_key_exists('cnt', $row)) {
600                return (int) $row['cnt'];
601            }
602        }
603
604        return 0;
605    }
606
607    /**
608     * Checks if a category has a link to a specific FAQ.
609     */
610    public function hasLinkToFaq(int $faqId, int $categoryId): bool
611    {
612        $query = sprintf(
613            'SELECT COUNT(*) AS cnt FROM %sfaqcategoryrelations WHERE category_id = %d AND record_id = %d',
614            Database::getTablePrefix(),
615            $categoryId,
616            $faqId,
617        );
618
619        $result = $this->configuration->getDb()->query($query);
620        if ($result) {
621            $row = $this->configuration->getDb()->fetchArray($result);
622            if ($row !== false && $row !== null && $row !== [] && array_key_exists('cnt', $row)) {
623                return (int) $row['cnt'] > 0;
624            }
625        }
626
627        return false;
628    }
629}