PluginProbe
404 Solution / 4.3.3
404 Solution v4.3.3
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.3, at includes/database/DatabaseCore.php

302 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 );
115 $this->noticeState = new ABJ_404_Solution_DatabaseNoticeStateHolder(
116 function (): bool {
117 // Deferred lookup: recoveryServices is assigned below.
118 return $this->recoveryServices->errorClassifier()->isQuotaCooldownActive();
119 }
120 );
121 $this->recoveryServices = new ABJ_404_Solution_DatabaseRecoveryServices(
122 $this,
123 $this->f,
124 $this->logger,
125 $this->queryServices->resultHarvester(),
126 $this->noticeState,
127 $this->queryServices->queryExecutor()
128 );
129 }
130
131 // =========================================================================
132 // Public accessors for the focused component classes. DAOs and other
133 // infrastructure-layer callers depend directly on the component they need
134 // and call methods on it. DatabaseCore itself satisfies
135 // DatabaseCoreInterface for type-system callers; non-interface surface
136 // does NOT dispatch through DatabaseCore (no __call, no pass-through
137 // wrappers).
138 // =========================================================================
139
140 /** @return ABJ_404_Solution_DatabaseConnectionManager */
141 public function connectionManager(): ABJ_404_Solution_DatabaseConnectionManager {
142 return $this->connectionManager;
143 }
144
145 /** @return ABJ_404_Solution_DatabaseQueryServices */
146 public function queryServices(): ABJ_404_Solution_DatabaseQueryServices {
147 return $this->queryServices;
148 }
149
150 /** @return ABJ_404_Solution_DatabaseQueryTimeoutManager */
151 public function queryTimeoutManager(): ABJ_404_Solution_DatabaseQueryTimeoutManager {
152 return $this->queryServices->queryTimeoutManager();
153 }
154
155 /** @return ABJ_404_Solution_DatabaseRecoveryServices */
156 public function recoveryServices(): ABJ_404_Solution_DatabaseRecoveryServices {
157 return $this->recoveryServices;
158 }
159
160 /** @return ABJ_404_Solution_DatabaseErrorClassifier */
161 public function errorClassifier(): ABJ_404_Solution_DatabaseErrorClassifier {
162 return $this->recoveryServices->errorClassifier();
163 }
164
165 /** @return ABJ_404_Solution_DatabaseRepairPolicy */
166 public function repairPolicy(): ABJ_404_Solution_DatabaseRepairPolicy {
167 return $this->recoveryServices->repairPolicy();
168 }
169
170 /** @return ABJ_404_Solution_DatabaseSqlErrorReporter */
171 public function sqlErrorReporter(): ABJ_404_Solution_DatabaseSqlErrorReporter {
172 return $this->recoveryServices->sqlErrorReporter();
173 }
174
175 /** @return ABJ_404_Solution_DatabaseTableNameResolver */
176 public function tableNameResolver(): ABJ_404_Solution_DatabaseTableNameResolver {
177 return $this->tableNameResolver;
178 }
179
180 /** @return ABJ_404_Solution_DatabaseNoticeStateHolder */
181 public function noticeState(): ABJ_404_Solution_DatabaseNoticeStateHolder {
182 return $this->noticeState;
183 }
184
185 /** @return ABJ_404_Solution_DatabaseCollationHelper */
186 public function collationHelper(): ABJ_404_Solution_DatabaseCollationHelper {
187 return $this->recoveryServices->collationHelper();
188 }
189
190 /** @return ABJ_404_Solution_DatabaseTableRepairer */
191 public function tableRepairer(): ABJ_404_Solution_DatabaseTableRepairer {
192 return $this->recoveryServices->tableRepairer();
193 }
194
195 /** @return ABJ_404_Solution_DatabaseWpdbResultHarvester */
196 public function resultHarvester(): ABJ_404_Solution_DatabaseWpdbResultHarvester {
197 return $this->queryServices->resultHarvester();
198 }
199
200 /** @return ABJ_404_Solution_DatabaseQueryDiagnostics */
201 public function queryDiagnostics(): ABJ_404_Solution_DatabaseQueryDiagnostics {
202 return $this->queryServices->queryDiagnostics();
203 }
204
205 /** @return ABJ_404_Solution_DatabaseTransactionExecutor */
206 public function transactionExecutor(): ABJ_404_Solution_DatabaseTransactionExecutor {
207 return $this->queryServices->transactionExecutor();
208 }
209
210 /** @return ABJ_404_Solution_DatabaseQueryRecoveryPolicy */
211 public function queryRecoveryPolicy(): ABJ_404_Solution_DatabaseQueryRecoveryPolicy {
212 return $this->queryServices->queryRecoveryPolicy();
213 }
214
215 /** @return ABJ_404_Solution_DatabaseQueryExecutor */
216 public function queryExecutor(): ABJ_404_Solution_DatabaseQueryExecutor {
217 return $this->queryServices->queryExecutor();
218 }
219
220 // =========================================================================
221 // Interface-required methods (DatabaseQueryInterface). These remain
222 // explicit so PHP's type system sees the contract.
223 // =========================================================================
224
225 /** @inheritDoc */
226 public function queryAndGetResults($query, $options = array()): array {
227 return $this->queryServices->queryExecutor()->queryAndGetResults($query, $options);
228 }
229
230 /** @inheritDoc */
231 public function queryScalarInt($query, $options = array()): int {
232 return $this->queryServices->queryExecutor()->queryScalarInt($query, $options);
233 }
234
235 /** @inheritDoc */
236 public function doTableNameReplacements($query): string {
237 return $this->tableNameResolver->doTableNameReplacements($query);
238 }
239
240 /** @inheritDoc */
241 public function executeAsTransaction(array $statementArray): void {
242 try {
243 $this->queryServices->transactionExecutor()->executeAsTransaction($statementArray);
244 } catch (Throwable $e) {
245 throw $e;
246 }
247 }
248
249 /**
250 * Retry a query after the server rejects the MariaDB SET STATEMENT timeout wrapper.
251 *
252 * @param string $query Passed by reference; mutated to the unwrapped query on success.
253 * @param array<string, mixed> $result Passed by reference; updated with retry result fields.
254 * @param 'OBJECT'|'OBJECT_K'|'ARRAY_A'|'ARRAY_N' $resultType wpdb output type for get_results().
255 * @return void
256 */
257 public function retryWithoutSetStatementWrapper(string &$query, array &$result, string $resultType): void {
258 $this->queryServices->queryTimeoutManager()->retryWithoutSetStatementWrapper($query, $result, $resultType);
259 }
260
261 // =========================================================================
262 // Special-case public surface: methods with local state or lazy init that
263 // cannot be a pure pass-through.
264 // =========================================================================
265
266 /**
267 * Lazy-resolve the Clock instance: previously cached value wins, then the
268 * service container (the standard test-injection seam per
269 * clock_injection_pattern.md), then a fresh SystemClock.
270 *
271 * @return ABJ_404_Solution_Clock
272 */
273 public function clock(): ABJ_404_Solution_Clock {
274 if ($this->clock !== null) { return $this->clock; }
275 if (class_exists('ABJ_404_Solution_ServiceContainer')) {
276 $resolved = ABJ_404_Solution_ServiceContainer::safeGet('clock');
277 if ($resolved instanceof ABJ_404_Solution_Clock) {
278 $this->clock = $resolved;
279 return $this->clock;
280 }
281 }
282 $this->clock = new ABJ_404_Solution_SystemClock();
283 return $this->clock;
284 }
285
286 /**
287 * Update the request-local "SET STATEMENT wrapper unsupported" cache.
288 * Confirmed rejection is also transient-backed across requests.
289 *
290 * @param bool $value
291 * @return void
292 */
293 public static function setSetStatementWrapperUnsupported(bool $value): void {
294 ABJ_404_Solution_DatabaseRuntimeState::setSetStatementWrapperUnsupported($value);
295 }
296
297 /** @return bool */
298 public static function isSetStatementWrapperUnsupported(): bool {
299 return ABJ_404_Solution_DatabaseRuntimeState::isSetStatementWrapperUnsupported();
300 }
301 }
302