From 4261dbe18cdafa1647dee85fc07b836ebf670d5d Mon Sep 17 00:00:00 2001 From: "n.gnato" Date: Wed, 29 Oct 2025 15:11:34 +0300 Subject: [PATCH] Allow unexpected attribites in http message and base kv struct --- CHANGELOG.md | 9 +- .../JsonApi/DTO/AbstractDocument.php | 4 +- .../JsonApi/DTO/BaseKeyValueStructure.php | 46 ++++++---- .../JsonApiDtoExceptionInterface.php | 8 ++ .../Exception/UnexpectedValueException.php | 8 ++ src/FreeElephants/JsonApi/DTO/TopLevel.php | 15 +-- .../JsonApi/DTO/DocumentTest.php | 91 ++++++++++++++++++- 7 files changed, 156 insertions(+), 25 deletions(-) create mode 100644 src/FreeElephants/JsonApi/DTO/Exception/JsonApiDtoExceptionInterface.php create mode 100644 src/FreeElephants/JsonApi/DTO/Exception/UnexpectedValueException.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 29d83be..c9b9ef9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.1.0] - 2025-10-29 + +### Added +- Allow non-documented attributes in base kv structure with BaseKeyValueStructure::ignoreUnexpectedAttributes() static accessor +- Allow non-documented attributes in TopLevel::fromHttpMessage() with second argument + ## [0.0.9] - 2025-09-11 ### Added @@ -53,7 +59,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Extract all DTO types from FreeElephants/json-api-php-toolkit to this project -[Unreleased]: https://github.com/FreeElephants/json-api-dto/compare/0.0.9...HEAD +[Unreleased]: https://github.com/FreeElephants/json-api-dto/compare/0.1.0...HEAD +[0.1.0]: https://github.com/FreeElephants/json-api-dto/releases/tag/0.1.0 [0.0.9]: https://github.com/FreeElephants/json-api-dto/releases/tag/0.0.9 [0.0.8]: https://github.com/FreeElephants/json-api-dto/releases/tag/0.0.8 [0.0.7]: https://github.com/FreeElephants/json-api-dto/releases/tag/0.0.7 diff --git a/src/FreeElephants/JsonApi/DTO/AbstractDocument.php b/src/FreeElephants/JsonApi/DTO/AbstractDocument.php index 9fc152f..4609e6b 100644 --- a/src/FreeElephants/JsonApi/DTO/AbstractDocument.php +++ b/src/FreeElephants/JsonApi/DTO/AbstractDocument.php @@ -2,6 +2,8 @@ namespace FreeElephants\JsonApi\DTO; +use FreeElephants\JsonApi\DTO\Exception\UnexpectedValueException; + /** * @property AbstractResourceObject|mixed $data */ @@ -33,7 +35,7 @@ final public function __construct(array $payload) if ($dataClassName !== 'array') { $data = new $dataClassName($payload['data']); } else { - throw new \UnexpectedValueException('`data` property must be typed, for array of resources use AbstractCollection instead ' . self::class); + throw new UnexpectedValueException('`data` property must be typed, for array of resources use AbstractCollection instead ' . self::class); } $this->data = $data; } diff --git a/src/FreeElephants/JsonApi/DTO/BaseKeyValueStructure.php b/src/FreeElephants/JsonApi/DTO/BaseKeyValueStructure.php index cbbbb69..6fa0194 100644 --- a/src/FreeElephants/JsonApi/DTO/BaseKeyValueStructure.php +++ b/src/FreeElephants/JsonApi/DTO/BaseKeyValueStructure.php @@ -2,38 +2,52 @@ namespace FreeElephants\JsonApi\DTO; +use FreeElephants\JsonApi\DTO\Exception\UnexpectedValueException; use FreeElephants\JsonApi\DTO\Field\DateTimeFieldValue; class BaseKeyValueStructure { + private static bool $ignoreUnexpectedAttributes = false; + + public static function ignoreUnexpectedAttributes(bool $ignore = true): void + { + static::$ignoreUnexpectedAttributes = $ignore; + } + public function __construct(array $attributes) { + $concreteClass = new \ReflectionClass($this); foreach ($attributes as $name => $value) { - $this->assignFieldValue($name, $value); + $this->assignFieldValue($concreteClass, $name, $value); } } - protected function assignFieldValue(string $name, $value): self + protected function assignFieldValue(\ReflectionClass $class, string $name, $value): self { - $concreteClass = new \ReflectionClass($this); - $property = $concreteClass->getProperty($name); - if ($property->hasType()) { - $propertyType = $property->getType(); - if ($propertyType instanceof \ReflectionNamedType && !$propertyType->isBuiltin()) { - if($propertyType->allowsNull() && is_null($value)) { - $value = null; - } else { - $propertyClassName = $propertyType->getName(); - if(in_array($propertyClassName, [\DateTimeInterface::class, \DateTime::class])) { - $value = new DateTimeFieldValue($value); + if ($class->hasProperty($name)) { + $property = $class->getProperty($name); + if ($property->hasType()) { + $propertyType = $property->getType(); + if ($propertyType instanceof \ReflectionNamedType && !$propertyType->isBuiltin()) { + if ($propertyType->allowsNull() && is_null($value)) { + $value = null; } else { - $value = new $propertyClassName($value); + $propertyClassName = $propertyType->getName(); + if (in_array($propertyClassName, [\DateTimeInterface::class, \DateTime::class])) { + $value = new DateTimeFieldValue($value); + } else { + $value = new $propertyClassName($value); + } } } } - } - $this->$name = $value; + $this->$name = $value; + } else { + if (!self::$ignoreUnexpectedAttributes) { + throw new UnexpectedValueException(sprintf('Provided field with name `%s` does not exists in this type (%s)', $name, $class)); + } + } return $this; } diff --git a/src/FreeElephants/JsonApi/DTO/Exception/JsonApiDtoExceptionInterface.php b/src/FreeElephants/JsonApi/DTO/Exception/JsonApiDtoExceptionInterface.php new file mode 100644 index 0000000..99ae911 --- /dev/null +++ b/src/FreeElephants/JsonApi/DTO/Exception/JsonApiDtoExceptionInterface.php @@ -0,0 +1,8 @@ +getBody()->rewind(); $rawJson = $httpMessage->getBody()->getContents(); $decodedJson = json_decode($rawJson, true); - return new static($decodedJson); + if($ignoreUnexpectedAttributes) { + BaseKeyValueStructure::ignoreUnexpectedAttributes($ignoreUnexpectedAttributes); + } + $dto = new static($decodedJson); + + BaseKeyValueStructure::ignoreUnexpectedAttributes(false); + + return $dto; } } diff --git a/tests/FreeElephants/JsonApi/DTO/DocumentTest.php b/tests/FreeElephants/JsonApi/DTO/DocumentTest.php index 4e8e6ef..9ff85aa 100644 --- a/tests/FreeElephants/JsonApi/DTO/DocumentTest.php +++ b/tests/FreeElephants/JsonApi/DTO/DocumentTest.php @@ -3,12 +3,13 @@ namespace FreeElephants\JsonApi\DTO; use FreeElephants\JsonApi\AbstractTestCase; +use FreeElephants\JsonApi\DTO\Exception\UnexpectedValueException; use Nyholm\Psr7\ServerRequest; class DocumentTest extends AbstractTestCase { - public function testFromRequest() + public function testFromRequest(): void { $request = new ServerRequest('POST', '/foo'); $rawJson = <<assertJsonStringEqualsJsonString($rawJson, json_encode($fooDTO)); } + + public function testFromRequestWithUnexpectedAttributes(): void + { + $request = new ServerRequest('POST', '/foo'); + $rawJson = <<getBody()->write($rawJson); + + $this->expectException(UnexpectedValueException::class); + FooDocument::fromHttpMessage($request); + } + + public function testFromRequestWithAllowUnexpectedAttributes(): void + { + $request = new ServerRequest('POST', '/foo'); + $rawJson = <<getBody()->write($rawJson); + + $fooDTO = FooDocument::fromHttpMessage($request, true); + + $this->assertInstanceOf(FooResource::class, $fooDTO->data); + $this->assertInstanceOf(FooAttributes::class, $fooDTO->data->attributes); + $this->assertSame('foo', $fooDTO->data->type); + $this->assertSame('bar', $fooDTO->data->attributes->foo); + $this->assertEquals(new \DateTime('2012-04-23T18:25:43.511+03'), $fooDTO->data->attributes->date); + $this->assertSame('someValue', $fooDTO->data->attributes->nested->someNestedStructure->someKey); + $this->assertSame('baz-id', $fooDTO->data->relationships->baz->data->id); + $this->assertNull($fooDTO->data->attributes->nullableObjectField); + $this->assertNull($fooDTO->data->attributes->nullableScalarField); + $this->assertSame('baz', $fooDTO->data->attributes->nullableScalarFilledField); + + $this->assertJsonStringNotEqualsJsonString($rawJson, json_encode($fooDTO), 'Ignored attributes not present in resulted dto'); + } } class FooDocument extends AbstractDocument