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 / view-build / ViewReadService.php

ViewReadService.php in 404 Solution 4.3.0, at includes/view-build/ViewReadService.php

456 lines 18.3 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 require_once __DIR__ . '/AdminViewReadCoordinator.php';
8
9 /**
10 * Compatibility facade for admin view-read collaborators.
11 *
12 * The class started as a kitchen-sink "ViewReadService" carrying status
13 * counts, bulk redirect reads, logs metrics, DB metadata, and the staged
14 * view-read pipeline itself. Its current responsibility is preserving the
15 * public interface while delegating the actual work to focused collaborators:
16 *
17 * - ABJ_404_Solution_AdminViewReadCoordinator -- row/count reads + snapshots
18 * - ABJ_404_Solution_StatusCountsRepository -- aggregate status tallies
19 * - ABJ_404_Solution_RedirectsBulkReader -- non-paginated redirect reads
20 * - ABJ_404_Solution_LogsMetricsReader -- logs row count + disk usage
21 * - ABJ_404_Solution_DatabaseMetadataReader -- engine + post-type metadata
22 * - ABJ_404_Solution_ViewQueryBuilder -- single-table SQL construction
23 * - ABJ_404_Solution_ViewCacheInvalidator -- invalidation primitives
24 * - ABJ_404_Solution_ViewDiagnostics -- failure diagnostics
25 *
26 * The slim-facade pattern matches NGramFilter (i804): the interface stays
27 * intact so existing callers and ~80 test stubs keep working, while the
28 * actual work happens in single-responsibility collaborators that the rest
29 * of the codebase can also wire directly.
30 *
31 * @see docs/dataaccess-refactor-plan.md Phase 6.
32 */
33 class ABJ_404_Solution_ViewReadService implements ABJ_404_Solution_ViewReadServiceInterface {
34
35 const CACHE_KEY_REDIRECT_STATUS = ABJ_404_Solution_ViewReadRuntimeState::CACHE_KEY_REDIRECT_STATUS;
36 const CACHE_KEY_CAPTURED_STATUS = ABJ_404_Solution_ViewReadRuntimeState::CACHE_KEY_CAPTURED_STATUS;
37 const CACHE_KEY_HIGH_IMPACT_CAPTURED = ABJ_404_Solution_ViewReadRuntimeState::CACHE_KEY_HIGH_IMPACT_CAPTURED;
38 const STATUS_CACHE_TTL = ABJ_404_Solution_ViewReadRuntimeState::STATUS_CACHE_TTL;
39 const STATUS_CACHE_TIMEOUT_SELFHEAL_TTL = ABJ_404_Solution_ViewReadRuntimeState::STATUS_CACHE_TIMEOUT_SELFHEAL_TTL;
40 const LOGS_COUNT_CACHE_TTL_SECONDS = ABJ_404_Solution_LogsMetricsReader::LOGS_COUNT_CACHE_TTL_SECONDS;
41
42 /** @var bool Per-request "bulk mutation in progress" flag. */
43 public static $bulkMutationInProgress = false;
44
45 /** @var ABJ_404_Solution_DatabaseCore */
46 private $dbCore;
47
48 /** @var ABJ_404_Solution_Functions */
49 private $f;
50
51 /** @var ABJ_404_Solution_Logging */
52 private $logger;
53
54 // --- Collaborators ---
55
56 /** @var ABJ_404_Solution_ViewQueryBuilder */
57 private $queryBuilder;
58
59 /** @var ABJ_404_Solution_ViewDiagnostics */
60 private $diagnostics;
61
62 /** @var ABJ_404_Solution_ViewCacheInvalidator */
63 private $cacheInvalidator;
64
65 /** @var ABJ_404_Solution_StatusCountsRepository */
66 private $statusCounts;
67
68 /** @var ABJ_404_Solution_RedirectsBulkReader */
69 private $redirectsBulkReader;
70
71 /** @var ABJ_404_Solution_LogsMetricsReader */
72 private $logsMetricsReader;
73
74 /** @var ABJ_404_Solution_DatabaseMetadataReader */
75 private $dbMetadataReader;
76
77 /** @var ABJ_404_Solution_HitsTableRebuildPolicy */
78 private $hitsTableRebuildPolicy;
79
80 /** @var ABJ_404_Solution_AdminViewReadCoordinator */
81 private $adminViewReadCoordinator;
82
83 /** @var ABJ_404_Solution_RedirectsViewLiveResolver */
84 private $liveResolver;
85
86 /**
87 * @param ABJ_404_Solution_DatabaseCore $dbCore
88 * @param ABJ_404_Solution_LogsRepository $logsRepo
89 * @param ABJ_404_Solution_RedirectsRepository $redirectsRepo
90 * @param ABJ_404_Solution_Functions|null $f Falls back to abj_service('functions')
91 * @param ABJ_404_Solution_Logging|null $logger Falls back to abj_service('logging')
92 */
93 public function __construct(
94 ABJ_404_Solution_DatabaseCore $dbCore,
95 ABJ_404_Solution_LogsRepository $logsRepo,
96 ABJ_404_Solution_RedirectsRepository $redirectsRepo,
97 $f = null,
98 $logger = null
99 ) {
100 $this->dbCore = $dbCore;
101 $this->f = $f !== null ? $f : abj_service('functions');
102 $this->logger = $logger !== null ? $logger : abj_service('logging');
103
104 $this->diagnostics = new ABJ_404_Solution_ViewDiagnostics($dbCore);
105 $this->cacheInvalidator = new ABJ_404_Solution_ViewCacheInvalidator(
106 $dbCore, $redirectsRepo, $this->viewDoneFreshnessOptionName()
107 );
108 $this->queryBuilder = new ABJ_404_Solution_ViewQueryBuilder($dbCore);
109 $this->liveResolver = new ABJ_404_Solution_RedirectsViewLiveResolver($dbCore, $this->f);
110
111 $this->statusCounts = new ABJ_404_Solution_StatusCountsRepository($dbCore, $logsRepo, $this->queryBuilder);
112 $this->redirectsBulkReader = new ABJ_404_Solution_RedirectsBulkReader($dbCore, $this->queryBuilder, $this->f);
113 $this->logsMetricsReader = new ABJ_404_Solution_LogsMetricsReader($dbCore, $logsRepo, $this->f, $this->logger);
114 $this->dbMetadataReader = new ABJ_404_Solution_DatabaseMetadataReader($dbCore);
115 $this->hitsTableRebuildPolicy = new ABJ_404_Solution_HitsTableRebuildPolicy($dbCore, $logsRepo, $this->logger);
116 $this->adminViewReadCoordinator = new ABJ_404_Solution_AdminViewReadCoordinator(
117 $dbCore,
118 $this->queryBuilder,
119 $this->diagnostics,
120 $this->cacheInvalidator,
121 $this->liveResolver,
122 $this->logger
123 );
124 }
125
126 /** @return string */
127 private function viewDoneFreshnessOptionName(): string {
128 return $this->dbCore->tableNameResolver()->getLowercasePrefix() . 'abj404_view_done_built_at';
129 }
130
131 // =========================================================================
132 // Delegated: AdminViewReadCoordinator
133 // =========================================================================
134
135 /**
136 * @param string $sub
137 * @param array<string, mixed> $tableOptions
138 * @return array<int|string, mixed>
139 */
140 function getRedirectsForView($sub, $tableOptions) {
141 return $this->adminViewReadCoordinator->getRedirectsForView($sub, $tableOptions);
142 }
143
144 /**
145 * Map a UI orderby alias to the narrow sort-key column that backs it, or ''
146 * for a sort that is not sort-key-backed (it orders by a real always-present
147 * column and is therefore always available).
148 *
149 * @var array<string, string>
150 */
151 const ORDERBY_TO_SORT_KEY = array(
152 'url' => 'url_sort_key',
153 'dest' => 'dest_sort_key',
154 'final_dest' => 'dest_sort_key',
155 );
156
157 /** @var int|null Per-request memo of MAX(id) for the progress denominator. */
158 private $sortKeyMaxIdMemo = null;
159
160 /**
161 * Whether ordering the admin list by $orderby can be served index-ordered
162 * right now. For a narrow-sort-key-backed column (URL, Destination) that
163 * means the column exists AND its one-time legacy-row drain has converged
164 * (the latch is set) -- the SAME condition the read path uses before ordering
165 * by the key (see ViewQueryBuilder::wideColumnSortPendingBackfill /
166 * AdminViewReadCoordinator). Sorts on real always-populated columns
167 * (logshits, last_used, score, timestamp, ...) are always ready.
168 *
169 * The admin header uses this to disable the URL / Destination sort links on
170 * the captured tab during the post-upgrade window, where ordering by those
171 * columns would otherwise filesort the captured majority over the wide
172 * varchar(2048) source column and risk the host's max_statement_time.
173 *
174 * @param string $orderby UI orderby alias (url, final_dest, logshits, ...).
175 * @return bool
176 */
177 public function isSortReadyForOrderby(string $orderby): bool {
178 return $this->sortReadinessStatusForOrderby($orderby)
179 === ABJ_404_Solution_ViewReadServiceInterface::SORT_READINESS_READY;
180 }
181
182 /**
183 * @param string $orderby UI orderby alias (url, final_dest, logshits, ...).
184 * @return string One of ABJ_404_Solution_ViewReadServiceInterface::SORT_READINESS_*.
185 */
186 public function sortReadinessStatusForOrderby(string $orderby): string {
187 $column = self::ORDERBY_TO_SORT_KEY[strtolower($orderby)] ?? '';
188 if ($column === '') {
189 return ABJ_404_Solution_ViewReadServiceInterface::SORT_READINESS_READY;
190 }
191
192 $readiness = $this->liveResolver->schemaReadiness();
193 if (!$readiness->sortKeySchemaAvailableForColumn($column)) {
194 return ABJ_404_Solution_ViewReadServiceInterface::SORT_READINESS_SCHEMA_UNAVAILABLE;
195 }
196 if ($readiness->sortKeyReadyForColumn($column)) {
197 return ABJ_404_Solution_ViewReadServiceInterface::SORT_READINESS_READY;
198 }
199 return ABJ_404_Solution_ViewReadServiceInterface::SORT_READINESS_BACKFILL_PENDING;
200 }
201
202 /**
203 * Backfill progress for $orderby's narrow sort key as a 0..100 integer, for
204 * the admin "building the index" tooltip. 100 once ready (or when the sort is
205 * not sort-key-backed). Otherwise derived from the drain cursor (highest
206 * redirect id drained) over MAX(id): a wp_options read plus an O(1)
207 * primary-key probe, NEVER a COUNT over the captured rows. This is a
208 * high-water estimate over the id space, not an exact row-count fraction, so
209 * sparse ids are acceptable. Capped at 99 until the latch flips so the
210 * tooltip never claims 100% before the sort is actually available. A wrapped
211 * cursor of 0 is shown as 0% until the latch flips or the next drain advances
212 * it again.
213 *
214 * @param string $orderby UI orderby alias.
215 * @return int
216 */
217 public function sortBackfillPercentForOrderby(string $orderby): int {
218 if ($this->isSortReadyForOrderby($orderby)) {
219 return 100;
220 }
221 $column = self::ORDERBY_TO_SORT_KEY[strtolower($orderby)] ?? '';
222 if ($column === '' || !function_exists('get_option')) {
223 return 0;
224 }
225 $cursorOption = ABJ_404_Solution_RedirectsDenormColumnSql::sortKeyBackfillCursorOption($column);
226 $cursorRaw = get_option($cursorOption);
227 $cursor = is_numeric($cursorRaw) ? (int) $cursorRaw : 0;
228 $maxId = $this->sortKeyMaxId();
229 if ($maxId <= 0 || $cursor <= 0) {
230 return 0;
231 }
232 return max(1, min(99, (int) floor(100 * $cursor / $maxId)));
233 }
234
235 /** @return int MAX(id) on the redirects table (O(1) PK probe), memoized per request. */
236 private function sortKeyMaxId(): int {
237 if ($this->sortKeyMaxIdMemo !== null) {
238 return $this->sortKeyMaxIdMemo;
239 }
240 $table = $this->dbCore->doTableNameReplacements('{wp_abj404_redirects}');
241 $result = $this->dbCore->queryAndGetResults('SELECT MAX(id) AS max_id FROM ' . $table);
242 $rows = is_array($result['rows'] ?? null) ? $result['rows'] : array();
243 $firstRow = is_array($rows[0] ?? null) ? $rows[0] : array();
244 $raw = $firstRow['max_id'] ?? 0;
245 $this->sortKeyMaxIdMemo = is_numeric($raw) ? max(0, (int) $raw) : 0;
246 return $this->sortKeyMaxIdMemo;
247 }
248
249 /**
250 * Whether the most recent getRedirectsForView() result is NOT a trustworthy
251 * "genuinely empty" listing (pending build, errored read, or an empty
252 * snapshot contradicting the live source count). Drives the renderer's
253 * "still preparing" state and the AJAX view-build poller re-engagement.
254 *
255 * @return bool
256 */
257 function lastRedirectsViewReadWasIncomplete(): bool {
258 return $this->adminViewReadCoordinator->lastRedirectsViewReadWasIncomplete();
259 }
260
261 /**
262 * @param string $sub
263 * @param array<string, mixed> $tableOptions
264 * @return int
265 */
266 function getRedirectsForViewCount(string $sub, array $tableOptions): int {
267 return $this->adminViewReadCoordinator->getRedirectsForViewCount($sub, $tableOptions);
268 }
269
270 // =========================================================================
271 // Delegated: HitsTableRebuildPolicy
272 // =========================================================================
273
274 /** @return void */
275 function maybeUpdateRedirectsForViewHitsTable(): void {
276 $this->hitsTableRebuildPolicy->maybeUpdateRedirectsForViewHitsTable();
277 }
278
279 // =========================================================================
280 // Delegated: StatusCountsRepository
281 // =========================================================================
282
283 /** @param bool $bypassCache @return array<string, int> */
284 function getRedirectStatusCounts($bypassCache = false): array {
285 return $this->statusCounts->getRedirectStatusCounts((bool)$bypassCache);
286 }
287
288 /** @param bool $bypassCache @return array<string, int> */
289 function getCapturedStatusCounts($bypassCache = false): array {
290 return $this->statusCounts->getCapturedStatusCounts((bool)$bypassCache);
291 }
292
293 /** @return int */
294 function getHighImpactCapturedCount(): int {
295 return $this->statusCounts->getHighImpactCapturedCount();
296 }
297
298 /** @return int */
299 function getCapturedCount() {
300 return $this->statusCounts->getCapturedCount();
301 }
302
303 /**
304 * @param array<int, int> $types
305 * @param int $trashed
306 * @return int
307 */
308 function getRecordCount($types = array(), $trashed = 0) {
309 return $this->statusCounts->getRecordCount(is_array($types) ? $types : array(), $trashed);
310 }
311
312 // =========================================================================
313 // Delegated: RedirectsBulkReader
314 // =========================================================================
315
316 /** @return array<int, array<string, mixed>> */
317 function getRedirectsAll() {
318 return $this->redirectsBulkReader->getRedirectsAll();
319 }
320
321 /** @param string $tempFile @return void */
322 function doRedirectsExport(string $tempFile): void {
323 $this->redirectsBulkReader->doRedirectsExport($tempFile);
324 }
325
326 /** @return array<int, array<string, mixed>> */
327 function getRedirectsWithLogs() {
328 return $this->redirectsBulkReader->getRedirectsWithLogs();
329 }
330
331 /** @return array<int, array<string, mixed>> */
332 function getRedirectsWithRegEx() {
333 return $this->redirectsBulkReader->getRedirectsWithRegEx();
334 }
335
336 /** @return array<int, array<string, mixed>> */
337 function getManualRedirectsWithRegexMetachars() {
338 return $this->redirectsBulkReader->getManualRedirectsWithRegexMetachars();
339 }
340
341 /** @param array<int, string> $postIDs @return array<int, mixed> */
342 function getExtraDataToPermalinkSuggestions(array $postIDs): array {
343 return $this->redirectsBulkReader->getExtraDataToPermalinkSuggestions($postIDs);
344 }
345
346 // =========================================================================
347 // Delegated: LogsMetricsReader
348 // =========================================================================
349
350 /** @param int $logID @return int */
351 function getLogsCount($logID) {
352 return $this->logsMetricsReader->getLogsCount($logID);
353 }
354
355 /** @return int */
356 function getLogDiskUsage() {
357 return $this->logsMetricsReader->getLogDiskUsage();
358 }
359
360 // =========================================================================
361 // Delegated: DatabaseMetadataReader
362 // =========================================================================
363
364 /** @return array<string, mixed> */
365 function getTableEngines() {
366 return $this->dbMetadataReader->getTableEngines();
367 }
368
369 /** @return bool */
370 function isMyISAMSupported(): bool {
371 return $this->dbMetadataReader->isMyISAMSupported();
372 }
373
374 /** @return array<int, string> */
375 function getAllPostTypes() {
376 return $this->dbMetadataReader->getAllPostTypes();
377 }
378
379 // =========================================================================
380 // Delegated: ViewQueryBuilder
381 // =========================================================================
382
383 /** @return string */
384 function buildHighImpactCapturedCountQuery(): string {
385 return $this->queryBuilder->buildHighImpactCapturedCountQuery();
386 }
387
388 /**
389 * Single-table redirects read for one page (Denorm Step 3b): fetch the
390 * ordered/filtered page off wp_abj404_redirects, then resolve the visible
391 * rows' derived/display values live and write the four denorm columns back.
392 * This is the live read path that replaces the staged view_done read.
393 *
394 * @param string $sub
395 * @param array<string, mixed> $tableOptions
396 * @return array<int, array<string, mixed>>
397 */
398 public function readRedirectsSingleTable(string $sub, array $tableOptions): array {
399 return $this->adminViewReadCoordinator->readRedirectsSingleTable($sub, $tableOptions);
400 }
401
402 /**
403 * Single-table filtered count against wp_abj404_redirects (Denorm Step 3b).
404 *
405 * @param string $sub
406 * @param array<string, mixed> $tableOptions
407 * @return int
408 */
409 public function countRedirectsSingleTable(string $sub, array $tableOptions): int {
410 return $this->adminViewReadCoordinator->countRedirectsSingleTable($sub, $tableOptions);
411 }
412
413 // =========================================================================
414 // Delegated: ViewCacheInvalidator
415 // =========================================================================
416
417 /**
418 * @template T
419 * @param callable():T $work
420 * @return T
421 */
422 public function runWithDeferredInvalidation(callable $work) {
423 return $this->cacheInvalidator->runWithDeferredInvalidation($work);
424 }
425
426 /** @return void */
427 function invalidateStatusCountsCache(): void {
428 $this->cacheInvalidator->invalidateStatusCountsCache();
429 }
430
431 /** @return void */
432 function invalidateViewSnapshotCache(): void {
433 $this->cacheInvalidator->invalidateViewSnapshotCache();
434 }
435
436 /** @return void */
437 function clearRegexRedirectsCache(): void {
438 $this->cacheInvalidator->clearRegexRedirectsCache();
439 }
440
441 // =========================================================================
442 // Delegated: ViewDiagnostics
443 // =========================================================================
444
445 /**
446 * @param string $sub
447 * @param string $failedQuery
448 * @param array<string, mixed> $tableOptions
449 * @param array<string, mixed> $queryResult
450 * @return array<string, mixed>
451 */
452 public function captureViewQueryFailureDiagnostics(string $sub, string $failedQuery, array $tableOptions, array $queryResult): array {
453 return $this->diagnostics->captureViewQueryFailureDiagnostics($sub, $failedQuery, $tableOptions, $queryResult);
454 }
455 }
456