ReleaseBrowseService::getBrowseCount()   F
last analyzed

Complexity

Conditions 13
Paths 465

Size

Total Lines 98
Code Lines 52

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 52
c 3
b 0
f 0
dl 0
loc 98
rs 3.193
cc 13
nc 465
nop 4

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace App\Services\Releases;
4
5
use App\Facades\Search;
6
use App\Models\Category;
7
use App\Models\Release;
8
use App\Models\Settings;
9
use Illuminate\Database\Eloquent\Collection;
10
use Illuminate\Support\Facades\Cache;
11
use Illuminate\Support\Facades\DB;
12
use Illuminate\Support\Facades\Log;
13
14
/**
15
 * Service for browsing and ordering releases on the frontend.
16
 */
17
class ReleaseBrowseService
18
{
19
    private const CACHE_VERSION_KEY = 'releases:cache_version';
20
21
    // RAR/ZIP Password indicator.
22
    public const PASSWD_NONE = 0; // No password.
23
24
    public const PASSWD_RAR = 1; // Definitely passworded.
25
26
    public function __construct() {}
27
28
    /**
29
     * Used for Browse results with optional search term filtering via search index.
30
     *
31
     * @param  string|null  $searchTerm  Optional search term to filter by (uses search index)
32
     * @return Collection|mixed
33
     */
34
    public function getBrowseRange($page, $cat, $start, $num, $orderBy, int $maxAge = -1, array $excludedCats = [], int|string $groupName = -1, int $minSize = 0, ?string $searchTerm = null): mixed
35
    {
36
        $cacheVersion = $this->getCacheVersion();
37
        $page = max(1, $page);
38
        $start = max(0, $start);
39
40
        $orderBy = $this->getBrowseOrder($orderBy);
41
42
        // Use search index filtering when a search term is provided
43
        $searchIndexFilter = '';
44
        $searchIndexIds = [];
45
        if (! empty($searchTerm) && Search::isAvailable()) {
46
            $searchResult = Search::searchReleasesWithFuzzy(['searchname' => $searchTerm], $num * 10);
47
            $searchIndexIds = $searchResult['ids'] ?? [];
48
49
            if (config('app.debug') && ($searchResult['fuzzy'] ?? false)) {
50
                Log::debug('getBrowseRange: Using fuzzy search results for browse filtering');
51
            }
52
53
            if (empty($searchIndexIds)) {
54
                // No results from search index, return empty result
55
                return [];
56
            }
57
58
            $searchIndexFilter = sprintf(' AND r.id IN (%s)', implode(',', array_map('intval', $searchIndexIds)));
59
        }
60
61
        $qry = sprintf(
62
            "SELECT r.id, r.searchname, r.groups_id, r.guid, r.postdate, r.categories_id, r.size, r.totalpart, r.fromname, r.passwordstatus, r.grabs, r.comments, r.adddate, r.videos_id, r.tv_episodes_id, r.haspreview, r.jpgstatus, r.nfostatus, cp.title AS parent_category, c.title AS sub_category, r.group_name,
63
				CONCAT(cp.title, ' > ', c.title) AS category_name,
64
				CONCAT(cp.id, ',', c.id) AS category_ids,
65
				df.failed AS failed,
66
				rn.releases_id AS nfoid,
67
				re.releases_id AS reid,
68
				v.tvdb, v.trakt, v.tvrage, v.tvmaze, v.imdb, v.tmdb,
69
				m.imdbid, m.tmdbid, m.traktid,
70
				tve.title, tve.firstaired
71
			FROM
72
			(
73
				SELECT r.id, r.searchname, r.guid, r.postdate, r.groups_id, r.categories_id, r.size, r.totalpart, r.fromname, r.passwordstatus, r.grabs, r.comments, r.adddate, r.videos_id, r.tv_episodes_id, r.haspreview, r.jpgstatus, r.nfostatus, g.name AS group_name, r.movieinfo_id
74
				FROM releases r
75
				LEFT JOIN usenet_groups g ON g.id = r.groups_id
76
				WHERE r.passwordstatus %1\$s
77
				%2\$s %3\$s %4\$s %5\$s %6\$s %7\$s
78
				ORDER BY %8\$s %9\$s %10\$s
79
			) r
80
			LEFT JOIN categories c ON c.id = r.categories_id
81
			LEFT JOIN root_categories cp ON cp.id = c.root_categories_id
82
			LEFT OUTER JOIN videos v ON r.videos_id = v.id
83
			LEFT OUTER JOIN tv_episodes tve ON r.tv_episodes_id = tve.id
84
			LEFT OUTER JOIN movieinfo m ON m.id = r.movieinfo_id
85
			LEFT OUTER JOIN video_data re ON re.releases_id = r.id
86
			LEFT OUTER JOIN release_nfos rn ON rn.releases_id = r.id
87
			LEFT OUTER JOIN dnzb_failures df ON df.release_id = r.id
88
			GROUP BY r.id
89
			ORDER BY %8\$s %9\$s",
90
            $this->showPasswords(),
91
            Category::getCategorySearch($cat),
92
            ($maxAge > 0 ? (' AND postdate > NOW() - INTERVAL '.$maxAge.' DAY ') : ''),
93
            (\count($excludedCats) ? (' AND r.categories_id NOT IN ('.implode(',', $excludedCats).')') : ''),
94
            ((int) $groupName !== -1 ? sprintf(' AND g.name = %s ', escapeString($groupName)) : ''),
95
            ($minSize > 0 ? sprintf('AND r.size >= %d', $minSize) : ''),
96
            $searchIndexFilter,
97
            $orderBy[0],
98
            $orderBy[1],
99
            ($start === 0 ? ' LIMIT '.$num : ' LIMIT '.$num.' OFFSET '.$start)
100
        );
101
102
        $cacheKey = md5($cacheVersion.$qry.$page);
103
        $releases = Cache::get($cacheKey);
104
        if ($releases !== null) {
105
            return $releases;
106
        }
107
        $sql = DB::select($qry);
108
        if (\count($sql) > 0) {
109
            // When using search index, use the ID count for total rows
110
            if (! empty($searchIndexIds)) {
111
                $sql[0]->_totalcount = $sql[0]->_totalrows = count($searchIndexIds);
112
            } else {
113
                $possibleRows = $this->getBrowseCount($cat, $maxAge, $excludedCats, $groupName);
114
                $sql[0]->_totalcount = $sql[0]->_totalrows = $possibleRows;
115
            }
116
        }
117
        $expiresAt = now()->addMinutes(config('nntmux.cache_expiry_medium'));
118
        Cache::put($cacheKey, $sql, $expiresAt);
119
120
        return $sql;
121
    }
122
123
    /**
124
     * Used for pager on browse page.
125
     * Optimized to avoid expensive COUNT queries on large tables.
126
     * Uses sample-based counting and avoids JOINs whenever possible.
127
     */
128
    public function getBrowseCount(array $cat, int $maxAge = -1, array $excludedCats = [], int|string $groupName = ''): int
129
    {
130
        $maxResults = (int) config('nntmux.max_pager_results', 500000);
131
        $cacheExpiry = (int) config('nntmux.cache_expiry_short', 5);
132
133
        // Build a unique cache key for this specific query
134
        $cacheKey = 'browse_count_'.md5(serialize($cat).$maxAge.serialize($excludedCats).$groupName);
135
136
        // Check cache first - use longer cache time for count queries since they're expensive
137
        $count = Cache::get($cacheKey);
138
        if ($count !== null) {
139
            return (int) $count;
140
        }
141
142
        // Build optimized count query - avoid JOINs when possible
143
        $conditions = ['r.passwordstatus '.$this->showPasswords()];
144
145
        // Add category conditions
146
        $catQuery = Category::getCategorySearch($cat);
147
        $catQuery = preg_replace('/^(WHERE|AND)\s+/i', '', trim($catQuery));
148
        if (! empty($catQuery) && $catQuery !== '1=1') {
149
            $conditions[] = $catQuery;
150
        }
151
152
        if ($maxAge > 0) {
153
            $conditions[] = 'r.postdate > NOW() - INTERVAL '.$maxAge.' DAY';
154
        }
155
156
        if (! empty($excludedCats)) {
157
            $conditions[] = 'r.categories_id NOT IN ('.implode(',', array_map('intval', $excludedCats)).')';
158
        }
159
160
        // Only add group filter if specified - this requires a JOIN
161
        $needsGroupJoin = (int) $groupName !== -1;
162
        if ($needsGroupJoin) {
163
            $conditions[] = sprintf('g.name = %s', escapeString($groupName));
164
        }
165
166
        $whereSql = 'WHERE '.implode(' AND ', $conditions);
167
168
        try {
169
            // For queries with maxResults limit, use sample-based counting
170
            if ($maxResults > 0) {
171
                // Quick check using a small sample (1000 rows) to determine if we should
172
                // just return maxResults or do a full count
173
                $sampleLimit = min(1000, $maxResults);
174
175
                if ($needsGroupJoin) {
176
                    // Need JOIN for group filtering
177
                    $sampleQuery = sprintf(
178
                        'SELECT r.id FROM releases r INNER JOIN usenet_groups g ON g.id = r.groups_id %s ORDER BY r.id DESC LIMIT %d',
179
                        $whereSql,
180
                        $sampleLimit
181
                    );
182
                } else {
183
                    // No JOIN needed - much faster query on releases table only
184
                    $sampleQuery = sprintf(
185
                        'SELECT r.id FROM releases r %s ORDER BY r.id DESC LIMIT %d',
186
                        $whereSql,
187
                        $sampleLimit
188
                    );
189
                }
190
191
                $sampleResult = DB::select($sampleQuery);
192
                $sampleCount = count($sampleResult);
193
194
                // If we got the full sample, assume there are more rows than maxResults
195
                if ($sampleCount >= $sampleLimit) {
196
                    Cache::put($cacheKey, $maxResults, now()->addMinutes($cacheExpiry * 2));
197
198
                    return $maxResults;
199
                }
200
201
                // Fewer than sample limit - this is the actual count
202
                $count = $sampleCount;
203
            } else {
204
                // No max limit set, need full count
205
                if ($needsGroupJoin) {
206
                    $query = sprintf(
207
                        'SELECT COUNT(r.id) AS count FROM releases r INNER JOIN usenet_groups g ON g.id = r.groups_id %s',
208
                        $whereSql
209
                    );
210
                } else {
211
                    // No JOIN needed - simple count on releases table
212
                    $query = sprintf('SELECT COUNT(r.id) AS count FROM releases r %s', $whereSql);
213
                }
214
                $result = DB::select($query);
215
                $count = isset($result[0]) ? (int) $result[0]->count : 0;
216
            }
217
218
            // Cache with longer expiry for count queries
219
            Cache::put($cacheKey, $count, now()->addMinutes($cacheExpiry * 2));
220
221
            return $count;
222
        } catch (\Exception $e) {
223
            Log::error('getBrowseCount failed', ['error' => $e->getMessage()]);
224
225
            return 0;
226
        }
227
    }
228
229
    /**
230
     * Get the passworded releases clause.
231
     */
232
    public function showPasswords(): string
233
    {
234
        $show = (int) Settings::settingValue('showpasswordedrelease');
235
        $setting = $show ?? 0;
236
237
        return match ($setting) {
238
            1 => '<= '.self::PASSWD_RAR,
239
            default => '= '.self::PASSWD_NONE,
240
        };
241
    }
242
243
    /**
244
     * Use to order releases on site.
245
     */
246
    public function getBrowseOrder(array|string $orderBy): array
247
    {
248
        $orderArr = explode('_', ($orderBy === '' ? 'posted_desc' : $orderBy));
0 ignored issues
show
Bug introduced by
$orderBy === '' ? 'posted_desc' : $orderBy of type array is incompatible with the type string expected by parameter $string of explode(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

248
        $orderArr = explode('_', /** @scrutinizer ignore-type */ ($orderBy === '' ? 'posted_desc' : $orderBy));
Loading history...
introduced by
The condition $orderBy === '' is always false.
Loading history...
249
        $orderField = match ($orderArr[0]) {
250
            'cat' => 'categories_id',
251
            'name' => 'searchname',
252
            'size' => 'size',
253
            'files' => 'totalpart',
254
            'stats' => 'grabs',
255
            'added' => 'adddate',
256
            default => 'postdate',
257
        };
258
259
        return [$orderField, isset($orderArr[1]) && preg_match('/^(asc|desc)$/i', $orderArr[1]) ? $orderArr[1] : 'desc'];
260
    }
261
262
    /**
263
     * Return ordering types usable on site.
264
     *
265
     * @return string[]
266
     */
267
    public function getBrowseOrdering(): array
268
    {
269
        return [
270
            'name_asc',
271
            'name_desc',
272
            'cat_asc',
273
            'cat_desc',
274
            'posted_asc',
275
            'posted_desc',
276
            'added_asc',
277
            'added_desc',
278
            'size_asc',
279
            'size_desc',
280
            'files_asc',
281
            'files_desc',
282
            'stats_asc',
283
            'stats_desc',
284
        ];
285
    }
286
287
    /**
288
     * @return \Illuminate\Cache\|\Illuminate\Database\Eloquent\Collection|mixed
289
     */
290
    public function getShowsRange($userShows, $offset, $limit, $orderBy, int $maxAge = -1, array $excludedCats = [])
291
    {
292
        $orderBy = $this->getBrowseOrder($orderBy);
293
        $sql = sprintf(
294
            "SELECT r.id, r.searchname, r.guid, r.postdate, r.groups_id, r.categories_id, r.size, r.totalpart, r.fromname, r.passwordstatus, r.grabs, r.comments, r.adddate, r.videos_id, r.tv_episodes_id, r.haspreview, r.jpgstatus,  cp.title AS parent_category, c.title AS sub_category,
295
					CONCAT(cp.title, '->', c.title) AS category_name
296
				FROM releases r
297
				LEFT JOIN categories c ON c.id = r.categories_id
298
				LEFT JOIN root_categories cp ON cp.id = c.root_categories_id
299
				WHERE %s %s
300
				AND r.categories_id BETWEEN %d AND %d
301
				AND r.passwordstatus %s
302
				%s
303
				GROUP BY r.id
304
				ORDER BY %s %s %s",
305
            $this->uSQL($userShows, 'videos_id'),
306
            (! empty($excludedCats) ? ' AND r.categories_id NOT IN ('.implode(',', $excludedCats).')' : ''),
307
            Category::TV_ROOT,
308
            Category::TV_OTHER,
309
            $this->showPasswords(),
310
            ($maxAge > 0 ? sprintf(' AND r.postdate > NOW() - INTERVAL %d DAY ', $maxAge) : ''),
311
            $orderBy[0],
312
            $orderBy[1],
313
            ($offset === false ? '' : (' LIMIT '.$limit.' OFFSET '.$offset))
314
        );
315
        $expiresAt = now()->addMinutes(config('nntmux.cache_expiry_long'));
316
        $result = Cache::get(md5($sql));
317
        if ($result !== null) {
318
            return $result;
319
        }
320
        $result = Release::fromQuery($sql);
321
        Cache::put(md5($sql), $result, $expiresAt);
322
323
        return $result;
324
    }
325
326
    public function getShowsCount($userShows, int $maxAge = -1, array $excludedCats = []): int
327
    {
328
        return $this->getPagerCount(
329
            sprintf(
330
                'SELECT r.id
331
				FROM releases r
332
				WHERE %s %s
333
				AND r.categories_id BETWEEN %d AND %d
334
				AND r.passwordstatus %s
335
				%s',
336
                $this->uSQL($userShows, 'videos_id'),
337
                (\count($excludedCats) ? ' AND r.categories_id NOT IN ('.implode(',', $excludedCats).')' : ''),
338
                Category::TV_ROOT,
339
                Category::TV_OTHER,
340
                $this->showPasswords(),
341
                ($maxAge > 0 ? sprintf(' AND r.postdate > NOW() - INTERVAL %d DAY ', $maxAge) : '')
342
            )
343
        );
344
    }
345
346
    /**
347
     * Creates part of a query for some functions.
348
     */
349
    public function uSQL(Collection|array $userQuery, string $type): string
350
    {
351
        $sql = '(1=2 ';
352
        foreach ($userQuery as $query) {
353
            $sql .= sprintf('OR (r.%s = %d', $type, $query->$type);
354
            if (! empty($query->categories)) {
355
                $catsArr = explode('|', $query->categories);
356
                if (\count($catsArr) > 1) {
357
                    $sql .= sprintf(' AND r.categories_id IN (%s)', implode(',', $catsArr));
358
                } else {
359
                    $sql .= sprintf(' AND r.categories_id = %d', $catsArr[0]);
360
                }
361
            }
362
            $sql .= ') ';
363
        }
364
        $sql .= ') ';
365
366
        return $sql;
367
    }
368
369
    public static function bumpCacheVersion(): void
370
    {
371
        $current = Cache::get(self::CACHE_VERSION_KEY, 1);
372
        Cache::forever(self::CACHE_VERSION_KEY, $current + 1);
373
    }
374
375
    private function getCacheVersion(): int
376
    {
377
        return Cache::get(self::CACHE_VERSION_KEY, 1);
378
    }
379
380
    /**
381
     * Search releases using the search index with category filtering.
382
     * This method pre-filters results via the search index before hitting the database,
383
     * significantly improving performance for searches with text terms.
384
     *
385
     * @param  string  $searchTerm  Search term to match
386
     * @param  array  $categories  Category IDs to filter by (optional)
387
     * @param  int  $limit  Maximum number of results
388
     * @return array Array of release IDs matching the search criteria
389
     */
390
    public function searchByIndexWithCategories(string $searchTerm, array $categories = [], int $limit = 1000): array
391
    {
392
        if (empty($searchTerm) || ! Search::isAvailable()) {
393
            return [];
394
        }
395
396
        $searchResult = Search::searchReleasesWithFuzzy(['searchname' => $searchTerm], $limit);
397
        $releaseIds = $searchResult['ids'] ?? [];
398
399
        if (empty($releaseIds)) {
400
            return [];
401
        }
402
403
        // If categories are specified, filter the results by querying the database for just the IDs
404
        if (! empty($categories) && ! in_array(-1, $categories, true)) {
405
            $filteredIds = Release::query()
406
                ->select('id')
407
                ->whereIn('id', $releaseIds)
408
                ->whereIn('categories_id', $categories)
409
                ->pluck('id')
410
                ->toArray();
411
412
            return $filteredIds;
413
        }
414
415
        return $releaseIds;
416
    }
417
418
    /**
419
     * Get releases by external media ID using search index.
420
     * Useful for movie/TV browse pages that need to find all releases for a specific movie/show.
421
     *
422
     * @param  array  $externalIds  Associative array of external IDs (e.g., ['imdbid' => 123456])
423
     * @param  int  $limit  Maximum number of results
424
     * @return array Array of release IDs
425
     */
426
    public function getReleasesByExternalId(array $externalIds, int $limit = 100): array
427
    {
428
        if (empty($externalIds) || ! Search::isAvailable()) {
429
            return [];
430
        }
431
432
        return Search::searchReleasesByExternalId($externalIds, $limit);
433
    }
434
435
    /**
436
     * Get the count of releases for pager.
437
     *
438
     * @param  string  $query  The query to get the count from.
439
     */
440
    private function getPagerCount(string $query): int
441
    {
442
        $maxResults = (int) config('nntmux.max_pager_results');
443
        $cacheExpiry = config('nntmux.cache_expiry_short');
444
445
        // Generate cache key from original query
446
        $cacheKey = 'pager_count_'.md5($query);
447
448
        // Check cache first
449
        $count = Cache::get($cacheKey);
450
        if ($count !== null) {
451
            return (int) $count;
452
        }
453
454
        // Check if this is already a COUNT query
455
        if (preg_match('/SELECT\s+COUNT\s*\(/is', $query)) {
456
            // It's already a COUNT query, just execute it
457
            try {
458
                $result = DB::select($query);
459
                if (isset($result[0])) {
460
                    // Handle different possible column names
461
                    $count = $result[0]->count ?? $result[0]->total ?? 0;
462
                    // Check for COUNT(*) result without alias
463
                    if ($count === 0) {
464
                        foreach ($result[0] as $value) {
465
                            $count = (int) $value;
466
                            break;
467
                        }
468
                    }
469
                } else {
470
                    $count = 0;
471
                }
472
473
                // Cap the count at max results if applicable
474
                if ($maxResults > 0 && $count > $maxResults) {
475
                    $count = $maxResults;
476
                }
477
478
                // Cache the result
479
                Cache::put($cacheKey, $count, now()->addMinutes($cacheExpiry));
480
481
                return $count;
482
            } catch (\Exception $e) {
483
                return 0;
484
            }
485
        }
486
487
        // For regular SELECT queries, optimize for counting
488
        $countQuery = $query;
489
490
        // Remove ORDER BY clause (not needed for COUNT)
491
        $countQuery = preg_replace('/ORDER\s+BY\s+[^)]+$/is', '', $countQuery);
492
493
        // Remove GROUP BY if it's only grouping by r.id
494
        $countQuery = preg_replace('/GROUP\s+BY\s+r\.id\s*$/is', '', $countQuery);
495
496
        // Check if query has DISTINCT in SELECT
497
        $hasDistinct = preg_match('/SELECT\s+DISTINCT/is', $countQuery);
498
499
        // Replace SELECT clause with COUNT
500
        if ($hasDistinct || preg_match('/GROUP\s+BY/is', $countQuery)) {
501
            // For queries with DISTINCT or GROUP BY, count distinct r.id
502
            $countQuery = preg_replace(
503
                '/SELECT\s+.+?\s+FROM/is',
504
                'SELECT COUNT(DISTINCT r.id) as count FROM',
505
                $countQuery
506
            );
507
        } else {
508
            // For simple queries, use COUNT(*)
509
            $countQuery = preg_replace(
510
                '/SELECT\s+.+?\s+FROM/is',
511
                'SELECT COUNT(*) as count FROM',
512
                $countQuery
513
            );
514
        }
515
516
        // Remove LIMIT/OFFSET from the count query
517
        $countQuery = preg_replace('/LIMIT\s+\d+(\s+OFFSET\s+\d+)?$/is', '', $countQuery);
518
519
        try {
520
            // If max results is set and query might return too many results
521
            if ($maxResults > 0) {
522
                // First check if count would exceed max
523
                $testQuery = sprintf('SELECT 1 FROM (%s) as test LIMIT %d',
524
                    preg_replace('/SELECT\s+COUNT.+?\s+FROM/is', 'SELECT 1 FROM', $countQuery),
525
                    $maxResults + 1
526
                );
527
528
                $testResult = DB::select($testQuery);
529
                if (count($testResult) > $maxResults) {
530
                    Cache::put($cacheKey, $maxResults, now()->addMinutes($cacheExpiry));
531
532
                    return $maxResults;
533
                }
534
            }
535
536
            // Execute the count query
537
            $result = DB::select($countQuery);
538
            $count = isset($result[0]) ? (int) $result[0]->count : 0;
539
540
            // Cache the result
541
            Cache::put($cacheKey, $count, now()->addMinutes($cacheExpiry));
542
543
            return $count;
544
        } catch (\Exception $e) {
545
            // If optimization fails, try a simpler approach
546
            try {
547
                // Extract the core table and WHERE conditions
548
                if (preg_match('/FROM\s+releases\s+r\s+(.+?)(?:ORDER\s+BY|LIMIT|$)/is', $query, $matches)) {
549
                    $conditions = $matches[1];
550
                    // Remove JOINs but keep WHERE
551
                    $conditions = preg_replace('/(?:LEFT\s+|INNER\s+)?(?:OUTER\s+)?JOIN\s+.+?(?=WHERE|LEFT|INNER|JOIN|$)/is', '', $conditions);
552
553
                    $fallbackQuery = sprintf('SELECT COUNT(*) as count FROM releases r %s', trim($conditions));
554
555
                    if ($maxResults > 0) {
556
                        $fallbackQuery = sprintf('SELECT COUNT(*) as count FROM (SELECT 1 FROM releases r %s LIMIT %d) as limited',
557
                            trim($conditions),
558
                            $maxResults
559
                        );
560
                    }
561
562
                    $result = DB::select($fallbackQuery);
563
                    $count = isset($result[0]) ? (int) $result[0]->count : 0;
564
565
                    Cache::put($cacheKey, $count, now()->addMinutes($cacheExpiry));
566
567
                    return $count;
568
                }
569
            } catch (\Exception $fallbackException) {
570
                // Log the error for debugging
571
                \Illuminate\Support\Facades\Log::error('getPagerCount failed', [
572
                    'query' => $query,
573
                    'error' => $fallbackException->getMessage(),
574
                ]);
575
            }
576
577
            return 0;
578
        }
579
    }
580
}
581