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 / database / DatabaseCore.php

DatabaseCore.php in 404 Solution 4.3.0, at includes/database/DatabaseCore.php

325 lines 13.5 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__ . '/DatabaseCoreInterface.php';
8 require_once __DIR__ . '/DatabaseQueryInterface.php';
9 require_once __DIR__ . '/DatabaseRuntimeState.php';
10 require_once __DIR__ . '/DatabaseConnectionManager.php';
11 require_once __DIR__ . '/DatabaseQueryTimeoutManager.php';
12 require_once __DIR__ . '/DatabaseInfrastructureErrorTaxonomy.php';
13 require_once __DIR__ . '/DatabaseStagedFailureClassifier.php';
14 require_once __DIR__ . '/DatabaseErrorTableInspector.php';
15 require_once __DIR__ . '/DatabasePrefixDiagnostics.php';
16 require_once __DIR__ . '/DatabaseErrorClassifier.php';
17 require_once __DIR__ . '/DatabaseRepairPolicy.php';
18 require_once __DIR__ . '/DatabaseSqlErrorReporter.php';
19 require_once __DIR__ . '/DatabaseTableNameResolver.php';
20 require_once __DIR__ . '/DatabaseNoticeStateHolder.php';
21 require_once __DIR__ . '/DatabaseCollationHelper.php';
22 require_once __DIR__ . '/DatabaseTableRepairer.php';
23 require_once __DIR__ . '/DatabaseWpdbResultHarvester.php';
24 require_once __DIR__ . '/DatabaseQueryDiagnostics.php';
25 require_once __DIR__ . '/DatabaseTransactionExecutor.php';
26 require_once __DIR__ . '/DatabaseQueryRecoveryPolicy.php';
27 require_once __DIR__ . '/DatabaseQueryExecutor.php';
28 require_once __DIR__ . '/DatabaseRecoveryServices.php';
29 require_once __DIR__ . '/DatabaseQueryServices.php';
30
31 /**
32 * Shared database infrastructure: query execution, error recovery, timeouts,
33 * connection management, table-name resolution, and error classification.
34 *
35 * Composition root for the database infrastructure components. Two cohesive
36 * sub-composition-roots own the bulk of the collaborator graph:
37 *
38 * - DatabaseRecoveryServices: error classifier, repair policy, sql error
39 * reporter, collation helper, table repairer (the "what to do when a
40 * query fails" cluster).
41 * - DatabaseQueryServices: query executor, query timeout manager, query
42 * recovery policy, result harvester, query diagnostics, transaction
43 * executor (the "run a SQL query and surface its result" cluster).
44 *
45 * DatabaseCore retains direct ownership of the three infrastructure
46 * collaborators that don't fit either cluster: connection manager (the
47 * dbh lifecycle), table name resolver (DDL/prefix queries), and notice
48 * state holder (admin-notice + runtime-flag bookkeeping).
49 *
50 * Public surface:
51 * - Query-interface methods (DatabaseQueryInterface) for callers that need
52 * the centralized query pipeline. Each is a one-line delegate to the
53 * relevant component.
54 * - Core-interface methods (DatabaseCoreInterface) for callers that need
55 * database component accessors.
56 * - Component accessor methods (connectionManager(), errorClassifier(),
57 * etc.) for DAO-internal callers that need non-interface behavior.
58 * - Lazy clock() resolver and the two static SET STATEMENT wrapper
59 * helpers that own per-request state.
60 *
61 * There is no __call() dispatch and no non-interface delegate surface:
62 * every component method is reached through its component accessor
63 * (e.g. errorClassifier()->taxonomy()->connectivity()->isTransientConnectionError(), not
64 * DatabaseCore::isTransientConnectionError()). The previous explicit
65 * delegate section was removed in i812; see design-audit-2026-06-02.md
66 * M202.
67 */
68 class ABJ_404_Solution_DatabaseCore implements
69 ABJ_404_Solution_DatabaseCoreInterface,
70 ABJ_404_Solution_DatabaseQueryInterface {
71
72 /** @var int Cooldown when DB query quota is exceeded. */
73 const DB_QUOTA_COOLDOWN_SECONDS = 900;
74 /** @var int Cooldown when DB is read-only or storage is full. */
75 const DB_WRITE_BLOCK_COOLDOWN_SECONDS = 900;
76
77 /** @var ABJ_404_Solution_Functions */
78 private $f;
79
80 /** @var ABJ_404_Solution_Logging */
81 private $logger;
82
83 /** @var ABJ_404_Solution_Clock|null */
84 private $clock = null;
85
86 /** @var ABJ_404_Solution_DatabaseConnectionManager */
87 private $connectionManager;
88
89 /** @var ABJ_404_Solution_DatabaseTableNameResolver */
90 private $tableNameResolver;
91
92 /** @var ABJ_404_Solution_DatabaseNoticeStateHolder */
93 private $noticeState;
94
95 /** @var ABJ_404_Solution_DatabaseRecoveryServices */
96 private $recoveryServices;
97
98 /** @var ABJ_404_Solution_DatabaseQueryServices */
99 private $queryServices;
100
101 /**
102 * @var bool Per-request cache: this server rejected the
103 * `SET STATEMENT max_statement_time=N FOR ...` timeout wrapper, so
104 * applyQueryTimeout() must skip wrapping for the rest of the request.
105 */
106 private static $setStatementWrapperUnsupported = false;
107
108 /**
109 * @param ABJ_404_Solution_Functions|null $functions
110 * @param ABJ_404_Solution_Logging|null $logging
111 */
112 public function __construct($functions = null, $logging = null) {
113 $this->f = $functions !== null ? $functions : abj_service('functions');
114 $this->logger = $logging !== null ? $logging : abj_service('logging');
115 $this->connectionManager = new ABJ_404_Solution_DatabaseConnectionManager($this, $this->logger);
116 $this->queryServices = new ABJ_404_Solution_DatabaseQueryServices($this, $this->logger);
117 $this->tableNameResolver = new ABJ_404_Solution_DatabaseTableNameResolver(
118 $this->f,
119 function (string $query, array $options): array {
120 return $this->queryAndGetResults($query, $options);
121 }
122 );
123 $this->noticeState = new ABJ_404_Solution_DatabaseNoticeStateHolder(
124 function (): bool {
125 // Deferred lookup: recoveryServices is assigned below.
126 return $this->recoveryServices->errorClassifier()->isQuotaCooldownActive();
127 }
128 );
129 $this->recoveryServices = new ABJ_404_Solution_DatabaseRecoveryServices(
130 $this,
131 $this->f,
132 $this->logger,
133 $this->queryServices->resultHarvester(),
134 $this->noticeState,
135 $this->queryServices->queryExecutor()
136 );
137 }
138
139 // =========================================================================
140 // Public accessors for the focused component classes. DAOs and other
141 // infrastructure-layer callers depend directly on the component they need
142 // and call methods on it. DatabaseCore itself satisfies
143 // DatabaseCoreInterface for type-system callers; non-interface surface
144 // does NOT dispatch through DatabaseCore (no __call, no pass-through
145 // wrappers).
146 // =========================================================================
147
148 /** @return ABJ_404_Solution_DatabaseConnectionManager */
149 public function connectionManager(): ABJ_404_Solution_DatabaseConnectionManager {
150 return $this->connectionManager;
151 }
152
153 /** @return ABJ_404_Solution_DatabaseQueryServices */
154 public function queryServices(): ABJ_404_Solution_DatabaseQueryServices {
155 return $this->queryServices;
156 }
157
158 /** @return ABJ_404_Solution_DatabaseQueryTimeoutManager */
159 public function queryTimeoutManager(): ABJ_404_Solution_DatabaseQueryTimeoutManager {
160 return $this->queryServices->queryTimeoutManager();
161 }
162
163 /** @return ABJ_404_Solution_DatabaseRecoveryServices */
164 public function recoveryServices(): ABJ_404_Solution_DatabaseRecoveryServices {
165 return $this->recoveryServices;
166 }
167
168 /** @return ABJ_404_Solution_DatabaseErrorClassifier */
169 public function errorClassifier(): ABJ_404_Solution_DatabaseErrorClassifier {
170 return $this->recoveryServices->errorClassifier();
171 }
172
173 /** @return ABJ_404_Solution_DatabaseRepairPolicy */
174 public function repairPolicy(): ABJ_404_Solution_DatabaseRepairPolicy {
175 return $this->recoveryServices->repairPolicy();
176 }
177
178 /** @return ABJ_404_Solution_DatabaseSqlErrorReporter */
179 public function sqlErrorReporter(): ABJ_404_Solution_DatabaseSqlErrorReporter {
180 return $this->recoveryServices->sqlErrorReporter();
181 }
182
183 /** @return ABJ_404_Solution_DatabaseTableNameResolver */
184 public function tableNameResolver(): ABJ_404_Solution_DatabaseTableNameResolver {
185 return $this->tableNameResolver;
186 }
187
188 /** @return ABJ_404_Solution_DatabaseNoticeStateHolder */
189 public function noticeState(): ABJ_404_Solution_DatabaseNoticeStateHolder {
190 return $this->noticeState;
191 }
192
193 /** @return ABJ_404_Solution_DatabaseCollationHelper */
194 public function collationHelper(): ABJ_404_Solution_DatabaseCollationHelper {
195 return $this->recoveryServices->collationHelper();
196 }
197
198 /** @return ABJ_404_Solution_DatabaseTableRepairer */
199 public function tableRepairer(): ABJ_404_Solution_DatabaseTableRepairer {
200 return $this->recoveryServices->tableRepairer();
201 }
202
203 /** @return ABJ_404_Solution_DatabaseWpdbResultHarvester */
204 public function resultHarvester(): ABJ_404_Solution_DatabaseWpdbResultHarvester {
205 return $this->queryServices->resultHarvester();
206 }
207
208 /** @return ABJ_404_Solution_DatabaseQueryDiagnostics */
209 public function queryDiagnostics(): ABJ_404_Solution_DatabaseQueryDiagnostics {
210 return $this->queryServices->queryDiagnostics();
211 }
212
213 /** @return ABJ_404_Solution_DatabaseTransactionExecutor */
214 public function transactionExecutor(): ABJ_404_Solution_DatabaseTransactionExecutor {
215 return $this->queryServices->transactionExecutor();
216 }
217
218 /** @return ABJ_404_Solution_DatabaseQueryRecoveryPolicy */
219 public function queryRecoveryPolicy(): ABJ_404_Solution_DatabaseQueryRecoveryPolicy {
220 return $this->queryServices->queryRecoveryPolicy();
221 }
222
223 /** @return ABJ_404_Solution_DatabaseQueryExecutor */
224 public function queryExecutor(): ABJ_404_Solution_DatabaseQueryExecutor {
225 return $this->queryServices->queryExecutor();
226 }
227
228 // =========================================================================
229 // Interface-required methods (DatabaseQueryInterface). These remain
230 // explicit so PHP's type system sees the contract.
231 // =========================================================================
232
233 /** @inheritDoc */
234 public function queryAndGetResults($query, $options = array()): array {
235 return $this->queryServices->queryExecutor()->queryAndGetResults($query, $options);
236 }
237
238 /** @inheritDoc */
239 public function queryScalarInt($query, $options = array()): int {
240 return $this->queryServices->queryExecutor()->queryScalarInt($query, $options);
241 }
242
243 /** @inheritDoc */
244 public function doTableNameReplacements($query): string {
245 return $this->tableNameResolver->doTableNameReplacements($query);
246 }
247
248 /**
249 * Classify a legacy view-build stage failure into 'resumable',
250 * 'skip', 'halt', or 'rethrow'. Retained for older diagnostics; the
251 * direct rebuild workflow no longer routes failures through staged
252 * collaborators.
253 *
254 * @param int $stageNumber
255 * @param string $errorText
256 * @return string
257 */
258 public function classifyStageFailure(int $stageNumber, string $errorText): string {
259 return $this->recoveryServices->errorClassifier()->stagedFailures()->classifyStageFailure($stageNumber, $errorText);
260 }
261
262 /** @inheritDoc */
263 public function executeAsTransaction(array $statementArray): void {
264 try {
265 $this->queryServices->transactionExecutor()->executeAsTransaction($statementArray);
266 } catch (Throwable $e) {
267 throw $e;
268 }
269 }
270
271 /**
272 * Retry a query after the server rejects the MariaDB SET STATEMENT timeout wrapper.
273 *
274 * @param string $query Passed by reference; mutated to the unwrapped query on success.
275 * @param array<string, mixed> $result Passed by reference; updated with retry result fields.
276 * @param 'OBJECT'|'OBJECT_K'|'ARRAY_A'|'ARRAY_N' $resultType wpdb output type for get_results().
277 * @return void
278 */
279 public function retryWithoutSetStatementWrapper(string &$query, array &$result, string $resultType): void {
280 $this->queryServices->queryTimeoutManager()->retryWithoutSetStatementWrapper($query, $result, $resultType);
281 }
282
283 // =========================================================================
284 // Special-case public surface: methods with local state or lazy init that
285 // cannot be a pure pass-through.
286 // =========================================================================
287
288 /**
289 * Lazy-resolve the Clock instance: previously cached value wins, then the
290 * service container (the standard test-injection seam per
291 * clock_injection_pattern.md), then a fresh SystemClock.
292 *
293 * @return ABJ_404_Solution_Clock
294 */
295 public function clock(): ABJ_404_Solution_Clock {
296 if ($this->clock !== null) { return $this->clock; }
297 if (class_exists('ABJ_404_Solution_ServiceContainer')) {
298 $resolved = ABJ_404_Solution_ServiceContainer::safeGet('clock');
299 if ($resolved instanceof ABJ_404_Solution_Clock) {
300 $this->clock = $resolved;
301 return $this->clock;
302 }
303 }
304 $this->clock = new ABJ_404_Solution_SystemClock();
305 return $this->clock;
306 }
307
308 /**
309 * Reset the per-request "SET STATEMENT wrapper unsupported" cache.
310 *
311 * @param bool $value
312 * @return void
313 */
314 public static function setSetStatementWrapperUnsupported(bool $value): void {
315 self::$setStatementWrapperUnsupported = $value;
316 ABJ_404_Solution_DatabaseRuntimeState::setSetStatementWrapperUnsupported($value);
317 }
318
319 /** @return bool */
320 public static function isSetStatementWrapperUnsupported(): bool {
321 return self::$setStatementWrapperUnsupported
322 || ABJ_404_Solution_DatabaseRuntimeState::isSetStatementWrapperUnsupported();
323 }
324 }
325