-
Notifications
You must be signed in to change notification settings - Fork 3.6k
Expand file tree
/
Copy pathAbstractEnum.php
More file actions
349 lines (347 loc) · 11.1 KB
/
Copy pathAbstractEnum.php
File metadata and controls
349 lines (347 loc) · 11.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
<?php
declare (strict_types=1);
namespace WordPress\AiClient\Common;
use BadMethodCallException;
use JsonSerializable;
use ReflectionClass;
use WordPress\AiClient\Common\Exception\InvalidArgumentException;
use WordPress\AiClient\Common\Exception\RuntimeException;
/**
* Abstract base class for enum-like behavior in PHP 7.4.
*
* This class provides enum-like functionality for PHP versions that don't support native enums.
* Child classes should define uppercase snake_case constants for enum values.
*
* @example
* class PersonEnum extends AbstractEnum {
* public const FIRST_NAME = 'first';
* public const LAST_NAME = 'last';
* }
*
* // Usage:
* $enum = PersonEnum::from('first'); // Creates instance with value 'first'
* $enum = PersonEnum::tryFrom('invalid'); // Returns null
* $enum = PersonEnum::firstName(); // Creates instance with value 'first'
* $enum->name; // 'FIRST_NAME'
* $enum->value; // 'first'
* $enum->equals('first'); // Returns true
* $enum->is(PersonEnum::firstName()); // Returns true
* PersonEnum::cases(); // Returns array of all enum instances
*
* @property-read string $value The value of the enum instance.
* @property-read string $name The name of the enum constant.
*
* @since 0.1.0
*/
abstract class AbstractEnum implements JsonSerializable
{
/**
* @var string The value of the enum instance.
*/
private string $value;
/**
* @var string The name of the enum constant.
*/
private string $name;
/**
* @var array<string, array<string, string>> Cache for reflection data.
*/
private static array $cache = [];
/**
* @var array<string, array<string, self>> Cache for enum instances.
*/
private static array $instances = [];
/**
* Constructor is private to ensure instances are created through static methods.
*
* @since 0.1.0
*
* @param string $value The enum value.
* @param string $name The constant name.
*/
final private function __construct(string $value, string $name)
{
$this->value = $value;
$this->name = $name;
}
/**
* Provides read-only access to properties.
*
* @since 0.1.0
*
* @param string $property The property name.
* @return mixed The property value.
* @throws BadMethodCallException If property doesn't exist.
*/
final public function __get(string $property)
{
if ($property === 'value' || $property === 'name') {
return $this->{$property};
}
throw new BadMethodCallException(sprintf('Property %s::%s does not exist', static::class, $property));
}
/**
* Prevents property modification.
*
* @since 0.1.0
*
* @param string $property The property name.
* @param mixed $value The value to set.
* @throws BadMethodCallException Always, as enum properties are read-only.
*/
final public function __set(string $property, $value): void
{
throw new BadMethodCallException(sprintf('Cannot modify property %s::%s - enum properties are read-only', static::class, $property));
}
/**
* Creates an enum instance from a value, throws exception if invalid.
*
* @since 0.1.0
*
* @param string $value The enum value.
* @return static The enum instance.
* @throws InvalidArgumentException If the value is not valid.
*/
final public static function from(string $value): self
{
$instance = self::tryFrom($value);
if ($instance === null) {
throw new InvalidArgumentException(sprintf('%s is not a valid backing value for enum %s', $value, static::class));
}
return $instance;
}
/**
* Tries to create an enum instance from a value, returns null if invalid.
*
* @since 0.1.0
*
* @param string $value The enum value.
* @return static|null The enum instance or null.
*/
final public static function tryFrom(string $value): ?self
{
$constants = static::getConstants();
foreach ($constants as $name => $constantValue) {
if ($constantValue === $value) {
return self::getInstance($constantValue, $name);
}
}
return null;
}
/**
* Gets all enum cases.
*
* @since 0.1.0
*
* @return static[] Array of all enum instances.
*/
final public static function cases(): array
{
$cases = [];
$constants = static::getConstants();
foreach ($constants as $name => $value) {
$cases[] = self::getInstance($value, $name);
}
return $cases;
}
/**
* Checks if this enum has the same value as the given value.
*
* @since 0.1.0
*
* @param string|self $other The value or enum to compare.
* @return bool True if values are equal.
*/
final public function equals($other): bool
{
if ($other instanceof self) {
return $this->is($other);
}
return $this->value === $other;
}
/**
* Checks if this enum is the same instance type and value as another enum.
*
* @since 0.1.0
*
* @param self $other The other enum to compare.
* @return bool True if enums are identical.
*/
final public function is(self $other): bool
{
return $this === $other;
// Since we're using singletons, we can use identity comparison
}
/**
* Gets all valid values for this enum.
*
* @since 0.1.0
*
* @return string[] List of all enum values.
*/
final public static function getValues(): array
{
return array_values(static::getConstants());
}
/**
* Checks if a value is valid for this enum.
*
* @since 0.1.0
*
* @param string $value The value to check.
* @return bool True if value is valid.
*/
final public static function isValidValue(string $value): bool
{
return in_array($value, self::getValues(), \true);
}
/**
* Gets or creates a singleton instance for the given value and name.
*
* @since 0.1.0
*
* @param string $value The enum value.
* @param string $name The constant name.
* @return static The enum instance.
*/
private static function getInstance(string $value, string $name): self
{
$className = static::class;
if (!isset(self::$instances[$className])) {
self::$instances[$className] = [];
}
if (!isset(self::$instances[$className][$name])) {
$instance = new $className($value, $name);
self::$instances[$className][$name] = $instance;
}
/** @var static */
return self::$instances[$className][$name];
}
/**
* Gets all constants for this enum class.
*
* @since 0.1.0
*
* @return array<string, string> Map of constant names to values.
* @throws RuntimeException If invalid constant found.
*/
final protected static function getConstants(): array
{
$className = static::class;
if (!isset(self::$cache[$className])) {
self::$cache[$className] = static::determineClassEnumerations($className);
}
return self::$cache[$className];
}
/**
* Determines the class enumerations by reflecting on class constants.
*
* This method can be overridden by subclasses to customize how
* enumerations are determined (e.g., to add dynamic constants).
*
* @since 0.1.0
*
* @param class-string $className The fully qualified class name.
* @return array<string, string> Map of constant names to values.
* @throws RuntimeException If invalid constant found.
*/
protected static function determineClassEnumerations(string $className): array
{
$reflection = new ReflectionClass($className);
$constants = $reflection->getConstants();
// Validate all constants
$enumConstants = [];
foreach ($constants as $name => $value) {
// Check if constant name follows uppercase snake_case pattern
if (!preg_match('/^[A-Z][A-Z0-9_]*$/', $name)) {
throw new RuntimeException(sprintf('Invalid enum constant name "%s" in %s. Constants must be UPPER_SNAKE_CASE.', $name, $className));
}
// Check if value is valid type
if (!is_string($value)) {
throw new RuntimeException(sprintf('Invalid enum value type for constant %s::%s. ' . 'Only string values are allowed, %s given.', $className, $name, gettype($value)));
}
$enumConstants[$name] = $value;
}
return $enumConstants;
}
/**
* Handles dynamic method calls for enum checking.
*
* @since 0.1.0
*
* @param string $name The method name.
* @param array<mixed> $arguments The method arguments.
* @return bool True if the enum value matches.
* @throws BadMethodCallException If the method doesn't exist.
*/
final public function __call(string $name, array $arguments): bool
{
// Handle is* methods
if (str_starts_with($name, 'is')) {
$constantName = self::camelCaseToConstant(substr($name, 2));
$constants = static::getConstants();
if (isset($constants[$constantName])) {
return $this->value === $constants[$constantName];
}
}
throw new BadMethodCallException(sprintf('Method %s::%s does not exist', static::class, $name));
}
/**
* Handles static method calls for enum creation.
*
* @since 0.1.0
*
* @param string $name The method name.
* @param array<mixed> $arguments The method arguments.
* @return static The enum instance.
* @throws BadMethodCallException If the method doesn't exist.
*/
final public static function __callStatic(string $name, array $arguments): self
{
$constantName = self::camelCaseToConstant($name);
$constants = static::getConstants();
if (isset($constants[$constantName])) {
return self::getInstance($constants[$constantName], $constantName);
}
throw new BadMethodCallException(sprintf('Method %s::%s does not exist', static::class, $name));
}
/**
* Converts camelCase to CONSTANT_CASE.
*
* @since 0.1.0
*
* @param string $camelCase The camelCase string.
* @return string The CONSTANT_CASE version.
*/
private static function camelCaseToConstant(string $camelCase): string
{
$snakeCase = preg_replace('/([a-z])([A-Z])/', '$1_$2', $camelCase);
if ($snakeCase === null) {
return strtoupper($camelCase);
}
return strtoupper($snakeCase);
}
/**
* Returns string representation of the enum.
*
* @since 0.1.0
*
* @return string The enum value.
*/
final public function __toString(): string
{
return $this->value;
}
/**
* Converts the enum to a JSON-serializable format.
*
* @since 0.1.0
*
* @return string The enum value.
*/
#[\ReturnTypeWillChange]
public function jsonSerialize()
{
return $this->value;
}
}