1: <?php declare(strict_types=1);
2:
3: namespace Salient\Core;
4:
5: use Salient\Contract\Core\ConvertibleEnumerationInterface;
6: use Salient\Utility\Inflect;
7: use Salient\Utility\Str;
8: use InvalidArgumentException;
9: use LogicException;
10:
11: /**
12: * Base class for enumerations that use reflection to map constants to and from
13: * their names
14: *
15: * @api
16: *
17: * @template TValue of array-key
18: *
19: * @extends AbstractEnumeration<TValue>
20: * @implements ConvertibleEnumerationInterface<TValue>
21: */
22: abstract class AbstractReflectiveEnumeration extends AbstractEnumeration implements ConvertibleEnumerationInterface
23: {
24: /**
25: * Class name => [ constant value => name ]
26: *
27: * @var array<string,array<TValue,string>>
28: */
29: private static $NameMaps = [];
30:
31: /**
32: * Class name => [ constant name => value ]
33: *
34: * @var array<string,array<string,TValue>>
35: */
36: private static $ValueMaps = [];
37:
38: /**
39: * @inheritDoc
40: */
41: final public static function fromName(string $name)
42: {
43: self::checkMaps();
44:
45: $value = self::$ValueMaps[static::class][$name]
46: ?? self::$ValueMaps[static::class][Str::upper($name)]
47: ?? null;
48:
49: if ($value === null) {
50: throw new InvalidArgumentException(
51: sprintf('Invalid name: %s', $name)
52: );
53: }
54:
55: /** @var TValue */
56: return $value;
57: }
58:
59: /**
60: * @inheritDoc
61: */
62: final public static function fromNames(array $names): array
63: {
64: self::checkMaps();
65:
66: $invalid = [];
67: foreach ($names as $name) {
68: $value = self::$ValueMaps[static::class][$name]
69: ?? self::$ValueMaps[static::class][Str::upper($name)]
70: ?? null;
71:
72: if ($value === null) {
73: $invalid[] = $name;
74: continue;
75: }
76:
77: $values[] = $value;
78: }
79:
80: if ($invalid) {
81: throw new InvalidArgumentException(
82: Inflect::format($invalid, 'Invalid {{#:name}}: %s', implode(',', $invalid))
83: );
84: }
85:
86: /** @var TValue[] */
87: return $values ?? [];
88: }
89:
90: /**
91: * @inheritDoc
92: */
93: final public static function toName($value): string
94: {
95: self::checkMaps();
96:
97: $name = self::$NameMaps[static::class][$value] ?? null;
98:
99: if ($name === null) {
100: throw new InvalidArgumentException(
101: sprintf('Invalid value: %s', $value)
102: );
103: }
104:
105: return $name;
106: }
107:
108: /**
109: * @inheritDoc
110: */
111: final public static function toNames(array $values): array
112: {
113: self::checkMaps();
114:
115: $invalid = [];
116: foreach ($values as $value) {
117: $name = self::$NameMaps[static::class][$value] ?? null;
118:
119: if ($name === null) {
120: $invalid[] = $value;
121: continue;
122: }
123:
124: $names[] = $name;
125: }
126:
127: if ($invalid) {
128: throw new InvalidArgumentException(
129: Inflect::format($invalid, 'Invalid {{#:value}}: %s', implode(',', $invalid))
130: );
131: }
132:
133: return $names ?? [];
134: }
135:
136: /**
137: * @inheritDoc
138: */
139: final public static function cases(): array
140: {
141: self::checkMaps();
142:
143: return self::$ValueMaps[static::class];
144: }
145:
146: /**
147: * @inheritDoc
148: */
149: final public static function hasValue($value): bool
150: {
151: self::checkMaps();
152:
153: return isset(self::$NameMaps[static::class][$value]);
154: }
155:
156: private static function checkMaps(): void
157: {
158: if (isset(self::$NameMaps[static::class])) {
159: return;
160: }
161:
162: $valueMap = [];
163: $nameMap = [];
164: /** @var mixed $value */
165: foreach (self::constants() as $name => $value) {
166: if (!is_int($value) && !is_string($value)) {
167: throw new LogicException(sprintf(
168: 'Public constant is not of type int|string: %s::%s',
169: static::class,
170: $name,
171: ));
172: }
173:
174: $valueMap[$name] = $value;
175: $nameMap[$value] = $name;
176: }
177:
178: if (!$valueMap) {
179: self::$ValueMaps[static::class] = [];
180: self::$NameMaps[static::class] = [];
181: return;
182: }
183:
184: if (count($valueMap) !== count($nameMap)) {
185: throw new LogicException(sprintf(
186: 'Public constants do not have unique values: %s',
187: static::class,
188: ));
189: }
190:
191: // Add UPPER_CASE names to $valueMap if not already present
192: $valueMap += array_change_key_case($valueMap, \CASE_UPPER);
193:
194: self::$ValueMaps[static::class] = $valueMap;
195: self::$NameMaps[static::class] = $nameMap;
196: }
197: }
198: