Skip to content

Commit

Permalink
fix: include property with default values in the returned array
Browse files Browse the repository at this point in the history
  • Loading branch information
phanan committed Apr 14, 2021
1 parent 49c3a3e commit 681b49d
Show file tree
Hide file tree
Showing 6 changed files with 131 additions and 52 deletions.
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,9 @@ $data->toArray(); // ['email' => 'alice@company.tld', 'password' => 'SoSecureWow

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

$data->get('email'); // 'alice@company.tld'
$data->password; // 'SoSecureWow'

$data->age; // throws "UserCreationData::$age must not be accessed before initialization."
$data->get('age', 30); // 30

$data->nope; // throws "Public property $nope does not exist in class UserCreationData"
$data->get('nope'); // throws "Public property $nope does not exist in class UserCreationData."
$data->nope; // throws "Public property $nope does not exist in class UserCreationData."
```

## Differences from spatie/data-transfer-object
Expand Down
24 changes: 18 additions & 6 deletions src/DataTransferObject.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ private function __construct(array $parameters = [])
{
foreach (static::getAssignableProperties() as $property) {
$this->propertyNames[] = $property->getName();

if ($property->isInitialized($this)) {
$this->data[$property->getName()] = $property->getValue($this);
}

unset($this->{$property->getName()});
}

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

/**
* @param $name
* @return mixed
*/
public function get($name)
/** @return mixed */
public function get(string $name, $default = null)
{
return $this->{$name};
$this->assertPropertyExists($name);

return array_key_exists($name, $this->data) ? $this->data[$name] : $default;
}

/** @return static */
Expand Down Expand Up @@ -138,6 +142,13 @@ private function assertPropertyExists(string $name): void
}
}

private function assertPropertyInitialized(string $name): void
{
if (!array_key_exists($name, $this->data)) {
throw DataTransferObjectException::propertyNotInitialized(static::class, $name);
}
}

public function __set($name, $value): void
{
$this->assertPropertyExists($name);
Expand All @@ -153,6 +164,7 @@ public function __unset($name): void
public function __get($name)
{
$this->assertPropertyExists($name);
$this->assertPropertyInitialized($name);

return $this->data[$name];
}
Expand Down
29 changes: 4 additions & 25 deletions src/DataTransferObjectException.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
namespace Eve\DTO;

use Exception;
use ReflectionProperty;
use Throwable;

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

public static function invalidType(ReflectionProperty $targetProperty, string $type, array $allowedTypes): self
public static function nonexistentProperty(string $class, string $propertyName): self
{
if (count($allowedTypes) === 1) {
return new static(
sprintf(
'%s::$%s must be of type %s, received a value of type %s.',
$targetProperty->class,
$targetProperty->name,
$allowedTypes[0],
$type
)
);
}

return new static(
sprintf(
'%s::$%s must be one of these types: %s; received a value of type %s.',
$targetProperty->class,
$targetProperty->name,
implode(', ', $allowedTypes),
$type
)
);
return new static(sprintf('Public property $%s does not exist in class %s.', $propertyName, $class));
}

public static function nonexistentProperty(string $class, string $propertyName): self
public static function propertyNotInitialized(string $class, string $propertyName)
{
return new static(sprintf('Public property $%s does not exist in class %s.', $propertyName, $class));
return new static(sprintf('%s::$%s must not be accessed before initialization.', $class, $propertyName));
}
}
2 changes: 1 addition & 1 deletion tests/Fixtures/SampleData.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class SampleData extends DataTransferObject
{
public string $simple_prop;
public ?string $nullable_prop;
public $mixed_prop;
public string $initialized_prop = 'Initialized';
public array $array_prop;
public Foo $object_prop;
public NestedData $nested;
Expand Down
11 changes: 11 additions & 0 deletions tests/Fixtures/UntypedData.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace Tests\Fixtures;

use Eve\DTO\DataTransferObject;

class UntypedData extends DataTransferObject
{
public $foo_prop = 'Foo';
public $null_prop;
}
107 changes: 89 additions & 18 deletions tests/Unit/DataTransferObjectTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Tests\Fixtures\Foo;
use Tests\Fixtures\NestedData;
use Tests\Fixtures\SampleData;
use Tests\Fixtures\UntypedData;

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

self::assertEquals([
'initialized_prop' => 'Initialized',
'simple_prop' => 'foo',
'nullable_prop' => 'bar',
], $data->toArray());
}

public function testInitializedProperty(): void
{
self::assertEquals(['initialized_prop' => 'Initialized'], SampleData::make()->toArray());
}

public function testArrayProperty(): void
{
$data = SampleData::make();
$data->array_prop = ['foo' => 'bar'];

self::assertEquals(['array_prop' => ['foo' => 'bar']], $data->toArray());
self::assertEquals([
'initialized_prop' => 'Initialized',
'array_prop' => ['foo' => 'bar'],
], $data->toArray());
}

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

self::assertEquals(['object_prop' => $foo], $data->toArray());
self::assertEquals([
'initialized_prop' => 'Initialized',
'object_prop' => $foo,
], $data->toArray());
}

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

self::assertSame(['simple_prop' => 'foo'], $data->toArray());
self::assertSame([
'initialized_prop' => 'Initialized',
'simple_prop' => 'foo',
], $data->toArray());
}

public function testSetArray(): void
Expand All @@ -64,6 +80,7 @@ public function testSetArray(): void
]);

self::assertSame([
'initialized_prop' => 'Initialized',
'simple_prop' => 'foo',
'nullable_prop' => 'bar',
], $data->toArray());
Expand All @@ -78,11 +95,15 @@ public function testBuiltinUnset(): void
self::assertEquals([
'simple_prop' => 'foo',
'nullable_prop' => 'bar',
'initialized_prop' => 'Initialized',
], $data->toArray());

unset($data->nullable_prop);

self::assertEquals(['simple_prop' => 'foo'], $data->toArray());
self::assertEquals([
'simple_prop' => 'foo',
'initialized_prop' => 'Initialized',
], $data->toArray());
}

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

self::assertEquals([
'initialized_prop' => 'Initialized',
'simple_prop' => 'foo',
'nullable_prop' => 'bar',
], $data->toArray());

$data->unset('nullable_prop');

self::assertEquals(['simple_prop' => 'foo'], $data->toArray());
self::assertEquals([
'simple_prop' => 'foo',
'initialized_prop' => 'Initialized',
], $data->toArray());
}

public function testUnsetSpread(): void
Expand All @@ -110,9 +135,10 @@ public function testUnsetSpread(): void
self::assertEquals([
'simple_prop' => 'foo',
'nullable_prop' => 'bar',
'initialized_prop' => 'Initialized',
], $data->toArray());

$data->unset('nullable_prop', 'simple_prop');
$data->unset('nullable_prop', 'simple_prop', 'initialized_prop');

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

self::assertEquals([
'initialized_prop' => 'Initialized',
'simple_prop' => 'foo',
'nullable_prop' => 'bar',
], $data->toArray());
Expand All @@ -144,7 +171,10 @@ public function testCompact(): void
$data->simple_prop = 'foo';
$data->nullable_prop = null;

self::assertEquals(['simple_prop' => 'foo'], $data->compact()->toArray());
self::assertEquals([
'initialized_prop' => 'Initialized',
'simple_prop' => 'foo',
], $data->compact()->toArray());
}

public function testOnly(): void
Expand All @@ -162,54 +192,61 @@ public function testOnlySpread(): void
$data = SampleData::make([
'simple_prop' => 'foo',
'nullable_prop' => 'bar',
'mixed_prop' => 'baz',
]);

self::assertEquals([
'simple_prop' => 'foo',
'mixed_prop' => 'baz',
], $data->only('simple_prop', 'mixed_prop')->toArray());
'initialized_prop' => 'Initialized',
], $data->only('simple_prop', 'initialized_prop')->toArray());
}

public function testExcept(): void
{
$data = SampleData::make([
'simple_prop' => 'foo',
'nullable_prop' => 'bar',
'mixed_prop' => 'baz',
]);

self::assertEquals([
'simple_prop' => 'foo',
'nullable_prop' => 'bar',
], $data->except('mixed_prop')->toArray());
], $data->except('initialized_prop')->toArray());
}

public function testExceptSpread(): void
{
$data = SampleData::make([
'simple_prop' => 'foo',
'nullable_prop' => 'bar',
'mixed_prop' => 'baz',
]);

self::assertEquals(['simple_prop' => 'foo'], $data->except('mixed_prop', 'nullable_prop')->toArray());
self::assertEquals(
['initialized_prop' => 'Initialized'],
$data->except('simple_prop', 'nullable_prop')->toArray()
);
}

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

self::assertEquals(['nested' => ['sample_prop' => 'sample']], $data->compact()->toArray());
self::assertEquals([
'nested' => ['sample_prop' => 'sample'],
'initialized_prop' => 'Initialized',
], $data->compact()->toArray());
}

public function testPropertyAccess(): void
public function testPropertyAccessViaGet(): void
{
$data = SampleData::make(['simple_prop' => 'foo']);

self::assertSame('foo', $data->get('simple_prop'));
self::assertSame('foo', $data->simple_prop);
}

public function testPropertyAccessViaGetWithDefault(): void
{
self::assertSame('foo', SampleData::make()->get('simple_prop', 'foo'));
}

public function testAccessingNonExistentPropertyWillThrow(): void
Expand All @@ -219,4 +256,38 @@ public function testAccessingNonExistentPropertyWillThrow(): void

echo SampleData::make()->nope;
}

public function testDirectPropertyAccess(): void
{
$data = SampleData::make(['simple_prop' => 'foo']);

self::assertSame('foo', $data->simple_prop);
}

public function testAccessingNonInitializedAccessThrows(): void
{
self::expectException(DataTransferObjectException::class);
self::expectExceptionMessage(
'Tests\Fixtures\SampleData::$simple_prop must not be accessed before initialization.'
);

echo SampleData::make()->simple_prop;
}

public function testUntypedData(): void
{
$data = UntypedData::make();

self::assertSame([
'foo_prop' => 'Foo',
'null_prop' => null,
], $data->toArray());

$data = UntypedData::make(['null_prop' => 'Not so null']);

self::assertSame([
'foo_prop' => 'Foo',
'null_prop' => 'Not so null',
], $data->toArray());
}
}

0 comments on commit 681b49d

Please sign in to comment.