Skip to content

Commit 681b49d

Browse files
committed
fix: include property with default values in the returned array
1 parent 49c3a3e commit 681b49d

File tree

6 files changed

+131
-52
lines changed

6 files changed

+131
-52
lines changed

README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,9 @@ $data->toArray(); // ['email' => 'alice@company.tld', 'password' => 'SoSecureWow
203203

204204
$data->compact()->toArray(); // ['email' => 'alice@company.tld', 'password' => 'SoSecureWow']
205205
```
206-
* `DataTransferObject::get(string $name): mixed` returns the value of `$name` property. If `$name` doesn't exist, an exception will be thrown.
206+
* `DataTransferObject::get(string $name, $default = null): mixed` returns the value of `$name` property.
207+
If `$name` doesn't exist in the class definition, an exception will be thrown. If `$name` exists but not initialized, `$default` will be returned.
208+
> Important: PHP treats non-typed properties e.g., `public $prop` as **initialized with NULL**.
207209
```php
208210
$data = UserCreationData::make([
209211
'email' => 'alice@company.tld',
@@ -212,8 +214,12 @@ $data->toArray(); // ['email' => 'alice@company.tld', 'password' => 'SoSecureWow
212214

213215
$data->get('email'); // 'alice@company.tld'
214216
$data->password; // 'SoSecureWow'
217+
218+
$data->age; // throws "UserCreationData::$age must not be accessed before initialization."
219+
$data->get('age', 30); // 30
215220

216-
$data->nope; // throws "Public property $nope does not exist in class UserCreationData"
221+
$data->get('nope'); // throws "Public property $nope does not exist in class UserCreationData."
222+
$data->nope; // throws "Public property $nope does not exist in class UserCreationData."
217223
```
218224

219225
## Differences from spatie/data-transfer-object

src/DataTransferObject.php

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ private function __construct(array $parameters = [])
1515
{
1616
foreach (static::getAssignableProperties() as $property) {
1717
$this->propertyNames[] = $property->getName();
18+
19+
if ($property->isInitialized($this)) {
20+
$this->data[$property->getName()] = $property->getValue($this);
21+
}
22+
1823
unset($this->{$property->getName()});
1924
}
2025

@@ -44,13 +49,12 @@ public function set($name, $value = null): self
4449
return $this;
4550
}
4651

47-
/**
48-
* @param $name
49-
* @return mixed
50-
*/
51-
public function get($name)
52+
/** @return mixed */
53+
public function get(string $name, $default = null)
5254
{
53-
return $this->{$name};
55+
$this->assertPropertyExists($name);
56+
57+
return array_key_exists($name, $this->data) ? $this->data[$name] : $default;
5458
}
5559

5660
/** @return static */
@@ -138,6 +142,13 @@ private function assertPropertyExists(string $name): void
138142
}
139143
}
140144

145+
private function assertPropertyInitialized(string $name): void
146+
{
147+
if (!array_key_exists($name, $this->data)) {
148+
throw DataTransferObjectException::propertyNotInitialized(static::class, $name);
149+
}
150+
}
151+
141152
public function __set($name, $value): void
142153
{
143154
$this->assertPropertyExists($name);
@@ -153,6 +164,7 @@ public function __unset($name): void
153164
public function __get($name)
154165
{
155166
$this->assertPropertyExists($name);
167+
$this->assertPropertyInitialized($name);
156168

157169
return $this->data[$name];
158170
}

src/DataTransferObjectException.php

Lines changed: 4 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
namespace Eve\DTO;
44

55
use Exception;
6-
use ReflectionProperty;
76
use Throwable;
87

98
class DataTransferObjectException extends Exception
@@ -13,33 +12,13 @@ private function __construct($message = '', $code = 0, ?Throwable $previous = nu
1312
parent::__construct($message, $code, $previous);
1413
}
1514

16-
public static function invalidType(ReflectionProperty $targetProperty, string $type, array $allowedTypes): self
15+
public static function nonexistentProperty(string $class, string $propertyName): self
1716
{
18-
if (count($allowedTypes) === 1) {
19-
return new static(
20-
sprintf(
21-
'%s::$%s must be of type %s, received a value of type %s.',
22-
$targetProperty->class,
23-
$targetProperty->name,
24-
$allowedTypes[0],
25-
$type
26-
)
27-
);
28-
}
29-
30-
return new static(
31-
sprintf(
32-
'%s::$%s must be one of these types: %s; received a value of type %s.',
33-
$targetProperty->class,
34-
$targetProperty->name,
35-
implode(', ', $allowedTypes),
36-
$type
37-
)
38-
);
17+
return new static(sprintf('Public property $%s does not exist in class %s.', $propertyName, $class));
3918
}
4019

41-
public static function nonexistentProperty(string $class, string $propertyName): self
20+
public static function propertyNotInitialized(string $class, string $propertyName)
4221
{
43-
return new static(sprintf('Public property $%s does not exist in class %s.', $propertyName, $class));
22+
return new static(sprintf('%s::$%s must not be accessed before initialization.', $class, $propertyName));
4423
}
4524
}

tests/Fixtures/SampleData.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ class SampleData extends DataTransferObject
88
{
99
public string $simple_prop;
1010
public ?string $nullable_prop;
11-
public $mixed_prop;
11+
public string $initialized_prop = 'Initialized';
1212
public array $array_prop;
1313
public Foo $object_prop;
1414
public NestedData $nested;

tests/Fixtures/UntypedData.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
namespace Tests\Fixtures;
4+
5+
use Eve\DTO\DataTransferObject;
6+
7+
class UntypedData extends DataTransferObject
8+
{
9+
public $foo_prop = 'Foo';
10+
public $null_prop;
11+
}

tests/Unit/DataTransferObjectTest.php

Lines changed: 89 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Tests\Fixtures\Foo;
88
use Tests\Fixtures\NestedData;
99
use Tests\Fixtures\SampleData;
10+
use Tests\Fixtures\UntypedData;
1011

1112
class DataTransferObjectTest extends TestCase
1213
{
@@ -17,17 +18,26 @@ public function testSimpleProperty(): void
1718
$data->nullable_prop = 'bar';
1819

1920
self::assertEquals([
21+
'initialized_prop' => 'Initialized',
2022
'simple_prop' => 'foo',
2123
'nullable_prop' => 'bar',
2224
], $data->toArray());
2325
}
2426

27+
public function testInitializedProperty(): void
28+
{
29+
self::assertEquals(['initialized_prop' => 'Initialized'], SampleData::make()->toArray());
30+
}
31+
2532
public function testArrayProperty(): void
2633
{
2734
$data = SampleData::make();
2835
$data->array_prop = ['foo' => 'bar'];
2936

30-
self::assertEquals(['array_prop' => ['foo' => 'bar']], $data->toArray());
37+
self::assertEquals([
38+
'initialized_prop' => 'Initialized',
39+
'array_prop' => ['foo' => 'bar'],
40+
], $data->toArray());
3141
}
3242

3343
public function testObjectProperty(): void
@@ -36,7 +46,10 @@ public function testObjectProperty(): void
3646
$data = SampleData::make();
3747
$data->object_prop = $foo;
3848

39-
self::assertEquals(['object_prop' => $foo], $data->toArray());
49+
self::assertEquals([
50+
'initialized_prop' => 'Initialized',
51+
'object_prop' => $foo,
52+
], $data->toArray());
4053
}
4154

4255
public function testSettingNonExistentPropertyThrows(): void
@@ -52,7 +65,10 @@ public function testSet(): void
5265
$data = SampleData::make();
5366
$data->set('simple_prop', 'foo');
5467

55-
self::assertSame(['simple_prop' => 'foo'], $data->toArray());
68+
self::assertSame([
69+
'initialized_prop' => 'Initialized',
70+
'simple_prop' => 'foo',
71+
], $data->toArray());
5672
}
5773

5874
public function testSetArray(): void
@@ -64,6 +80,7 @@ public function testSetArray(): void
6480
]);
6581

6682
self::assertSame([
83+
'initialized_prop' => 'Initialized',
6784
'simple_prop' => 'foo',
6885
'nullable_prop' => 'bar',
6986
], $data->toArray());
@@ -78,11 +95,15 @@ public function testBuiltinUnset(): void
7895
self::assertEquals([
7996
'simple_prop' => 'foo',
8097
'nullable_prop' => 'bar',
98+
'initialized_prop' => 'Initialized',
8199
], $data->toArray());
82100

83101
unset($data->nullable_prop);
84102

85-
self::assertEquals(['simple_prop' => 'foo'], $data->toArray());
103+
self::assertEquals([
104+
'simple_prop' => 'foo',
105+
'initialized_prop' => 'Initialized',
106+
], $data->toArray());
86107
}
87108

88109
public function testUnset(): void
@@ -92,13 +113,17 @@ public function testUnset(): void
92113
$data->nullable_prop = 'bar';
93114

94115
self::assertEquals([
116+
'initialized_prop' => 'Initialized',
95117
'simple_prop' => 'foo',
96118
'nullable_prop' => 'bar',
97119
], $data->toArray());
98120

99121
$data->unset('nullable_prop');
100122

101-
self::assertEquals(['simple_prop' => 'foo'], $data->toArray());
123+
self::assertEquals([
124+
'simple_prop' => 'foo',
125+
'initialized_prop' => 'Initialized',
126+
], $data->toArray());
102127
}
103128

104129
public function testUnsetSpread(): void
@@ -110,9 +135,10 @@ public function testUnsetSpread(): void
110135
self::assertEquals([
111136
'simple_prop' => 'foo',
112137
'nullable_prop' => 'bar',
138+
'initialized_prop' => 'Initialized',
113139
], $data->toArray());
114140

115-
$data->unset('nullable_prop', 'simple_prop');
141+
$data->unset('nullable_prop', 'simple_prop', 'initialized_prop');
116142

117143
self::assertEquals([], $data->toArray());
118144
}
@@ -133,6 +159,7 @@ public function testMake(): void
133159
]);
134160

135161
self::assertEquals([
162+
'initialized_prop' => 'Initialized',
136163
'simple_prop' => 'foo',
137164
'nullable_prop' => 'bar',
138165
], $data->toArray());
@@ -144,7 +171,10 @@ public function testCompact(): void
144171
$data->simple_prop = 'foo';
145172
$data->nullable_prop = null;
146173

147-
self::assertEquals(['simple_prop' => 'foo'], $data->compact()->toArray());
174+
self::assertEquals([
175+
'initialized_prop' => 'Initialized',
176+
'simple_prop' => 'foo',
177+
], $data->compact()->toArray());
148178
}
149179

150180
public function testOnly(): void
@@ -162,54 +192,61 @@ public function testOnlySpread(): void
162192
$data = SampleData::make([
163193
'simple_prop' => 'foo',
164194
'nullable_prop' => 'bar',
165-
'mixed_prop' => 'baz',
166195
]);
167196

168197
self::assertEquals([
169198
'simple_prop' => 'foo',
170-
'mixed_prop' => 'baz',
171-
], $data->only('simple_prop', 'mixed_prop')->toArray());
199+
'initialized_prop' => 'Initialized',
200+
], $data->only('simple_prop', 'initialized_prop')->toArray());
172201
}
173202

174203
public function testExcept(): void
175204
{
176205
$data = SampleData::make([
177206
'simple_prop' => 'foo',
178207
'nullable_prop' => 'bar',
179-
'mixed_prop' => 'baz',
180208
]);
181209

182210
self::assertEquals([
183211
'simple_prop' => 'foo',
184212
'nullable_prop' => 'bar',
185-
], $data->except('mixed_prop')->toArray());
213+
], $data->except('initialized_prop')->toArray());
186214
}
187215

188216
public function testExceptSpread(): void
189217
{
190218
$data = SampleData::make([
191219
'simple_prop' => 'foo',
192220
'nullable_prop' => 'bar',
193-
'mixed_prop' => 'baz',
194221
]);
195222

196-
self::assertEquals(['simple_prop' => 'foo'], $data->except('mixed_prop', 'nullable_prop')->toArray());
223+
self::assertEquals(
224+
['initialized_prop' => 'Initialized'],
225+
$data->except('simple_prop', 'nullable_prop')->toArray()
226+
);
197227
}
198228

199229
public function testNestedDTO(): void
200230
{
201231
$data = SampleData::make();
202232
$data->nested = NestedData::make(['sample_prop' => 'sample']);
203233

204-
self::assertEquals(['nested' => ['sample_prop' => 'sample']], $data->compact()->toArray());
234+
self::assertEquals([
235+
'nested' => ['sample_prop' => 'sample'],
236+
'initialized_prop' => 'Initialized',
237+
], $data->compact()->toArray());
205238
}
206239

207-
public function testPropertyAccess(): void
240+
public function testPropertyAccessViaGet(): void
208241
{
209242
$data = SampleData::make(['simple_prop' => 'foo']);
210-
243+
211244
self::assertSame('foo', $data->get('simple_prop'));
212-
self::assertSame('foo', $data->simple_prop);
245+
}
246+
247+
public function testPropertyAccessViaGetWithDefault(): void
248+
{
249+
self::assertSame('foo', SampleData::make()->get('simple_prop', 'foo'));
213250
}
214251

215252
public function testAccessingNonExistentPropertyWillThrow(): void
@@ -219,4 +256,38 @@ public function testAccessingNonExistentPropertyWillThrow(): void
219256

220257
echo SampleData::make()->nope;
221258
}
259+
260+
public function testDirectPropertyAccess(): void
261+
{
262+
$data = SampleData::make(['simple_prop' => 'foo']);
263+
264+
self::assertSame('foo', $data->simple_prop);
265+
}
266+
267+
public function testAccessingNonInitializedAccessThrows(): void
268+
{
269+
self::expectException(DataTransferObjectException::class);
270+
self::expectExceptionMessage(
271+
'Tests\Fixtures\SampleData::$simple_prop must not be accessed before initialization.'
272+
);
273+
274+
echo SampleData::make()->simple_prop;
275+
}
276+
277+
public function testUntypedData(): void
278+
{
279+
$data = UntypedData::make();
280+
281+
self::assertSame([
282+
'foo_prop' => 'Foo',
283+
'null_prop' => null,
284+
], $data->toArray());
285+
286+
$data = UntypedData::make(['null_prop' => 'Not so null']);
287+
288+
self::assertSame([
289+
'foo_prop' => 'Foo',
290+
'null_prop' => 'Not so null',
291+
], $data->toArray());
292+
}
222293
}

0 commit comments

Comments
 (0)