PluginProbe
404 Solution / trunk
404 Solution vtrunk
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 trunk, at includes/database/DatabaseCore.php

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