Skip to content

Commit 10a22ee

Browse files
authored
Merge pull request #9 from FreeElephants/ignore-attributes
Allow unexpected attribites in http message and base kv struct
2 parents 7f46939 + 4261dbe commit 10a22ee

File tree

7 files changed

+156
-25
lines changed

7 files changed

+156
-25
lines changed

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66

77
## [Unreleased]
88

9+
## [0.1.0] - 2025-10-29
10+
11+
### Added
12+
- Allow non-documented attributes in base kv structure with BaseKeyValueStructure::ignoreUnexpectedAttributes() static accessor
13+
- Allow non-documented attributes in TopLevel::fromHttpMessage() with second argument
14+
915
## [0.0.9] - 2025-09-11
1016

1117
### Added
@@ -53,7 +59,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
5359
### Added
5460
- Extract all DTO types from FreeElephants/json-api-php-toolkit to this project
5561

56-
[Unreleased]: https://github.com/FreeElephants/json-api-dto/compare/0.0.9...HEAD
62+
[Unreleased]: https://github.com/FreeElephants/json-api-dto/compare/0.1.0...HEAD
63+
[0.1.0]: https://github.com/FreeElephants/json-api-dto/releases/tag/0.1.0
5764
[0.0.9]: https://github.com/FreeElephants/json-api-dto/releases/tag/0.0.9
5865
[0.0.8]: https://github.com/FreeElephants/json-api-dto/releases/tag/0.0.8
5966
[0.0.7]: https://github.com/FreeElephants/json-api-dto/releases/tag/0.0.7

src/FreeElephants/JsonApi/DTO/AbstractDocument.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace FreeElephants\JsonApi\DTO;
44

5+
use FreeElephants\JsonApi\DTO\Exception\UnexpectedValueException;
6+
57
/**
68
* @property AbstractResourceObject|mixed $data
79
*/
@@ -33,7 +35,7 @@ final public function __construct(array $payload)
3335
if ($dataClassName !== 'array') {
3436
$data = new $dataClassName($payload['data']);
3537
} else {
36-
throw new \UnexpectedValueException('`data` property must be typed, for array of resources use AbstractCollection instead ' . self::class);
38+
throw new UnexpectedValueException('`data` property must be typed, for array of resources use AbstractCollection instead ' . self::class);
3739
}
3840
$this->data = $data;
3941
}

src/FreeElephants/JsonApi/DTO/BaseKeyValueStructure.php

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,38 +2,52 @@
22

33
namespace FreeElephants\JsonApi\DTO;
44

5+
use FreeElephants\JsonApi\DTO\Exception\UnexpectedValueException;
56
use FreeElephants\JsonApi\DTO\Field\DateTimeFieldValue;
67

78
class BaseKeyValueStructure
89
{
10+
private static bool $ignoreUnexpectedAttributes = false;
11+
12+
public static function ignoreUnexpectedAttributes(bool $ignore = true): void
13+
{
14+
static::$ignoreUnexpectedAttributes = $ignore;
15+
}
16+
917
public function __construct(array $attributes)
1018
{
19+
$concreteClass = new \ReflectionClass($this);
1120
foreach ($attributes as $name => $value) {
12-
$this->assignFieldValue($name, $value);
21+
$this->assignFieldValue($concreteClass, $name, $value);
1322
}
1423
}
1524

16-
protected function assignFieldValue(string $name, $value): self
25+
protected function assignFieldValue(\ReflectionClass $class, string $name, $value): self
1726
{
18-
$concreteClass = new \ReflectionClass($this);
19-
$property = $concreteClass->getProperty($name);
20-
if ($property->hasType()) {
21-
$propertyType = $property->getType();
22-
if ($propertyType instanceof \ReflectionNamedType && !$propertyType->isBuiltin()) {
23-
if($propertyType->allowsNull() && is_null($value)) {
24-
$value = null;
25-
} else {
26-
$propertyClassName = $propertyType->getName();
27-
if(in_array($propertyClassName, [\DateTimeInterface::class, \DateTime::class])) {
28-
$value = new DateTimeFieldValue($value);
27+
if ($class->hasProperty($name)) {
28+
$property = $class->getProperty($name);
29+
if ($property->hasType()) {
30+
$propertyType = $property->getType();
31+
if ($propertyType instanceof \ReflectionNamedType && !$propertyType->isBuiltin()) {
32+
if ($propertyType->allowsNull() && is_null($value)) {
33+
$value = null;
2934
} else {
30-
$value = new $propertyClassName($value);
35+
$propertyClassName = $propertyType->getName();
36+
if (in_array($propertyClassName, [\DateTimeInterface::class, \DateTime::class])) {
37+
$value = new DateTimeFieldValue($value);
38+
} else {
39+
$value = new $propertyClassName($value);
40+
}
3141
}
3242
}
3343
}
34-
}
3544

36-
$this->$name = $value;
45+
$this->$name = $value;
46+
} else {
47+
if (!self::$ignoreUnexpectedAttributes) {
48+
throw new UnexpectedValueException(sprintf('Provided field with name `%s` does not exists in this type (%s)', $name, $class));
49+
}
50+
}
3751

3852
return $this;
3953
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace FreeElephants\JsonApi\DTO\Exception;
4+
5+
interface JsonApiDtoExceptionInterface
6+
{
7+
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace FreeElephants\JsonApi\DTO\Exception;
4+
5+
class UnexpectedValueException extends \UnexpectedValueException implements JsonApiDtoExceptionInterface
6+
{
7+
8+
}

src/FreeElephants/JsonApi/DTO/TopLevel.php

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,19 @@
1010
*/
1111
abstract class TopLevel
1212
{
13-
/**
14-
* @param MessageInterface $httpMessage
15-
* @return static
16-
*/
17-
public static function fromHttpMessage(MessageInterface $httpMessage): self
13+
public static function fromHttpMessage(MessageInterface $httpMessage, bool $ignoreUnexpectedAttributes = false): self
1814
{
1915
$httpMessage->getBody()->rewind();
2016
$rawJson = $httpMessage->getBody()->getContents();
2117
$decodedJson = json_decode($rawJson, true);
2218

23-
return new static($decodedJson);
19+
if($ignoreUnexpectedAttributes) {
20+
BaseKeyValueStructure::ignoreUnexpectedAttributes($ignoreUnexpectedAttributes);
21+
}
22+
$dto = new static($decodedJson);
23+
24+
BaseKeyValueStructure::ignoreUnexpectedAttributes(false);
25+
26+
return $dto;
2427
}
2528
}

tests/FreeElephants/JsonApi/DTO/DocumentTest.php

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@
33
namespace FreeElephants\JsonApi\DTO;
44

55
use FreeElephants\JsonApi\AbstractTestCase;
6+
use FreeElephants\JsonApi\DTO\Exception\UnexpectedValueException;
67
use Nyholm\Psr7\ServerRequest;
78

89
class DocumentTest extends AbstractTestCase
910
{
1011

11-
public function testFromRequest()
12+
public function testFromRequest(): void
1213
{
1314
$request = new ServerRequest('POST', '/foo');
1415
$rawJson = <<<JSON
@@ -56,6 +57,94 @@ public function testFromRequest()
5657

5758
$this->assertJsonStringEqualsJsonString($rawJson, json_encode($fooDTO));
5859
}
60+
61+
public function testFromRequestWithUnexpectedAttributes(): void
62+
{
63+
$request = new ServerRequest('POST', '/foo');
64+
$rawJson = <<<JSON
65+
{
66+
"data": {
67+
"id": "123",
68+
"type": "foo",
69+
"attributes": {
70+
"foo": "bar",
71+
"date": "2012-04-23T18:25:43.511+03:00",
72+
"unexpectedAttribute": true,
73+
"nested": {
74+
"someNestedStructure": {
75+
"someKey": "someValue"
76+
}
77+
},
78+
"nullableObjectField": null,
79+
"nullableScalarField": null,
80+
"nullableScalarFilledField": "baz"
81+
},
82+
"relationships": {
83+
"baz": {
84+
"data": {
85+
"type": "bazs",
86+
"id": "baz-id"
87+
}
88+
}
89+
}
90+
}
91+
}
92+
JSON;
93+
$request->getBody()->write($rawJson);
94+
95+
$this->expectException(UnexpectedValueException::class);
96+
FooDocument::fromHttpMessage($request);
97+
}
98+
99+
public function testFromRequestWithAllowUnexpectedAttributes(): void
100+
{
101+
$request = new ServerRequest('POST', '/foo');
102+
$rawJson = <<<JSON
103+
{
104+
"data": {
105+
"id": "123",
106+
"type": "foo",
107+
"attributes": {
108+
"foo": "bar",
109+
"date": "2012-04-23T18:25:43.511+03:00",
110+
"unexpectedAttribute": true,
111+
"nested": {
112+
"someNestedStructure": {
113+
"someKey": "someValue"
114+
}
115+
},
116+
"nullableObjectField": null,
117+
"nullableScalarField": null,
118+
"nullableScalarFilledField": "baz"
119+
},
120+
"relationships": {
121+
"baz": {
122+
"data": {
123+
"type": "bazs",
124+
"id": "baz-id"
125+
}
126+
}
127+
}
128+
}
129+
}
130+
JSON;
131+
$request->getBody()->write($rawJson);
132+
133+
$fooDTO = FooDocument::fromHttpMessage($request, true);
134+
135+
$this->assertInstanceOf(FooResource::class, $fooDTO->data);
136+
$this->assertInstanceOf(FooAttributes::class, $fooDTO->data->attributes);
137+
$this->assertSame('foo', $fooDTO->data->type);
138+
$this->assertSame('bar', $fooDTO->data->attributes->foo);
139+
$this->assertEquals(new \DateTime('2012-04-23T18:25:43.511+03'), $fooDTO->data->attributes->date);
140+
$this->assertSame('someValue', $fooDTO->data->attributes->nested->someNestedStructure->someKey);
141+
$this->assertSame('baz-id', $fooDTO->data->relationships->baz->data->id);
142+
$this->assertNull($fooDTO->data->attributes->nullableObjectField);
143+
$this->assertNull($fooDTO->data->attributes->nullableScalarField);
144+
$this->assertSame('baz', $fooDTO->data->attributes->nullableScalarFilledField);
145+
146+
$this->assertJsonStringNotEqualsJsonString($rawJson, json_encode($fooDTO), 'Ignored attributes not present in resulted dto');
147+
}
59148
}
60149

61150
class FooDocument extends AbstractDocument

0 commit comments

Comments
 (0)