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 / redirects / RedirectUpdate.php

RedirectUpdate.php in 404 Solution trunk, at includes/redirects/RedirectUpdate.php

212 lines 6.8 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 an "update an existing redirect row" request.
9 *
10 * Boundary parameter object for {@see ABJ_404_Solution_RedirectsRepository::updateRedirect()}.
11 *
12 * The repo method used to accept eight positional parameters:
13 *
14 * updateRedirect($type, $dest, $fromURL, $idForUpdate, $redirectCode,
15 * $statusType, $startTs = null, $endTs = null)
16 *
17 * Two of those (`$dest` and `$fromURL`) are arbitrary strings with no
18 * type-level distinction, and five more (`$type`, `$idForUpdate`,
19 * `$redirectCode`, `$startTs`, `$endTs`) are numbers with overlapping
20 * semantics. Swapping any pair compiles cleanly but writes garbage to the
21 * `wp_abj404_redirects` row. One such swap is silent at the DB layer
22 * because both columns store strings.
23 *
24 * Wrapping the call shape in a VO with named fields makes the swap
25 * structurally impossible: every caller has to spell out `id:`, `fromUrl:`,
26 * `destination:`, etc. via the named constructor, and PHPStan can type-check
27 * each field independently.
28 *
29 * Field semantics (mirrors the row columns):
30 *
31 * - id : int > 0 redirect row primary key (REQUIRED)
32 * - type : int >= 0 ABJ404_TYPE_* (post / page / external / etc.)
33 * - fromUrl : string source URL that 404s and should redirect
34 * - destination : string final destination URL / object id (engine-dependent)
35 * - code : string HTTP status code as string ("301", "302", ...)
36 * - statusType : string ABJ404_STATUS_* value, stored as-is
37 * - startTs : ?int epoch seconds; null clears the schedule lower bound
38 * - endTs : ?int epoch seconds; null clears the schedule upper bound
39 *
40 * Construct via {@see self::fromArray()} at new call sites. The constructor
41 * is private so the field order is never depended on at call sites.
42 */
43 final class ABJ_404_Solution_RedirectUpdate {
44
45 /** @var int */
46 private $id;
47
48 /** @var int */
49 private $type;
50
51 /** @var string */
52 private $fromUrl;
53
54 /** @var string */
55 private $destination;
56
57 /** @var string */
58 private $code;
59
60 /** @var string */
61 private $statusType;
62
63 /** @var int|null */
64 private $startTs;
65
66 /** @var int|null */
67 private $endTs;
68
69 /**
70 * @param int|null $startTs
71 * @param int|null $endTs
72 */
73 private function __construct(
74 int $id,
75 int $type,
76 string $fromUrl,
77 string $destination,
78 string $code,
79 string $statusType,
80 $startTs,
81 $endTs
82 ) {
83 $this->id = $id;
84 $this->type = $type;
85 $this->fromUrl = $fromUrl;
86 $this->destination = $destination;
87 $this->code = $code;
88 $this->statusType = $statusType;
89 $this->startTs = ($startTs === null) ? null : (int)$startTs;
90 $this->endTs = ($endTs === null) ? null : (int)$endTs;
91 }
92
93 /**
94 * Legacy positional factory. Prefer {@see self::fromArray()} at new call
95 * sites so same-type fields are spelled out before construction.
96 *
97 * @param int $id Primary key of the row to update.
98 * @param int $type ABJ404_TYPE_* enum value.
99 * @param string $fromUrl Source URL (the URL that 404'd).
100 * @param string $destination Final destination (URL or object id).
101 * @param string $code HTTP code as string ("301" / "302").
102 * @param string $statusType ABJ404_STATUS_* enum value.
103 * @param int|null $startTs Schedule start (epoch sec), or null.
104 * @param int|null $endTs Schedule end (epoch sec), or null.
105 */
106 public static function create(
107 int $id,
108 int $type,
109 string $fromUrl,
110 string $destination,
111 string $code,
112 string $statusType,
113 $startTs = null,
114 $endTs = null
115 ): self {
116 return new self($id, $type, $fromUrl, $destination, $code, $statusType, $startTs, $endTs);
117 }
118
119 /**
120 * Build a redirect-update request from named fields.
121 *
122 * @param array<string, mixed> $fields
123 * @return self
124 */
125 public static function fromArray(array $fields): self {
126 return new self(
127 self::requiredInt($fields, 'id'),
128 self::requiredInt($fields, 'type'),
129 self::requiredString($fields, 'fromUrl'),
130 self::requiredString($fields, 'destination'),
131 self::requiredString($fields, 'code'),
132 self::requiredString($fields, 'statusType'),
133 array_key_exists('startTs', $fields) ? self::nullableInt($fields, 'startTs') : null,
134 array_key_exists('endTs', $fields) ? self::nullableInt($fields, 'endTs') : null
135 );
136 }
137
138 /**
139 * @param array<string, mixed> $fields
140 */
141 private static function requiredString(array $fields, string $name): string {
142 if (!array_key_exists($name, $fields)) {
143 throw new InvalidArgumentException('RedirectUpdate is missing required field: ' . $name);
144 }
145 if (!is_string($fields[$name])) {
146 throw new InvalidArgumentException('RedirectUpdate field must be string: ' . $name);
147 }
148 return $fields[$name];
149 }
150
151 /**
152 * @param array<string, mixed> $fields
153 */
154 private static function requiredInt(array $fields, string $name): int {
155 if (!array_key_exists($name, $fields)) {
156 throw new InvalidArgumentException('RedirectUpdate is missing required field: ' . $name);
157 }
158 if (!is_int($fields[$name]) && !(is_string($fields[$name]) && is_numeric($fields[$name]))) {
159 throw new InvalidArgumentException('RedirectUpdate field must be int: ' . $name);
160 }
161 return (int)$fields[$name];
162 }
163
164 /**
165 * @param array<string, mixed> $fields
166 * @return int|null
167 */
168 private static function nullableInt(array $fields, string $name) {
169 if ($fields[$name] === null) {
170 return null;
171 }
172 if (!is_int($fields[$name]) && !(is_string($fields[$name]) && is_numeric($fields[$name]))) {
173 throw new InvalidArgumentException('RedirectUpdate field must be int or null: ' . $name);
174 }
175 return (int)$fields[$name];
176 }
177
178 public function getId(): int {
179 return $this->id;
180 }
181
182 public function getType(): int {
183 return $this->type;
184 }
185
186 public function getFromUrl(): string {
187 return $this->fromUrl;
188 }
189
190 public function getDestination(): string {
191 return $this->destination;
192 }
193
194 public function getCode(): string {
195 return $this->code;
196 }
197
198 public function getStatusType(): string {
199 return $this->statusType;
200 }
201
202 /** @return int|null */
203 public function getStartTs() {
204 return $this->startTs;
205 }
206
207 /** @return int|null */
208 public function getEndTs() {
209 return $this->endTs;
210 }
211 }
212