Skip to content

Commit 31699ef

Browse files
committed
feat: create payload representation
1 parent 375b35b commit 31699ef

File tree

5 files changed

+298
-16
lines changed

5 files changed

+298
-16
lines changed

src/Core/Fake/Resolver/FromEnum.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,11 @@ public function resolve(ReflectionParameter $parameter, Set $presets): ?Value
3636
/**
3737
* @throws RandomException
3838
*/
39-
private function resolveEnumValue(ReflectionEnum $reflectionEnum, ReflectionParameter $parameter, Set $presets): ?Value
40-
{
39+
private function resolveEnumValue(
40+
ReflectionEnum $reflectionEnum,
41+
ReflectionParameter $parameter,
42+
Set $presets,
43+
): ?Value {
4144
/** @var ReflectionEnumUnitCase[] $enumCases */
4245
$enumCases = $reflectionEnum->getCases();
4346
if (empty($enumCases)) {

src/Support/Payload.php

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Constructo\Support;
6+
7+
use Constructo\Exception\SchemaException;
8+
9+
use function is_array;
10+
11+
readonly class Payload extends Set
12+
{
13+
public static function createFrom(mixed $data): self
14+
{
15+
return new self($data);
16+
}
17+
18+
public function with(string $field, mixed $value): self
19+
{
20+
return new self(array_merge($this->toArray(), [$field => $value]));
21+
}
22+
23+
public function along(array $values): self
24+
{
25+
return new self(array_merge($this->toArray(), $values));
26+
}
27+
28+
public function __get(string $name): mixed
29+
{
30+
if (! $this->has($name)) {
31+
return null;
32+
}
33+
return $this->resolve($name);
34+
}
35+
36+
public function __set(string $name, mixed $value): void
37+
{
38+
throw new SchemaException('Cannot modify payload properties');
39+
}
40+
41+
public function __isset(string $name): bool
42+
{
43+
return $this->has($name);
44+
}
45+
46+
protected function resolve(string $name): mixed
47+
{
48+
$value = $this->get($name);
49+
if (! is_array($value)) {
50+
return $value;
51+
}
52+
$keys = array_keys($value);
53+
$filtered = array_filter($keys, fn (mixed $item) => is_string($item));
54+
if (count($keys) !== count($filtered)) {
55+
return $value;
56+
}
57+
return new self($value);
58+
}
59+
}

src/Support/Set.php

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@
1313
use function array_merge;
1414
use function count;
1515
use function is_array;
16+
use function is_string;
1617

17-
final readonly class Set
18+
readonly class Set
1819
{
1920
/**
2021
* @var array<string, mixed>
@@ -27,15 +28,15 @@ public function __construct(mixed $data = [])
2728
throw new SchemaException('Values must be an array.');
2829
}
2930
$keys = array_keys($data);
30-
$filtered = array_filter($keys, 'is_string');
31+
$filtered = array_filter($keys, fn (mixed $item) => is_string($item));
3132
if (count($keys) !== count($filtered)) {
3233
throw new SchemaException('All keys must be strings.');
3334
}
3435
/* @phpstan-ignore assign.propertyType */
3536
$this->data = $data;
3637
}
3738

38-
public static function createFrom(mixed $data): Set
39+
public static function createFrom(mixed $data): self
3940
{
4041
return new Set($data);
4142
}
@@ -53,9 +54,14 @@ public function at(string $field): mixed
5354
throw new SchemaException(sprintf("Field '%s' not found.", $field));
5455
}
5556

56-
public function with(string $field, mixed $value): Set
57+
public function with(string $field, mixed $value): self
5758
{
58-
return new Set(array_merge($this->toArray(), [$field => $value]));
59+
return new self(array_merge($this->toArray(), [$field => $value]));
60+
}
61+
62+
public function along(array $values): self
63+
{
64+
return new self(array_merge($this->toArray(), $values));
5965
}
6066

6167
/**
@@ -64,12 +70,12 @@ public function with(string $field, mixed $value): Set
6470
public function toArray(): array
6571
{
6672
/* @phpstan-ignore return.type */
67-
return array_map(fn (mixed $item) => $item instanceof Set ? $item->toArray() : $item, $this->data);
68-
}
69-
70-
public function along(array $values): Set
71-
{
72-
return new Set(array_merge($this->toArray(), $values));
73+
return array_map(
74+
fn (mixed $item) => $item instanceof static
75+
? $item->toArray()
76+
: $item,
77+
$this->data
78+
);
7379
}
7480

7581
public function has(string $field): bool

src/Testing/FakerExtension.php

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,22 @@
77
use Constructo\Core\Fake\Faker;
88
use Constructo\Support\Reflective\Notation;
99
use Faker\Generator;
10+
use ReflectionException;
1011

1112
trait FakerExtension
1213
{
1314
private ?Faker $faker = null;
1415

16+
/**
17+
* @throws ReflectionException
18+
* @SuppressWarnings(BooleanArgumentFlag)
19+
*/
1520
protected function faker(
1621
Notation $case = Notation::SNAKE,
1722
array $formatters = [],
1823
?string $locale = null,
1924
bool $ignoreFromDefaultValue = false,
20-
): Faker
21-
{
25+
): Faker {
2226
if ($this->faker === null) {
2327
$args = [
2428
'case' => $case,
@@ -31,9 +35,13 @@ protected function faker(
3135
return $this->faker;
3236
}
3337

38+
/**
39+
* @throws ReflectionException
40+
*/
3441
protected function generator(): Generator
3542
{
36-
return $this->faker()->generator();
43+
return $this->faker()
44+
->generator();
3745
}
3846

3947
/**

tests/Support/PayloadTest.php

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Constructo\Test\Support;
6+
7+
use Constructo\Exception\SchemaException;
8+
use Constructo\Support\Payload;
9+
use PHPUnit\Framework\TestCase;
10+
11+
final class PayloadTest extends TestCase
12+
{
13+
public function testCreateFromArray(): void
14+
{
15+
$payload = Payload::createFrom(['key' => 'value']);
16+
$this->assertEquals('value', $payload->get('key'));
17+
}
18+
19+
public function testMagicGetExistingKey(): void
20+
{
21+
$payload = new Payload(['key' => 'value']);
22+
$this->assertEquals('value', $payload->key);
23+
}
24+
25+
public function testMagicGetNonExistingKey(): void
26+
{
27+
$payload = new Payload(['key' => 'value']);
28+
$this->assertNull($payload->non_existing_key);
29+
}
30+
31+
public function testMagicIssetExistingKey(): void
32+
{
33+
$payload = new Payload(['key' => 'value']);
34+
$this->assertTrue(isset($payload->key));
35+
}
36+
37+
public function testMagicIssetNonExistingKey(): void
38+
{
39+
$payload = new Payload(['key' => 'value']);
40+
$this->assertFalse(isset($payload->non_existing_key));
41+
}
42+
43+
public function testMagicSetThrowsException(): void
44+
{
45+
$payload = new Payload(['key' => 'value']);
46+
$this->expectException(SchemaException::class);
47+
$this->expectExceptionMessage('Cannot modify payload properties');
48+
49+
// Use a closure to test the exception since readonly classes don't allow dynamic properties
50+
$test = function() use ($payload) {
51+
$payload->__set('key', 'new_value');
52+
};
53+
$test();
54+
}
55+
56+
public function testResolveWithScalarValue(): void
57+
{
58+
$payload = new Payload(['key' => 'value']);
59+
$this->assertEquals('value', $payload->key);
60+
}
61+
62+
public function testResolveWithNestedArrayCreatesPayload(): void
63+
{
64+
$payload = new Payload(['nested' => ['inner_key' => 'inner_value']]);
65+
$nested = $payload->nested;
66+
$this->assertInstanceOf(Payload::class, $nested);
67+
$this->assertEquals('inner_value', $nested->inner_key);
68+
}
69+
70+
public function testResolveWithArrayWithNonStringKeysReturnsArray(): void
71+
{
72+
$payload = new Payload(['mixed' => ['string_key' => 'value', 0 => 'indexed']]);
73+
$mixed = $payload->mixed;
74+
$this->assertIsArray($mixed);
75+
$this->assertEquals('value', $mixed['string_key']);
76+
$this->assertEquals('indexed', $mixed[0]);
77+
}
78+
79+
public function testNestedPayloadAccess(): void
80+
{
81+
$payload = new Payload([
82+
'level1' => [
83+
'level2' => [
84+
'level3' => 'deep_value'
85+
]
86+
]
87+
]);
88+
$this->assertEquals('deep_value', $payload->level1->level2->level3);
89+
}
90+
91+
public function testInheritedGetMethod(): void
92+
{
93+
$payload = new Payload(['key' => 'value']);
94+
$this->assertEquals('value', $payload->get('key'));
95+
$this->assertNull($payload->get('non_existing_key'));
96+
}
97+
98+
public function testInheritedAtMethod(): void
99+
{
100+
$payload = new Payload(['key' => 'value']);
101+
$this->assertEquals('value', $payload->at('key'));
102+
}
103+
104+
public function testInheritedAtMethodThrowsException(): void
105+
{
106+
$payload = new Payload(['key' => 'value']);
107+
$this->expectException(SchemaException::class);
108+
$this->expectExceptionMessage("Field 'non_existing_key' not found.");
109+
$payload->at('non_existing_key');
110+
}
111+
112+
public function testInheritedWithMethod(): void
113+
{
114+
$payload = new Payload(['key' => 'value']);
115+
$newPayload = $payload->with('new_key', 'new_value');
116+
$this->assertInstanceOf(Payload::class, $newPayload);
117+
$this->assertEquals('new_value', $newPayload->get('new_key'));
118+
$this->assertEquals('value', $newPayload->get('key'));
119+
}
120+
121+
public function testInheritedAlongMethod(): void
122+
{
123+
$payload = new Payload(['key' => 'value']);
124+
$newPayload = $payload->along(['new_key' => 'new_value']);
125+
$this->assertInstanceOf(Payload::class, $newPayload);
126+
$this->assertEquals('new_value', $newPayload->get('new_key'));
127+
$this->assertEquals('value', $newPayload->get('key'));
128+
}
129+
130+
public function testInheritedHasMethod(): void
131+
{
132+
$payload = new Payload(['key' => 'value']);
133+
$this->assertTrue($payload->has('key'));
134+
$this->assertFalse($payload->has('non_existing_key'));
135+
}
136+
137+
public function testInheritedHasNotMethod(): void
138+
{
139+
$payload = new Payload(['key' => 'value']);
140+
$this->assertFalse($payload->hasNot('key'));
141+
$this->assertTrue($payload->hasNot('non_existing_key'));
142+
}
143+
144+
public function testInheritedToArrayMethod(): void
145+
{
146+
$payload = new Payload(['key' => 'value', 'nested' => ['inner' => 'inner_value']]);
147+
$array = $payload->toArray();
148+
$this->assertIsArray($array);
149+
$this->assertEquals('value', $array['key']);
150+
$this->assertIsArray($array['nested']);
151+
$this->assertEquals('inner_value', $array['nested']['inner']);
152+
}
153+
154+
public function testInvalidValuesArray(): void
155+
{
156+
$this->expectException(SchemaException::class);
157+
$this->expectExceptionMessage('Values must be an array.');
158+
new Payload('invalid');
159+
}
160+
161+
public function testInvalidKeysInArray(): void
162+
{
163+
$this->expectException(SchemaException::class);
164+
$this->expectExceptionMessage('All keys must be strings.');
165+
new Payload(['value', 5 => 'foo', 'key' => 'value']);
166+
}
167+
168+
public function testResolveWithNullValue(): void
169+
{
170+
$payload = new Payload(['null_key' => null]);
171+
$this->assertNull($payload->null_key);
172+
}
173+
174+
public function testResolveWithEmptyArray(): void
175+
{
176+
$payload = new Payload(['empty_array' => []]);
177+
$resolved = $payload->empty_array;
178+
$this->assertInstanceOf(Payload::class, $resolved);
179+
}
180+
181+
public function testMagicMethodsWithComplexData(): void
182+
{
183+
$data = [
184+
'user' => [
185+
'name' => 'John Doe',
186+
'email' => 'john@example.com',
187+
'profile' => [
188+
'age' => 30,
189+
'settings' => [
190+
'theme' => 'dark',
191+
'notifications' => true
192+
]
193+
]
194+
]
195+
];
196+
197+
$payload = new Payload($data);
198+
199+
$this->assertTrue(isset($payload->user));
200+
$this->assertEquals('John Doe', $payload->user->name);
201+
$this->assertEquals('john@example.com', $payload->user->email);
202+
$this->assertEquals(30, $payload->user->profile->age);
203+
$this->assertEquals('dark', $payload->user->profile->settings->theme);
204+
$this->assertTrue($payload->user->profile->settings->notifications);
205+
}
206+
}

0 commit comments

Comments
 (0)