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 / logs / LoggingStateStore.php

LoggingStateStore.php in 404 Solution trunk, at includes/logs/LoggingStateStore.php

184 lines 6.3 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 * Single recursion-safe chokepoint for logging-owned scalars stored
10 * as keys inside the abj404_settings option row.
11 *
12 * Every logging code path that needs the debug-file suffix, the last-emailed
13 * debug-log line, the weekly heartbeat timestamp, or the user debug-mode flag
14 * goes through this adapter. The adapter reaches storage ONLY through the raw,
15 * non-normalizing, non-logging accessors getRawSettingValue()/
16 * setRawSettingValue() on the options repository. It never calls
17 * getOptions()/updateOptions()/normalizeForRead().
18 *
19 * Why this matters (4.3.0 "broken sites after the latest update" OOM): the
20 * runtime logger reads settings to locate its own debug-log file. Routing those
21 * reads through getOptions() runs the normalize pipeline, which logs a warning
22 * on any schema-validation failure; that warning re-enters the logger, which
23 * re-reads options, recursing without bound until memory is exhausted. Reading
24 * raw keeps logging strictly downstream of the settings repository so it can
25 * never re-enter the settings-read pipeline. Concentrating all logging<->storage
26 * access here makes that property a structural invariant of one small class
27 * rather than a convention scattered across the logging subsystem.
28 *
29 * No storage migration: the scalars live as keys inside the abj404_settings
30 * option row.
31 */
32 class ABJ_404_Solution_LoggingStateStore {
33
34 /** abj404_settings key holding the debug-file suffix. */
35 const DEBUG_FILE_KEY = 'debug_file_key';
36
37 /** abj404_settings key holding the last-emailed debug-log line. */
38 const LAST_SENT_LINE = 'last_sent_line';
39
40 /** abj404_settings key holding the last successful weekly heartbeat send time. */
41 const LAST_HEARTBEAT_SENT_AT = 'last_heartbeat_sent_at';
42
43 /** abj404_settings key holding the user debug-mode flag (user config; read-only here). */
44 const DEBUG_MODE = 'debug_mode';
45
46 /**
47 * Options repository exposing the raw, non-normalizing, non-logging
48 * accessors getRawSettingValue()/setRawSettingValue(). Typed loosely (not
49 * the concrete resolver) so test doubles and a degraded-boot stub can stand
50 * in; null is tolerated (the raw accessors no-op) so logging never crashes
51 * for want of a repository. Access is guarded by is_object()/method_exists()
52 * below.
53 *
54 * @var object|null
55 */
56 private $optionsRepo;
57
58 /**
59 * @param object|null $optionsRepo Options repository providing
60 * getRawSettingValue(string)/setRawSettingValue(string, mixed), or
61 * null in degraded boot (the raw accessors then no-op).
62 */
63 public function __construct($optionsRepo) {
64 $this->optionsRepo = $optionsRepo;
65 }
66
67 /**
68 * Resolve the shared logging-state store from the service container, falling
69 * back to a fresh instance over the options repository when the container
70 * cannot serve it (degraded boot). Always returns a usable, correctly-typed
71 * store: the fallback tolerates a missing options repository (its raw
72 * accessors no-op), so logging never crashes for want of a state store.
73 *
74 * @return ABJ_404_Solution_LoggingStateStore
75 */
76 public static function resolve(): ABJ_404_Solution_LoggingStateStore {
77 $store = abj_service_optional('logging_state_store');
78 if ($store instanceof self) {
79 return $store;
80 }
81 $optionsRepo = abj_service_optional('options_repository');
82 return new self(is_object($optionsRepo) ? $optionsRepo : null);
83 }
84
85 /**
86 * Current debug-file suffix, or null when unset / not a string.
87 *
88 * @return string|null
89 */
90 public function getDebugFileKey(): ?string {
91 $v = $this->rawGet(self::DEBUG_FILE_KEY);
92 return is_string($v) ? $v : null;
93 }
94
95 /**
96 * Persist the debug-file suffix (null clears it).
97 *
98 * @param string|null $key
99 * @return void
100 */
101 public function setDebugFileKey(?string $key): void {
102 $this->rawSet(self::DEBUG_FILE_KEY, $key);
103 }
104
105 /**
106 * Last-emailed debug-log line number, or -1 when unset / not scalar.
107 *
108 * @return int
109 */
110 public function getLastSentLine(): int {
111 $v = $this->rawGet(self::LAST_SENT_LINE);
112 return is_scalar($v) ? (int)$v : -1;
113 }
114
115 /**
116 * Persist the last-emailed debug-log line number.
117 *
118 * @param int $line
119 * @return void
120 */
121 public function setLastSentLine(int $line): void {
122 $this->rawSet(self::LAST_SENT_LINE, $line);
123 }
124
125 /**
126 * Last successful weekly heartbeat send time, or 0 when unset / not scalar.
127 *
128 * @return int
129 */
130 public function getLastHeartbeatSentAt(): int {
131 $v = $this->rawGet(self::LAST_HEARTBEAT_SENT_AT);
132 return is_scalar($v) ? (int)$v : 0;
133 }
134
135 /**
136 * Persist the last successful weekly heartbeat send time.
137 *
138 * @param int $timestamp Unix seconds.
139 * @return void
140 */
141 public function setLastHeartbeatSentAt(int $timestamp): void {
142 $this->rawSet(self::LAST_HEARTBEAT_SENT_AT, $timestamp);
143 }
144
145 /**
146 * Whether the user has enabled debug mode. Read-only here: debug_mode is
147 * user configuration owned by the settings UI; logging only consumes it.
148 *
149 * @return bool
150 */
151 public function isDebugMode(): bool {
152 return $this->rawGet(self::DEBUG_MODE) == true;
153 }
154
155 /**
156 * Raw, side-effect-free read of one abj404_settings key. Returns null when
157 * the repository cannot serve a raw read (degraded boot / unexpected
158 * double), which every caller treats as "unset".
159 *
160 * @param string $key
161 * @return mixed
162 */
163 private function rawGet(string $key) {
164 if (is_object($this->optionsRepo) && method_exists($this->optionsRepo, 'getRawSettingValue')) {
165 return $this->optionsRepo->getRawSettingValue($key);
166 }
167 return null;
168 }
169
170 /**
171 * Raw, side-effect-free write of one abj404_settings key. No-op when the
172 * repository cannot serve a raw write (degraded boot / unexpected double).
173 *
174 * @param string $key
175 * @param mixed $value
176 * @return void
177 */
178 private function rawSet(string $key, $value): void {
179 if (is_object($this->optionsRepo) && method_exists($this->optionsRepo, 'setRawSettingValue')) {
180 $this->optionsRepo->setRawSettingValue($key, $value);
181 }
182 }
183 }
184