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 / settings / SuggestionDisplayOptions.php

SuggestionDisplayOptions.php in 404 Solution trunk, at includes/settings/SuggestionDisplayOptions.php

254 lines 7.9 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 /**
8 * Typed value object for the suggestion-display slice of the `abj404_settings`
9 * WordPress option.
10 *
11 * Boundary normalizer (task: type-pressure at module boundaries).
12 *
13 * The full `abj404_settings` option is a wide heterogeneous bag (60+ keys
14 * with mixed string/int types and historical defaults). Wrapping the
15 * whole option in one VO would be premature and high-churn. Instead, this
16 * VO focuses on the slice used by the suggestion-display pipeline, which
17 * is read in at least three sites:
18 *
19 * - `FrontendRequestPipeline::dispatchRedirect` reads suggest_cats /
20 * suggest_tags to feed SpellChecker::findMatchingPosts.
21 * - `ShortCode::renderSuggestionsShortcode` reads suggest_max,
22 * suggest_minscore, suggest_minscore_enabled to filter rendered hits.
23 * - `ViewTrait_Settings` reads the same fields to render the admin form.
24 *
25 * Before this VO, each site re-implemented its own shape probing:
26 *
27 * $suggestCats = isset($options['suggest_cats']) && is_string($options['suggest_cats']) ? $options['suggest_cats'] : '1';
28 * $suggestTags = isset($options['suggest_tags']) && is_string($options['suggest_tags']) ? $options['suggest_tags'] : '1';
29 *
30 * The fields are stored as the strings '0' and '1' (legacy WP option
31 * serialization), so consumers that wanted a bool had to also cast.
32 * The VO surfaces both the legacy string form (for the rendering path
33 * that echoes the value back into form HTML) and a typed bool.
34 *
35 * Defaults match `PluginLogic::getDefaultOptions()` so a missing key
36 * is treated identically to the documented default.
37 */
38 final class ABJ_404_Solution_SuggestionDisplayOptions {
39
40 /** @var string */
41 private $suggestCats;
42
43 /** @var string */
44 private $suggestTags;
45
46 /** @var int */
47 private $suggestMax;
48
49 /** @var int */
50 private $suggestMinscore;
51
52 /** @var string */
53 private $suggestMinscoreEnabled;
54
55 /** @var string */
56 private $suggestTitle;
57
58 /** @var string */
59 private $suggestBefore;
60
61 /** @var string */
62 private $suggestAfter;
63
64 /** @var string */
65 private $suggestEntryBefore;
66
67 /** @var string */
68 private $suggestEntryAfter;
69
70 /** @var string */
71 private $suggestNoResults;
72
73 private function __construct(
74 string $suggestCats,
75 string $suggestTags,
76 int $suggestMax,
77 int $suggestMinscore,
78 string $suggestMinscoreEnabled,
79 string $suggestTitle,
80 string $suggestBefore,
81 string $suggestAfter,
82 string $suggestEntryBefore,
83 string $suggestEntryAfter,
84 string $suggestNoResults
85 ) {
86 $this->suggestCats = $suggestCats;
87 $this->suggestTags = $suggestTags;
88 $this->suggestMax = $suggestMax;
89 $this->suggestMinscore = $suggestMinscore;
90 $this->suggestMinscoreEnabled = $suggestMinscoreEnabled;
91 $this->suggestTitle = $suggestTitle;
92 $this->suggestBefore = $suggestBefore;
93 $this->suggestAfter = $suggestAfter;
94 $this->suggestEntryBefore = $suggestEntryBefore;
95 $this->suggestEntryAfter = $suggestEntryAfter;
96 $this->suggestNoResults = $suggestNoResults;
97 }
98
99 /**
100 * Normalize the raw `get_option('abj404_settings')` array (or any
101 * compatible payload) into a typed VO. A non-array input falls back
102 * to the documented defaults, matching the behaviour of
103 * `PluginLogic::getOptions()` when the option row is missing.
104 *
105 * @param mixed $raw
106 */
107 public static function fromOptionsArray($raw): self {
108 $options = is_array($raw) ? $raw : array();
109
110 return new self(
111 self::coerceBoolString($options, 'suggest_cats', '1'),
112 self::coerceBoolString($options, 'suggest_tags', '1'),
113 self::coercePositiveInt($options, 'suggest_max', 5),
114 self::coerceNonNegativeInt($options, 'suggest_minscore', 25),
115 self::coerceBoolString($options, 'suggest_minscore_enabled', '0'),
116 self::coerceString($options, 'suggest_title', '<h3>{suggest_title_text}</h3>'),
117 self::coerceString($options, 'suggest_before', '<ol>'),
118 self::coerceString($options, 'suggest_after', '</ol>'),
119 self::coerceString($options, 'suggest_entrybefore', '<li>'),
120 self::coerceString($options, 'suggest_entryafter', '</li>'),
121 self::coerceString($options, 'suggest_noresults', '<p>{suggest_noresults_text}</p>')
122 );
123 }
124
125 /**
126 * Legacy '0'/'1' string. Consumers that echo into form HTML or pass
127 * to SpellChecker (which expects the legacy shape) want this.
128 */
129 public function getSuggestCatsString(): string {
130 return $this->suggestCats;
131 }
132
133 public function getSuggestTagsString(): string {
134 return $this->suggestTags;
135 }
136
137 public function shouldSuggestCategories(): bool {
138 return $this->suggestCats === '1';
139 }
140
141 public function shouldSuggestTags(): bool {
142 return $this->suggestTags === '1';
143 }
144
145 public function getSuggestMax(): int {
146 return $this->suggestMax;
147 }
148
149 public function getSuggestMinscore(): int {
150 return $this->suggestMinscore;
151 }
152
153 public function isMinscoreEnabled(): bool {
154 return $this->suggestMinscoreEnabled === '1';
155 }
156
157 public function getSuggestTitleHtml(): string {
158 return $this->suggestTitle;
159 }
160
161 public function getSuggestBeforeHtml(): string {
162 return $this->suggestBefore;
163 }
164
165 public function getSuggestAfterHtml(): string {
166 return $this->suggestAfter;
167 }
168
169 public function getSuggestEntryBeforeHtml(): string {
170 return $this->suggestEntryBefore;
171 }
172
173 public function getSuggestEntryAfterHtml(): string {
174 return $this->suggestEntryAfter;
175 }
176
177 public function getSuggestNoResultsHtml(): string {
178 return $this->suggestNoResults;
179 }
180
181 /**
182 * @param array<mixed, mixed> $options
183 */
184 private static function coerceString(array $options, string $key, string $default): string {
185 if (!isset($options[$key])) {
186 return $default;
187 }
188 $v = $options[$key];
189 if (is_string($v)) {
190 return $v;
191 }
192 if (is_scalar($v)) {
193 return (string)$v;
194 }
195 return $default;
196 }
197
198 /**
199 * Legacy boolean stored as '0' / '1'. Coerces other truthy/falsy
200 * scalar shapes (numeric int, bool) into the canonical string.
201 *
202 * @param array<mixed, mixed> $options
203 */
204 private static function coerceBoolString(array $options, string $key, string $default): string {
205 if (!isset($options[$key])) {
206 return $default;
207 }
208 $v = $options[$key];
209 if (is_string($v)) {
210 return $v === '1' ? '1' : '0';
211 }
212 if (is_bool($v)) {
213 return $v ? '1' : '0';
214 }
215 if (is_int($v) || is_float($v)) {
216 return ((int)$v) === 1 ? '1' : '0';
217 }
218 return $default;
219 }
220
221 /**
222 * @param array<mixed, mixed> $options
223 */
224 private static function coerceNonNegativeInt(array $options, string $key, int $default): int {
225 if (!isset($options[$key])) {
226 return $default;
227 }
228 $v = $options[$key];
229 if (is_int($v)) {
230 return $v < 0 ? $default : $v;
231 }
232 if (is_float($v)) {
233 $i = (int)$v;
234 return $i < 0 ? $default : $i;
235 }
236 if (is_string($v) && is_numeric($v)) {
237 $i = (int)$v;
238 return $i < 0 ? $default : $i;
239 }
240 return $default;
241 }
242
243 /**
244 * suggest_max must be >= 1; a 0 or negative would render no
245 * suggestions at all and is treated as a malformed override.
246 *
247 * @param array<mixed, mixed> $options
248 */
249 private static function coercePositiveInt(array $options, string $key, int $default): int {
250 $i = self::coerceNonNegativeInt($options, $key, $default);
251 return $i < 1 ? $default : $i;
252 }
253 }
254