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

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

324 lines 13.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3
4 if (!defined('ABSPATH')) {
5 exit;
6 }
7
8 /**
9 * Composition root and singleton lifecycle for the database/repository/view
10 * collaborator graph.
11 *
12 * Exposes typed accessors so callers obtain a concrete collaborator
13 * (DatabaseCore, ContentRepository, RedirectsRepository,
14 * RedirectsRetentionService, LogsRepository, StatsRepository,
15 * ViewReadService) without depending on this class
16 * to dispatch unrelated work for them. Collaborators are resolved on demand
17 * by the plugin classmap autoloader registered in 404-solution.php
18 * (production) and tests/bootstrap.php (tests); manual require_once wiring
19 * at parse time is intentionally absent (see
20 * tests/DataAccessRequireTimeWiringTest.php for the structural guard).
21 *
22 * Three classes of legacy facade surface were removed by the 2026-06-06
23 * audit (M200) narrowing. Each was a test-only compatibility shim that
24 * recreated god-object coupling:
25 * 1. 33 constant re-exports from {LogsRepository, StatsRepository,
26 * RedirectsRepository, ViewReadRuntimeState, DatabaseRuntimeState}.
27 * Tests now reach the owning class directly.
28 * 2. getPostOrGetSanitize / getPostOrGetSanitizeUrl request-sanitization
29 * fallbacks. The authoritative implementation lives on Functions; no
30 * production caller routed through DataAccess.
31 * 3. stageFailurePolicy() vestigial classifier marker. The actual
32 * per-stage policy is owned by
33 * ABJ_404_Solution_DatabaseStagedFailureClassifier::classifyStageFailure().
34 * The __call() rejector below is the structural guard preventing ad-hoc
35 * pass-throughs from being re-added.
36 */
37 class ABJ_404_Solution_DataAccess {
38
39 /** @var self|null */
40 private static $instance = null;
41
42 /** @var ABJ_404_Solution_DatabaseCore The extracted database infrastructure layer. */
43 private $dbCore;
44
45 /** @var ABJ_404_Solution_ContentRepository The extracted content/cache repository. */
46 private $contentRepo;
47
48 /** @var ABJ_404_Solution_RedirectsRepository The extracted redirects repository. */
49 private $redirectsRepo;
50
51 /** @var ABJ_404_Solution_RedirectsRetentionService|null Lazy-initialized retention workflow service. */
52 private $retentionService = null;
53
54 /** @var ABJ_404_Solution_LogsRepository The extracted logs repository. */
55 private $logsRepo;
56
57 /**
58 * @var ABJ_404_Solution_StatsRepository
59 *
60 * Test-only composition surface. Production code MUST resolve the stats
61 * repository through StatsRepositoryInterface (constructor-injected) or via
62 * ABJ_404_Solution_StatsRepositoryResolver::resolve(). Held on DataAccess
63 * exclusively so tests that construct a real (or stubbed-deps) DataAccess
64 * can drive the real StatsRepository against a custom DbCore/LogsRepo.
65 *
66 * Production prohibition is enforced by
67 * StatsRepositoryExtractionTest::testProductionCallersDoNotResolveStatsRepositoryThroughDataAccess.
68 * The 12 prior pass-through methods (getStatsCount, getPeriodicStatsSummary,
69 * getStatsDashboardSnapshot, refreshStatsDashboardSnapshot,
70 * getEarliestLogTimestamp, getTopCapturedForDigest,
71 * buildTopCapturedForDigestQuery, getDigestSummaryStats,
72 * getCapturedCountForNotification, getPostsNeedingContentKeywords,
73 * bulkUpdateContentKeywords, getPeriodicStatsSummariesCached) have been
74 * removed; see StatsRepositoryExtractionTest::testDataAccessNoLongerExposesStatsRepoPassThroughs.
75 */
76 private $statsRepo;
77
78 /** @var ABJ_404_Solution_ViewReadService The extracted view read service (Phase 6). */
79 private $viewReadService;
80
81 /** @var ABJ_404_Solution_Functions */
82 private $f;
83
84 /** @var ABJ_404_Solution_Logging */
85 private $logger;
86
87 /**
88 * @param ABJ_404_Solution_DataAccessDependencies|null $dependencies
89 * @throws InvalidArgumentException when legacy positional arguments are supplied.
90 */
91 public function __construct(?ABJ_404_Solution_DataAccessDependencies $dependencies = null) {
92 if (func_num_args() > 1) {
93 throw new InvalidArgumentException(
94 'DataAccess constructor accepts a DataAccessDependencies bundle; positional collaborator arguments were removed.'
95 );
96 }
97
98 $dependencies = $dependencies !== null ? $dependencies : new ABJ_404_Solution_DataAccessDependencies();
99 $this->f = self::resolveFunctions($dependencies->functions());
100 $this->logger = $this->resolveLogger($dependencies->logging());
101 $dbCore = $dependencies->dbCore();
102 $this->dbCore = $dbCore !== null ? $dbCore : $this->createDbCore();
103 $contentRepo = $dependencies->contentRepo();
104 if ($contentRepo !== null) {
105 $this->contentRepo = $contentRepo;
106 } else {
107 $this->contentRepo = new ABJ_404_Solution_ContentRepository($this->dbCore, $this->f, $this->logger);
108 }
109
110 $redirectsRepo = $dependencies->redirectsRepo();
111 if ($redirectsRepo !== null) {
112 $this->redirectsRepo = $redirectsRepo;
113 } else {
114 $this->redirectsRepo = new ABJ_404_Solution_RedirectsRepository($this->dbCore, $this->f, $this->logger);
115 }
116
117 $logsRepo = $dependencies->logsRepo();
118 $this->logsRepo = $logsRepo !== null
119 ? $logsRepo
120 : new ABJ_404_Solution_LogsRepository($this->dbCore, $this->f, $this->logger);
121
122 $statsRepo = $dependencies->statsRepo();
123 if ($statsRepo !== null) {
124 $this->statsRepo = $statsRepo;
125 } else {
126 $this->statsRepo = new ABJ_404_Solution_StatsRepository($this->dbCore, $this->logsRepo, $this->f, $this->logger);
127 }
128
129 $this->retentionService = $dependencies->retentionService();
130 $viewReadService = $dependencies->viewReadService();
131 if ($viewReadService !== null) {
132 $this->viewReadService = $viewReadService;
133 } else {
134 $this->viewReadService = new ABJ_404_Solution_ViewReadService(
135 $this->dbCore, $this->logsRepo, $this->redirectsRepo, $this->f, $this->logger
136 );
137 }
138 }
139
140 /**
141 * @param mixed $functions
142 * @return ABJ_404_Solution_Functions
143 */
144 private static function resolveFunctions($functions) {
145 if ($functions instanceof ABJ_404_Solution_Functions) {
146 return $functions;
147 }
148 return abj_service('functions');
149 }
150
151 /**
152 * @param mixed $logging
153 * @return ABJ_404_Solution_Logging
154 */
155 private function resolveLogger($logging) {
156 if ($logging instanceof ABJ_404_Solution_Logging) {
157 return $logging;
158 }
159 // Accept duck-typed test spy loggers (warn/errorMessage/debugMessage)
160 // so per-test log-level assertions can observe what production code
161 // dispatched. Without this, the strict instanceof check above silently
162 // drops the spy and DAO sub-services capture the production singleton
163 // instead, making warn() / errorMessage() invisible to the test.
164 // The chain below ($contentRepo, $redirectsRepo, $logsRepo,
165 // $statsRepo, $viewReadService, and DbCore
166 // including its recovery sub-services) accept untyped $logger
167 // parameters, so the spy reaches all of them.
168 if (is_object($logging) && method_exists($logging, 'warn') && method_exists($logging, 'errorMessage')) {
169 /** @var ABJ_404_Solution_Logging $logging */
170 return $logging;
171 }
172 return abj_service('logging');
173 }
174
175 /** @return ABJ_404_Solution_DatabaseCore */
176 private function createDbCore() {
177 return new ABJ_404_Solution_DatabaseCore($this->f, $this->logger);
178 }
179
180 /** @return ABJ_404_Solution_DatabaseCore */
181 public function getDbCore(): ABJ_404_Solution_DatabaseCore {
182 if ($this->dbCore === null) {
183 $this->dbCore = new ABJ_404_Solution_DatabaseCore($this->f, $this->logger);
184 }
185 return $this->dbCore;
186 }
187
188 /** @return ABJ_404_Solution_ContentRepository */
189 public function getContentRepo(): ABJ_404_Solution_ContentRepository {
190 if ($this->contentRepo === null) {
191 $this->contentRepo = new ABJ_404_Solution_ContentRepository($this->getDbCore(), $this->f, $this->logger);
192 }
193 return $this->contentRepo;
194 }
195
196 /** @return ABJ_404_Solution_RedirectsRepository */
197 public function getRedirectsRepo(): ABJ_404_Solution_RedirectsRepository {
198 if ($this->redirectsRepo === null) {
199 $this->redirectsRepo = new ABJ_404_Solution_RedirectsRepository($this->getDbCore(), $this->f, $this->logger);
200 }
201 return $this->redirectsRepo;
202 }
203
204 /** @return ABJ_404_Solution_RedirectsRetentionService */
205 public function getRetentionService(): ABJ_404_Solution_RedirectsRetentionService {
206 if ($this->retentionService === null) {
207 $this->retentionService = new ABJ_404_Solution_RedirectsRetentionService(
208 $this->dbCore !== null ? $this->dbCore : $this->getDbCore(),
209 $this->redirectsRepo !== null ? $this->redirectsRepo : $this->getRedirectsRepo(),
210 $this->f,
211 $this->logger
212 );
213 }
214 return $this->retentionService;
215 }
216
217 /** @return ABJ_404_Solution_LogsRepository */
218 public function getLogsRepo(): ABJ_404_Solution_LogsRepository {
219 if ($this->logsRepo === null) {
220 $this->logsRepo = new ABJ_404_Solution_LogsRepository($this->getDbCore(), $this->f, $this->logger);
221 }
222 return $this->logsRepo;
223 }
224
225 /**
226 * Test-only composition surface. Production callers must inject
227 * StatsRepositoryInterface via constructor or call
228 * ABJ_404_Solution_StatsRepositoryResolver::resolve(); using
229 * $dao->getStatsRepo() in includes/ is forbidden and is enforced by
230 * StatsRepositoryExtractionTest::testProductionCallersDoNotResolveStatsRepositoryThroughDataAccess
231 * (also catches the obfuscated 'get'.'StatsRepo' and quoted-string variants).
232 *
233 * Retained only as a composition exposure for tests that subclass
234 * DataAccess (e.g. StatsCacheInv_TestDAO, the makeRecordingDao() pattern in
235 * DataAccessQueryTimeoutAuditTest) and need the StatsRepository instance
236 * composed from the same private deps the test wired into DataAccess.
237 *
238 * @return ABJ_404_Solution_StatsRepositoryInterface
239 */
240 public function getStatsRepo(): ABJ_404_Solution_StatsRepositoryInterface {
241 if ($this->statsRepo === null) {
242 $this->statsRepo = new ABJ_404_Solution_StatsRepository($this->getDbCore(), $this->getLogsRepo(), $this->f, $this->logger);
243 }
244 return $this->statsRepo;
245 }
246
247 /** @return ABJ_404_Solution_ViewReadService */
248 public function getViewReadService(): ABJ_404_Solution_ViewReadService {
249 if ($this->viewReadService === null) {
250 $this->viewReadService = new ABJ_404_Solution_ViewReadService(
251 $this->getDbCore(), $this->getLogsRepo(), $this->getRedirectsRepo(), $this->f, $this->logger
252 );
253 }
254 return $this->viewReadService;
255 }
256
257 /**
258 * Rejects calls to removed facade pass-through methods.
259 *
260 * Extracted repositories and services are intentionally absent from a
261 * delegate chain: callers must use the typed accessor/injected interface
262 * for the owning collaborator instead of relying on this compatibility
263 * facade to redispatch arbitrary public methods.
264 *
265 * @param string $name
266 * @param array<int, mixed> $arguments
267 * @return mixed
268 * @throws \BadMethodCallException
269 */
270 public function __call(string $name, array $arguments) {
271 throw new \BadMethodCallException(
272 'Method ' . $name . '() not found on ' . static::class . ' or its sub-services.'
273 );
274 }
275
276 /**
277 * Return the current singleton instance without consulting the container
278 * or building a new one. Used by `abj_service()` to honor a test-installed
279 * singleton override without forcing the container to cache a stale
280 * binding. Mirrors the pattern on PluginLogic / Logging.
281 *
282 * @return self|null
283 */
284 public static function peekInstance() {
285 return self::$instance;
286 }
287
288 /**
289 * Install a singleton instance directly. Symmetric with `peekInstance()`;
290 * the canonical seam for tests that need to swap in a test double, and
291 * for callers that have already constructed a fully configured instance.
292 * Pass `null` to clear the cached singleton.
293 *
294 * @param self|null $instance
295 * @return void
296 */
297 public static function setInstance($instance) {
298 self::$instance = $instance;
299 }
300
301 /** @return self */
302 public static function getInstance() {
303 if (self::$instance !== null) {
304 return self::$instance;
305 }
306
307 // If the DI container is initialized, prefer it.
308 if (class_exists('ABJ_404_Solution_ServiceContainer')) {
309 $resolved = ABJ_404_Solution_ServiceContainer::safeGet('data_access');
310 if ($resolved instanceof self) {
311 self::$instance = $resolved;
312 return self::$instance;
313 }
314 }
315
316 // For backward compatibility, create with no arguments
317 // The constructor will use getInstance() for dependencies
318 self::$instance = new ABJ_404_Solution_DataAccess();
319
320 return self::$instance;
321 }
322
323 }
324