Skip to content

Commit 8c3b6fc

Browse files
fix: populate enum-typed properties with enum instances
1 parent 7d947c2 commit 8c3b6fc

2 files changed

Lines changed: 79 additions & 3 deletions

File tree

src/Core/Conversion/EnumOf.php

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,12 @@ final class EnumOf implements Converter
1919

2020
/**
2121
* @param list<bool|float|int|string|null> $members
22+
* @param class-string<\BackedEnum>|null $class
2223
*/
23-
public function __construct(private readonly array $members)
24-
{
24+
public function __construct(
25+
private readonly array $members,
26+
private readonly ?string $class = null,
27+
) {
2528
$type = 'NULL';
2629
foreach ($this->members as $member) {
2730
$type = gettype($member);
@@ -33,13 +36,24 @@ public function __construct(private readonly array $members)
3336
public static function fromBackedEnum(string $enum): self
3437
{
3538
// @phpstan-ignore-next-line argument.type
36-
return self::$cache[$enum] ??= new self(array_column($enum::cases(), column_key: 'value'));
39+
return self::$cache[$enum] ??= new self(
40+
array_column($enum::cases(), column_key: 'value'),
41+
class: $enum,
42+
);
3743
}
3844

3945
public function coerce(mixed $value, CoerceState $state): mixed
4046
{
4147
$this->tally($value, state: $state);
4248

49+
if ($value instanceof \BackedEnum) {
50+
return $value;
51+
}
52+
53+
if (null !== $this->class && (is_int($value) || is_string($value))) {
54+
return ($this->class)::tryFrom($value) ?? $value;
55+
}
56+
4357
return $value;
4458
}
4559

tests/Core/ModelTest.php

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,30 @@ public function __construct(
4747
}
4848
}
4949

50+
enum TicketPriority: string
51+
{
52+
case Low = 'low';
53+
case High = 'high';
54+
}
55+
56+
class Ticket implements BaseModel
57+
{
58+
/** @use SdkModel<array<string, mixed>> */
59+
use SdkModel;
60+
61+
#[Required(enum: TicketPriority::class)]
62+
public TicketPriority $priority;
63+
64+
/** @var list<TicketPriority> */
65+
#[Required(list: TicketPriority::class)]
66+
public array $labels;
67+
68+
public function __construct()
69+
{
70+
$this->initialize();
71+
}
72+
}
73+
5074
/**
5175
* @internal
5276
*
@@ -141,4 +165,42 @@ public function testSerializeModelWithExplicitNull(): void
141165
json_encode($model)
142166
);
143167
}
168+
169+
#[Test]
170+
public function testScalarEnumCoercesToInstance(): void
171+
{
172+
$model = Ticket::fromArray(['priority' => 'low', 'labels' => []]);
173+
$this->assertSame(TicketPriority::Low, $model->priority);
174+
}
175+
176+
#[Test]
177+
public function testListOfEnumCoercesElementsToInstances(): void
178+
{
179+
$model = Ticket::fromArray(['priority' => 'low', 'labels' => ['low', 'high']]);
180+
$this->assertCount(2, $model->labels);
181+
$this->assertSame(TicketPriority::Low, $model->labels[0]);
182+
$this->assertSame(TicketPriority::High, $model->labels[1]);
183+
}
184+
185+
#[Test]
186+
public function testEnumInstancePassesThrough(): void
187+
{
188+
$model = Ticket::fromArray(['priority' => TicketPriority::High, 'labels' => []]);
189+
$this->assertSame(TicketPriority::High, $model->priority);
190+
}
191+
192+
#[Test]
193+
public function testInvalidEnumScalarFallsBackToData(): void
194+
{
195+
$model = Ticket::fromArray(['priority' => 'urgent', 'labels' => []]);
196+
$this->assertSame('urgent', $model['priority']);
197+
}
198+
199+
#[Test]
200+
public function testEnumWireFormatStableAcrossConstruction(): void
201+
{
202+
$fromScalar = Ticket::fromArray(['priority' => 'low', 'labels' => ['high']]);
203+
$fromInstance = Ticket::fromArray(['priority' => TicketPriority::Low, 'labels' => [TicketPriority::High]]);
204+
$this->assertSame(json_encode($fromScalar), json_encode($fromInstance));
205+
}
144206
}

0 commit comments

Comments
 (0)