PluginProbe
404 Solution / 4.3.0
404 Solution v4.3.0
4.3.5 4.3.4 4.3.3 4.3.2 4.3.1 4.3.0 4.2.0 4.1.19 4.1.18 4.1.17 4.1.16 4.1.15 4.1.13 4.1.12 4.1.11 4.1.10 4.1.9 4.1.8 4.1.7 4.1.6 4.1.5 4.1.4 4.1.3 trunk 2.30.0 All 109 releases
404-solution / includes / stats / StatusCountsRepository.php

StatusCountsRepository.php in 404 Solution 4.3.0, at includes/stats/StatusCountsRepository.php

256 lines 10.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if (!defined('ABSPATH')) {
4 exit;
5 }
6
7 /**
8 * Aggregate "redirects grouped by status" tallies with transient caching.
9 *
10 * Owns the SUM(CASE WHEN ...) aggregation queries that previously lived
11 * inline in ViewReadService:
12 * - Active/manual/auto/regex/trash counts (admin redirects list badges)
13 * - Captured/ignored/later/trash counts (admin captures list badges)
14 * - High-impact captured count (logs-joined; gated by hits table presence)
15 * - The simple total counters (`getCapturedCount`, `getRecordCount`)
16 *
17 * Extracted in the i805 ViewReadService decomposition. The cache TTLs and
18 * key names are reused verbatim from ViewReadRuntimeState so existing
19 * invalidation paths continue to delete the same keys.
20 */
21 class ABJ_404_Solution_StatusCountsRepository {
22
23 const CACHE_KEY_REDIRECT_STATUS = ABJ_404_Solution_ViewReadRuntimeState::CACHE_KEY_REDIRECT_STATUS;
24 const CACHE_KEY_CAPTURED_STATUS = ABJ_404_Solution_ViewReadRuntimeState::CACHE_KEY_CAPTURED_STATUS;
25 const CACHE_KEY_HIGH_IMPACT_CAPTURED = ABJ_404_Solution_ViewReadRuntimeState::CACHE_KEY_HIGH_IMPACT_CAPTURED;
26 const STATUS_CACHE_TTL = ABJ_404_Solution_ViewReadRuntimeState::STATUS_CACHE_TTL;
27 const STATUS_CACHE_TIMEOUT_SELFHEAL_TTL = ABJ_404_Solution_ViewReadRuntimeState::STATUS_CACHE_TIMEOUT_SELFHEAL_TTL;
28
29 /** @var ABJ_404_Solution_DatabaseQueryInterface */
30 private $dbCore;
31
32 /** @var ABJ_404_Solution_LogsRepository */
33 private $logsRepo;
34
35 /** @var ABJ_404_Solution_ViewQueryBuilder */
36 private $queryBuilder;
37
38 /**
39 * @param ABJ_404_Solution_DatabaseQueryInterface $dbCore
40 * @param ABJ_404_Solution_LogsRepository $logsRepo
41 * @param ABJ_404_Solution_ViewQueryBuilder $queryBuilder
42 */
43 public function __construct(
44 ABJ_404_Solution_DatabaseQueryInterface $dbCore,
45 ABJ_404_Solution_LogsRepository $logsRepo,
46 ABJ_404_Solution_ViewQueryBuilder $queryBuilder
47 ) {
48 $this->dbCore = $dbCore;
49 $this->logsRepo = $logsRepo;
50 $this->queryBuilder = $queryBuilder;
51 }
52
53 /**
54 * @param bool $bypassCache
55 * @return array<string, int>
56 */
57 public function getRedirectStatusCounts(bool $bypassCache = false): array {
58 if (!$bypassCache) {
59 $cached = get_transient(self::CACHE_KEY_REDIRECT_STATUS);
60 if ($cached !== false && is_array($cached)) {
61 /** @var array<string, int> $cached */
62 return $cached;
63 }
64 }
65
66 $query = "SELECT
67 SUM(CASE WHEN disabled = 0 THEN 1 ELSE 0 END) as active_count,
68 SUM(CASE WHEN disabled = 0 AND status = " . ABJ404_STATUS_MANUAL . " THEN 1 ELSE 0 END) as manual_count,
69 SUM(CASE WHEN disabled = 0 AND status = " . ABJ404_STATUS_AUTO . " THEN 1 ELSE 0 END) as auto_count,
70 SUM(CASE WHEN disabled = 0 AND status = " . ABJ404_STATUS_REGEX . " THEN 1 ELSE 0 END) as regex_count,
71 SUM(CASE WHEN disabled = 1 THEN 1 ELSE 0 END) as trash_count
72 FROM {wp_abj404_redirects}
73 WHERE status IN (" . ABJ404_STATUS_MANUAL . ", " . ABJ404_STATUS_AUTO . ", " . ABJ404_STATUS_REGEX . ")";
74 $query = $this->dbCore->doTableNameReplacements($query);
75
76 $result = $this->dbCore->queryAndGetResults($query);
77 $hadError = !empty($result['last_error']) || !empty($result['timed_out']);
78 $rows = is_array($result['rows']) ? $result['rows'] : array();
79
80 $counts = array('all' => 0, 'manual' => 0, 'auto' => 0, 'regex' => 0, 'trash' => 0);
81 if (!empty($rows)) {
82 $row = is_array($rows[0] ?? null) ? $rows[0] : array();
83 $counts = array(
84 'all' => self::scalarToInt($row['active_count'] ?? 0),
85 'manual' => self::scalarToInt($row['manual_count'] ?? 0),
86 'auto' => self::scalarToInt($row['auto_count'] ?? 0),
87 'regex' => self::scalarToInt($row['regex_count'] ?? 0),
88 'trash' => self::scalarToInt($row['trash_count'] ?? 0)
89 );
90 }
91
92 if (!$hadError && !$bypassCache) {
93 set_transient(self::CACHE_KEY_REDIRECT_STATUS, $counts, self::STATUS_CACHE_TTL);
94 }
95
96 return $counts;
97 }
98
99 /**
100 * @param bool $bypassCache
101 * @return array<string, int>
102 */
103 public function getCapturedStatusCounts(bool $bypassCache = false): array {
104 if (!$bypassCache) {
105 $cached = get_transient(self::CACHE_KEY_CAPTURED_STATUS);
106 if ($cached !== false && is_array($cached)) {
107 /** @var array<string, int> $cached */
108 return $cached;
109 }
110 }
111
112 $query = "SELECT
113 COUNT(*) as total,
114 SUM(CASE WHEN disabled = 0 THEN 1 ELSE 0 END) as active,
115 SUM(CASE WHEN disabled = 0 AND status = " . ABJ404_STATUS_CAPTURED . " THEN 1 ELSE 0 END) as captured,
116 SUM(CASE WHEN disabled = 0 AND status = " . ABJ404_STATUS_IGNORED . " THEN 1 ELSE 0 END) as ignored,
117 SUM(CASE WHEN disabled = 0 AND status = " . ABJ404_STATUS_LATER . " THEN 1 ELSE 0 END) as later,
118 SUM(CASE WHEN disabled = 1 THEN 1 ELSE 0 END) as trash
119 FROM {wp_abj404_redirects}
120 WHERE status IN (" . ABJ404_STATUS_CAPTURED . ", " . ABJ404_STATUS_IGNORED . ", " . ABJ404_STATUS_LATER . ")";
121 $query = $this->dbCore->doTableNameReplacements($query);
122
123 $result = $this->dbCore->queryAndGetResults($query);
124 $hadError = !empty($result['last_error']) || !empty($result['timed_out']);
125 $rows = is_array($result['rows']) ? $result['rows'] : array();
126
127 $counts = array('all' => 0, 'captured' => 0, 'ignored' => 0, 'later' => 0, 'trash' => 0);
128 if (!empty($rows)) {
129 $row = is_array($rows[0] ?? null) ? $rows[0] : array();
130 $counts = array(
131 'all' => self::scalarToInt($row['active'] ?? 0),
132 'captured' => self::scalarToInt($row['captured'] ?? 0),
133 'ignored' => self::scalarToInt($row['ignored'] ?? 0),
134 'later' => self::scalarToInt($row['later'] ?? 0),
135 'trash' => self::scalarToInt($row['trash'] ?? 0)
136 );
137 }
138
139 if (!$hadError && !$bypassCache) {
140 set_transient(self::CACHE_KEY_CAPTURED_STATUS, $counts, self::STATUS_CACHE_TTL);
141 }
142
143 return $counts;
144 }
145
146 /**
147 * @return int Cached count; on timeout returns 0 and schedules a hits-table rebuild
148 * (the cached 0 is overwritten with the real value once rebuild completes).
149 */
150 public function getHighImpactCapturedCount(): int {
151 $cached = get_transient(self::CACHE_KEY_HIGH_IMPACT_CAPTURED);
152 if ($cached !== false) {
153 return intval(is_scalar($cached) ? $cached : 0);
154 }
155
156 if (!$this->logsRepo->logsHitsTableExists()) {
157 $this->logsRepo->scheduleHitsTableRebuild();
158 return 0;
159 }
160
161 $query = $this->queryBuilder->buildHighImpactCapturedCountQuery();
162
163 $result = $this->dbCore->queryAndGetResults($query, array('timeout' => 60));
164 $timedOut = !empty($result['timed_out']);
165 $hadError = !empty($result['last_error']) || $timedOut;
166 $rows = is_array($result['rows']) ? $result['rows'] : array();
167 $firstRow = (!empty($rows) && is_array($rows[0] ?? null)) ? $rows[0] : array();
168 $count = self::scalarToInt($firstRow['cnt'] ?? 0);
169
170 if ($timedOut) {
171 $this->logsRepo->scheduleHitsTableRebuild();
172 // allow-cache-empty: timeout self-heal sentinel, 5-minute window. Real value returns once the rebuild completes and the short cache expires.
173 set_transient(self::CACHE_KEY_HIGH_IMPACT_CAPTURED, 0, self::STATUS_CACHE_TIMEOUT_SELFHEAL_TTL);
174 return 0;
175 }
176
177 if ($hadError) {
178 return 0;
179 }
180
181 if ($count === 0 && $this->isHitsTableEmpty()) {
182 $this->logsRepo->scheduleHitsTableRebuild();
183 return 0;
184 }
185
186 set_transient(self::CACHE_KEY_HIGH_IMPACT_CAPTURED, $count, self::STATUS_CACHE_TTL);
187
188 return $count;
189 }
190
191 /** @return int */
192 public function getCapturedCount(): int {
193 $query = "select count(id) from {wp_abj404_redirects} where status = " . absint(ABJ404_STATUS_CAPTURED);
194 $result = $this->dbCore->queryAndGetResults($query);
195 if (!empty($result['timed_out']) || (isset($result['last_error']) && $result['last_error'] != '')) {
196 return 0;
197 }
198 $rows = is_array($result['rows'] ?? null) ? $result['rows'] : array();
199 if (empty($rows)) {
200 return 0;
201 }
202 $first = $rows[0];
203 $value = is_array($first) ? reset($first) : $first;
204 return self::scalarToInt($value);
205 }
206
207 /**
208 * @param array<int, int> $types Status codes to include (passed through absint).
209 * @param int $trashed 0 for live rows, 1 for trashed rows.
210 * @return int
211 */
212 public function getRecordCount(array $types = array(), $trashed = 0): int {
213 if (count($types) < 1) {
214 return 0;
215 }
216 $filteredTypes = array_map('absint', $types);
217 $typesForSQL = implode(', ', $filteredTypes);
218 $query = "select count(id) as count from {wp_abj404_redirects} where 1 and (status in ("
219 . $typesForSQL . "))"
220 . " and disabled = " . absint($trashed);
221
222 $result = $this->dbCore->queryAndGetResults($query);
223 $rows = is_array($result['rows']) ? $result['rows'] : array();
224 if (empty($rows)) {
225 return 0;
226 }
227 $row = is_array($rows[0] ?? null) ? $rows[0] : array();
228 return isset($row['count']) && is_scalar($row['count']) ? intval($row['count']) : 0;
229 }
230
231 /**
232 * Probe the hits table for "is this rollup empty?" to distinguish a real
233 * zero from a not-yet-rebuilt state.
234 *
235 * @return bool
236 */
237 private function isHitsTableEmpty(): bool {
238 $check = "SELECT 1 FROM {wp_abj404_logs_hits} LIMIT 1";
239 $check = $this->dbCore->doTableNameReplacements($check);
240 $result = $this->dbCore->queryAndGetResults($check);
241 if (!empty($result['last_error']) || !empty($result['timed_out'])) {
242 return false;
243 }
244 $rows = is_array($result['rows'] ?? null) ? $result['rows'] : array();
245 return empty($rows);
246 }
247
248 /**
249 * @param mixed $value
250 * @return int
251 */
252 private static function scalarToInt($value): int {
253 return is_scalar($value) ? intval($value) : 0;
254 }
255 }
256