PluginProbe
404 Solution / 4.1.19
404 Solution v4.1.19
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 / PostRef.php

PostRef.php in 404 Solution 4.1.19, at includes/PostRef.php

249 lines 7.0 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 a WP_Post return from `get_post()` / `get_posts()`.
9 *
10 * Boundary normalizer (task: type-pressure at module boundaries).
11 *
12 * WordPress's `get_post()` is typed `WP_Post|array|null` and has a
13 * documented escape hatch (`OBJECT_K` / `ARRAY_A` / `ARRAY_N` output modes)
14 * that changes the return shape. Even in OBJECT mode (the default), the
15 * underlying WP_Post properties are typed as `string|int` in stubs (the
16 * raw DB column types) and consumers must shape-probe them before use:
17 *
18 * $post = get_post($destId);
19 * if (!is_object($post)) { return false; }
20 * $postStatus = strtolower($post->post_status);
21 *
22 * foreach ($posts as $post) {
23 * if (!is_object($post)) { continue; }
24 * $content = property_exists($post, 'post_content') ? (string)$post->post_content : '';
25 * $postId = property_exists($post, 'ID') ? intval($post->ID) : 0;
26 * $postTitle = property_exists($post, 'post_title') ? (string)$post->post_title : '';
27 * }
28 *
29 * Two consumer shapes for the same boundary, both reinventing their own
30 * property-existence probing. This VO collapses them into one:
31 *
32 * $ref = ABJ_404_Solution_PostRef::fromWpPost(get_post($destId));
33 * if ($ref === null) { return false; }
34 * if ($ref->isPublished()) { ... }
35 *
36 * Schema (after normalization):
37 *
38 * - id : int >= 0
39 * - title : string
40 * - content : string
41 * - status : string (lowercased; '' if absent / non-scalar)
42 * - type : string (post_type; '' if absent)
43 * - parentId : int >= 0
44 *
45 * Construction accepts WP_Post, an associative array (ARRAY_A output mode
46 * or a hand-built fixture), or `null` / non-object input. `fromWpPost`
47 * returns `null` when the input cannot be coerced to a usable post
48 * reference.
49 */
50 final class ABJ_404_Solution_PostRef {
51
52 /** @var int */
53 private $id;
54
55 /** @var string */
56 private $title;
57
58 /** @var string */
59 private $content;
60
61 /** @var string */
62 private $status;
63
64 /** @var string */
65 private $type;
66
67 /** @var int */
68 private $parentId;
69
70 private function __construct(
71 int $id,
72 string $title,
73 string $content,
74 string $status,
75 string $type,
76 int $parentId
77 ) {
78 $this->id = $id;
79 $this->title = $title;
80 $this->content = $content;
81 $this->status = $status;
82 $this->type = $type;
83 $this->parentId = $parentId;
84 }
85
86 /**
87 * Normalize a `get_post()` return into a typed VO, or null when the
88 * input is unrecoverably malformed (not an object or array, or
89 * lacking an ID that we can act on).
90 *
91 * @param mixed $raw
92 */
93 public static function fromWpPost($raw): ?self {
94 if ($raw === null || is_bool($raw)) {
95 return null;
96 }
97 if (is_object($raw)) {
98 $id = self::coerceObjectInt($raw, 'ID');
99 if ($id <= 0) {
100 return null;
101 }
102 $title = self::coerceObjectString($raw, 'post_title');
103 $content = self::coerceObjectString($raw, 'post_content');
104 $status = strtolower(self::coerceObjectString($raw, 'post_status'));
105 $type = self::coerceObjectString($raw, 'post_type');
106 $parentId = self::coerceObjectInt($raw, 'post_parent');
107 return new self($id, $title, $content, $status, $type, $parentId);
108 }
109 if (is_array($raw)) {
110 $id = self::coerceArrayInt($raw, 'ID');
111 if ($id <= 0) {
112 return null;
113 }
114 $title = self::coerceArrayString($raw, 'post_title');
115 $content = self::coerceArrayString($raw, 'post_content');
116 $status = strtolower(self::coerceArrayString($raw, 'post_status'));
117 $type = self::coerceArrayString($raw, 'post_type');
118 $parentId = self::coerceArrayInt($raw, 'post_parent');
119 return new self($id, $title, $content, $status, $type, $parentId);
120 }
121 return null;
122 }
123
124 /**
125 * @param array<int, mixed> $rawList
126 * @return array<int, self> Skips inputs that fail normalization.
127 */
128 public static function fromWpPostList(array $rawList): array {
129 $result = array();
130 foreach ($rawList as $raw) {
131 $vo = self::fromWpPost($raw);
132 if ($vo !== null) {
133 $result[] = $vo;
134 }
135 }
136 return $result;
137 }
138
139 public function getId(): int {
140 return $this->id;
141 }
142
143 public function getTitle(): string {
144 return $this->title;
145 }
146
147 public function getContent(): string {
148 return $this->content;
149 }
150
151 /** Lowercased post_status. '' when absent. */
152 public function getStatus(): string {
153 return $this->status;
154 }
155
156 public function getType(): string {
157 return $this->type;
158 }
159
160 public function getParentId(): int {
161 return $this->parentId;
162 }
163
164 /**
165 * True iff the post is considered live by `DataAccessTrait_Redirects`:
166 * the historical predicate covers both 'publish' and 'published'
167 * because some imports / legacy plugins store the alternate spelling.
168 */
169 public function isPublished(): bool {
170 return in_array($this->status, array('publish', 'published'), true);
171 }
172
173 /**
174 * @param object $obj
175 */
176 private static function coerceObjectString($obj, string $key): string {
177 if (!property_exists($obj, $key)) {
178 return '';
179 }
180 $v = $obj->{$key};
181 if (is_string($v)) {
182 return $v;
183 }
184 if (is_scalar($v)) {
185 return (string)$v;
186 }
187 return '';
188 }
189
190 /**
191 * @param object $obj
192 */
193 private static function coerceObjectInt($obj, string $key): int {
194 if (!property_exists($obj, $key)) {
195 return 0;
196 }
197 $v = $obj->{$key};
198 return self::scalarToInt($v);
199 }
200
201 /**
202 * @param array<mixed, mixed> $arr
203 */
204 private static function coerceArrayString(array $arr, string $key): string {
205 if (!isset($arr[$key])) {
206 return '';
207 }
208 $v = $arr[$key];
209 if (is_string($v)) {
210 return $v;
211 }
212 if (is_scalar($v)) {
213 return (string)$v;
214 }
215 return '';
216 }
217
218 /**
219 * @param array<mixed, mixed> $arr
220 */
221 private static function coerceArrayInt(array $arr, string $key): int {
222 if (!isset($arr[$key])) {
223 return 0;
224 }
225 return self::scalarToInt($arr[$key]);
226 }
227
228 /**
229 * @param mixed $v
230 */
231 private static function scalarToInt($v): int {
232 if (is_int($v)) {
233 return $v < 0 ? 0 : $v;
234 }
235 if (is_float($v)) {
236 $i = (int)$v;
237 return $i < 0 ? 0 : $i;
238 }
239 if (is_string($v) && is_numeric($v)) {
240 $i = (int)$v;
241 return $i < 0 ? 0 : $i;
242 }
243 if (is_bool($v)) {
244 return $v ? 1 : 0;
245 }
246 return 0;
247 }
248 }
249