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 / StatsDigestDataProvider.php

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

148 lines 5.6 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 * Provides digest-shaped stats rows consumed by email notifications.
9 */
10 class ABJ_404_Solution_StatsDigestDataProvider {
11
12 /** @var ABJ_404_Solution_DatabaseQueryInterface */
13 private $dbCore;
14 /** @var ABJ_404_Solution_LogsRepositoryInterface */
15 private $logsRepo;
16 /** @var ABJ_404_Solution_StatsReadRepository */
17 private $statsReadRepository;
18 /** @var ABJ_404_Solution_Logging */
19 private $logger;
20
21 /**
22 * @param ABJ_404_Solution_DatabaseQueryInterface $dbCore
23 * @param ABJ_404_Solution_LogsRepositoryInterface $logsRepo
24 * @param ABJ_404_Solution_StatsReadRepository $statsReadRepository
25 * @param ABJ_404_Solution_Logging $logging
26 */
27 public function __construct(
28 ABJ_404_Solution_DatabaseQueryInterface $dbCore,
29 ABJ_404_Solution_LogsRepositoryInterface $logsRepo,
30 ABJ_404_Solution_StatsReadRepository $statsReadRepository,
31 $logging
32 ) {
33 $this->dbCore = $dbCore;
34 $this->logsRepo = $logsRepo;
35 $this->statsReadRepository = $statsReadRepository;
36 $this->logger = $logging;
37 }
38
39 /**
40 * @param int $limit
41 * @return array<int, array<string, mixed>>
42 */
43 public function getTopCapturedForDigest(int $limit): array {
44 $limit = max(1, $limit);
45
46 if (!$this->logsRepo->logsHitsTableExists()) {
47 $this->logger->warn('getTopCapturedForDigest: logs_hits rollup unavailable; '
48 . 'digest top-captured table will be empty until rebuild completes. '
49 . 'EmailDigest pre-checks via logsHitsTableExists() to render an "unavailable" message instead.');
50 $this->logsRepo->scheduleHitsTableRebuild();
51 return array();
52 }
53
54 $query = $this->buildTopCapturedForDigestQuery($limit);
55 $result = $this->dbCore->queryAndGetResults($query, array('timeout' => 60));
56
57 if (!empty($result['timed_out']) || (isset($result['last_error']) && $result['last_error'] != '')) {
58 $errRaw = $result['last_error'] ?? '';
59 $errMsg = is_string($errRaw) ? $errRaw : '';
60 $timedOut = !empty($result['timed_out']);
61 $this->logger->warn('getTopCapturedForDigest: query failed against present rollup; '
62 . 'digest top-captured table will be empty. timed_out=' . ($timedOut ? '1' : '0')
63 . ', error=' . ($errMsg !== '' ? $errMsg : '(none)'));
64 return array();
65 }
66
67 $rawRows = is_array($result['rows'] ?? null) ? $result['rows'] : array();
68 $rows = array();
69 foreach ($rawRows as $row) {
70 if (is_array($row)) {
71 $rows[] = $row;
72 }
73 }
74 return $rows;
75 }
76
77 /** @param int $limit @return string */
78 public function buildTopCapturedForDigestQuery(int $limit): string {
79 $limit = max(1, $limit);
80 // Plain equality gives the optimizer an indexable requested_url probe;
81 // the BINARY predicate keeps exact-match URL semantics.
82 $query = "SELECT r.url, COALESCE(h.logshits, 0) AS logshits, r.timestamp AS created
83 FROM {wp_abj404_redirects} r
84 LEFT JOIN {wp_abj404_logs_hits} h
85 ON h.requested_url =
86 COALESCE(r.canonical_url, CONCAT('/', TRIM(BOTH '/' FROM r.url)))
87 AND BINARY h.requested_url = BINARY
88 COALESCE(r.canonical_url, CONCAT('/', TRIM(BOTH '/' FROM r.url)))
89 WHERE r.status = " . ABJ404_STATUS_CAPTURED . " AND r.disabled = 0
90 ORDER BY logshits DESC, r.url ASC
91 LIMIT " . $limit;
92 return $this->dbCore->doTableNameReplacements($query);
93 }
94
95 /**
96 * @param callable|null $countProvider Optional facade count method for subclass compatibility.
97 * @return array{total_captured: int, total_manual: int, total_auto: int}
98 */
99 public function getDigestSummaryStats($countProvider = null): array {
100 $zero = array(
101 'total_captured' => 0,
102 'total_manual' => 0,
103 'total_auto' => 0,
104 );
105
106 $redirectsTable = $this->dbCore->doTableNameReplacements('{wp_abj404_redirects}');
107 $count = is_callable($countProvider)
108 ? $countProvider
109 : array($this->statsReadRepository, 'getStatsCount');
110
111 try {
112 $total_captured = call_user_func(
113 $count,
114 "SELECT COUNT(id) FROM {$redirectsTable} WHERE status = %d AND disabled = 0",
115 array(ABJ404_STATUS_CAPTURED)
116 );
117 $total_manual = call_user_func(
118 $count,
119 "SELECT COUNT(id) FROM {$redirectsTable} WHERE status = %d AND disabled = 0",
120 array(ABJ404_STATUS_MANUAL)
121 );
122 $total_auto = call_user_func(
123 $count,
124 "SELECT COUNT(id) FROM {$redirectsTable} WHERE status = %d AND disabled = 0",
125 array(ABJ404_STATUS_AUTO)
126 );
127 } catch (Throwable $e) {
128 $this->logger->warn(
129 'getRedirectsBreakdownStats failed; returning zero counts: '
130 . $e->getMessage()
131 );
132 return $zero;
133 }
134
135 return array(
136 'total_captured' => intval($total_captured),
137 'total_manual' => intval($total_manual),
138 'total_auto' => intval($total_auto),
139 );
140 }
141
142 /** @return int */
143 public function getCapturedCountForNotification(): int {
144 $viewRead = abj_service('view_read_service');
145 return $viewRead->getRecordCount(array(ABJ404_STATUS_CAPTURED));
146 }
147 }
148