ReleaseBrowseService::getBrowseRange()   C
last analyzed

Complexity

Conditions 14
Paths 14

Size

Total Lines 87
Code Lines 67

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 67
c 1
b 0
f 0
dl 0
loc 87
rs 5.7333
cc 14
nc 14
nop 10

How to fix   Long Method    Complexity    Many Parameters   

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:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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