Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion src/FreeElephants/JsonApi/DTO/AbstractDocument.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace FreeElephants\JsonApi\DTO;

use FreeElephants\JsonApi\DTO\Exception\UnexpectedValueException;

/**
* @property AbstractResourceObject|mixed $data
*/
Expand Down Expand Up @@ -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;
}
Expand Down
46 changes: 30 additions & 16 deletions src/FreeElephants/JsonApi/DTO/BaseKeyValueStructure.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace FreeElephants\JsonApi\DTO\Exception;

interface JsonApiDtoExceptionInterface
{

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace FreeElephants\JsonApi\DTO\Exception;

class UnexpectedValueException extends \UnexpectedValueException implements JsonApiDtoExceptionInterface
{

}
15 changes: 9 additions & 6 deletions src/FreeElephants/JsonApi/DTO/TopLevel.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,19 @@
*/
abstract class TopLevel
{
/**
* @param MessageInterface $httpMessage
* @return static
*/
public static function fromHttpMessage(MessageInterface $httpMessage): self
public static function fromHttpMessage(MessageInterface $httpMessage, bool $ignoreUnexpectedAttributes = false): self
{
$httpMessage->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;
}
}
91 changes: 90 additions & 1 deletion tests/FreeElephants/JsonApi/DTO/DocumentTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <<<JSON
Expand Down Expand Up @@ -56,6 +57,94 @@ public function testFromRequest()

$this->assertJsonStringEqualsJsonString($rawJson, json_encode($fooDTO));
}

public function testFromRequestWithUnexpectedAttributes(): void
{
$request = new ServerRequest('POST', '/foo');
$rawJson = <<<JSON
{
"data": {
"id": "123",
"type": "foo",
"attributes": {
"foo": "bar",
"date": "2012-04-23T18:25:43.511+03:00",
"unexpectedAttribute": true,
"nested": {
"someNestedStructure": {
"someKey": "someValue"
}
},
"nullableObjectField": null,
"nullableScalarField": null,
"nullableScalarFilledField": "baz"
},
"relationships": {
"baz": {
"data": {
"type": "bazs",
"id": "baz-id"
}
}
}
}
}
JSON;
$request->getBody()->write($rawJson);

$this->expectException(UnexpectedValueException::class);
FooDocument::fromHttpMessage($request);
}

public function testFromRequestWithAllowUnexpectedAttributes(): void
{
$request = new ServerRequest('POST', '/foo');
$rawJson = <<<JSON
{
"data": {
"id": "123",
"type": "foo",
"attributes": {
"foo": "bar",
"date": "2012-04-23T18:25:43.511+03:00",
"unexpectedAttribute": true,
"nested": {
"someNestedStructure": {
"someKey": "someValue"
}
},
"nullableObjectField": null,
"nullableScalarField": null,
"nullableScalarFilledField": "baz"
},
"relationships": {
"baz": {
"data": {
"type": "bazs",
"id": "baz-id"
}
}
}
}
}
JSON;
$request->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
Expand Down