Skip to content

Commit 9e96a96

Browse files
authored
Union types support (#25)
* Remove unused import * Support union types for relationships dto * Split testsuites by php versions * Add test case without union types * Use php version var for docker * Check union types case first * Fix typo in const name * Extract common resource examples * Update docs * Suggest nyholm/psr7 package
1 parent ecc4d0f commit 9e96a96

25 files changed

+244
-16
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
/.env
2+
13
/.idea/
24

35
/.phpunit.result.cache

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
- Data Transfer Object classes generation from swagger spec
1111
- JsonApiResponseFactory::createResponse() `meta` and `links` arguments
1212
- CacheableDispatcherFactoryProxy
13+
- Union types support for `data.relationships` DTO: use suitable type for input data structure
1314

1415
### Changed
1516
- Best composer dependencies compatibility: allow more versions

Dockerfile

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
FROM php:8-cli
1+
ARG PHP_VERSION
2+
3+
FROM php:${PHP_VERSION}-cli
24

35
RUN apt-get update \
46
&& apt-get install -y \

Makefile

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
PATH := $(shell pwd)/bin:$(PATH)
2+
$(shell cp -n dev.env .env)
3+
include .env
24

35
build:
4-
docker build -t free-elephants/json-api-php-toolkit .
6+
docker build --build-arg PHP_VERSION=${PHP_VERSION} -t free-elephants/json-api-php-toolkit .
57

68
install: build
79
composer install

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,9 @@ Available in [/docs](/docs).
2828
1. [Serialize doctrine entities](/docs/doctrine.md)
2929
1. [DTO from psr server request](/docs/dto-psr7.md)
3030
1. [Validation](/docs/validation.md)
31+
32+
## Development
33+
34+
All dev env is dockerized. Your can use make receipts and `bin/` scripts without locally installed php, composer.
35+
36+
For run tests with different php version change `PHP_VERSION` value in .env and rebuild image with `make build`.

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@
4040
"suggest": {
4141
"doctrine/orm": "^2.7",
4242
"free-elephants/di": "*",
43-
"laminas/laminas-httphandlerrunner": "*"
43+
"laminas/laminas-httphandlerrunner": "*",
44+
"nyholm/psr7": "^1.2"
4445
},
4546
"autoload": {
4647
"psr-4": {

dev.env

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
PHP_VERSION=8.0

docs/dto-psr7.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@
33
Full typed objects from request or response body.
44

55
See example in [test](/tests/FreeElephants/JsonApiToolkit/DTO/DocumentTest.php).
6+
7+
Union types support for relationships in PHP 8.

docs/routing.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,8 @@ JSON
6060

6161
(new DispatcherFactory(null, new Parsers\JsonFileParser()))->buildDispatcher('swagger.json');
6262
```
63+
64+
## About performance
65+
66+
Parse large swagger files have performance issues. Use `FreeElephants\JsonApiToolkit\Routing\FastRoute\CacheableDispatcherFactoryProxy` for production usage: it based on psr/cache component compare swagger file hash.
67+

phpunit.xml.dist

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,11 @@
1313
<ini name="error_reporting" value="E_ALL"/>
1414
</php>
1515
<testsuites>
16-
<testsuite name="JsonApi Toolkit Tests">
17-
<directory>./tests</directory>
16+
<testsuite name="8.0">
17+
<directory suffix="TestPHP8.php" phpVersion="8.0">./tests</directory>
18+
</testsuite>
19+
<testsuite name="7.4">
20+
<directory suffix="Test.php" phpVersion="7.4">./tests</directory>
1821
</testsuite>
1922
</testsuites>
2023
<logging/>

src/FreeElephants/JsonApiToolkit/DTO/AbstractResourceObject.php

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
namespace FreeElephants\JsonApiToolkit\DTO;
44

5+
use FreeElephants\JsonApiToolkit\DTO\Reflection\SuitableRelationshipsTypeDetector;
6+
57
/**
6-
* @property AbstractAttributes $attributes
8+
* @property AbstractAttributes $attributes
79
* @property AbstractRelationships $relationships
810
*/
911
class AbstractResourceObject
@@ -25,10 +27,20 @@ public function __construct(array $data)
2527
}
2628

2729
if (property_exists($this, 'relationships')) {
30+
$relationshipsData = $data['relationships'];
2831
$concreteClass = new \ReflectionClass($this);
2932
$relationshipsProperty = $concreteClass->getProperty('relationships');
30-
$relationshipsClass = $relationshipsProperty->getType()->getName();
31-
$this->relationships = new $relationshipsClass($data['relationships']);
33+
$reflectionType = $relationshipsProperty->getType();
34+
35+
// handle php 8 union types
36+
if (class_exists(\ReflectionUnionType::class) && $reflectionType instanceof \ReflectionUnionType) {
37+
$relationshipsClass = (new SuitableRelationshipsTypeDetector())->detect($reflectionType, $relationshipsData);
38+
} else {
39+
$relationshipsClass = $reflectionType->getName();
40+
}
41+
42+
$relationshipsDto = new $relationshipsClass($relationshipsData);
43+
$this->relationships = $relationshipsDto;
3244
}
3345
}
3446
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
namespace FreeElephants\JsonApiToolkit\DTO\Reflection;
4+
5+
class SuitableRelationshipsTypeDetector
6+
{
7+
8+
public function detect(\ReflectionUnionType $propertyUnionType, array $data): ?string
9+
{
10+
foreach ($propertyUnionType->getTypes() as $type) {
11+
$candidateType = $type->getName();
12+
$candidateClass = new \ReflectionClass($candidateType);
13+
$isCandidate = false;
14+
foreach ($data as $propertyName => $propertyData) {
15+
if (!$candidateClass->hasProperty($propertyName)) {
16+
break;
17+
}
18+
$isCandidate = true;
19+
}
20+
if ($isCandidate) {
21+
break;
22+
}
23+
}
24+
25+
return $candidateType;
26+
}
27+
}

tests/FreeElephants/JsonApiToolkit/AbstractTestCase.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,5 @@
66

77
class AbstractTestCase extends TestCase
88
{
9-
protected const FIXTERE_PATH = __DIR__ . '/../../_fixtures';
9+
protected const FIXTURE_PATH = __DIR__ . '/../../_fixtures';
1010
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace FreeElephants\JsonApiToolkit\DTO\Example;
4+
5+
use FreeElephants\JsonApiToolkit\DTO\AbstractAttributes;
6+
7+
class Attributes extends AbstractAttributes
8+
{
9+
public string $foo;
10+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
namespace FreeElephants\JsonApiToolkit\DTO\Example;
4+
5+
use FreeElephants\JsonApiToolkit\DTO\AbstractRelationships;
6+
use FreeElephants\JsonApiToolkit\DTO\RelationshipToOne;
7+
8+
class OneRelationships extends AbstractRelationships
9+
{
10+
public RelationshipToOne $one;
11+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
namespace FreeElephants\JsonApiToolkit\DTO\Example;
4+
5+
use FreeElephants\JsonApiToolkit\DTO\AbstractRelationships;
6+
use FreeElephants\JsonApiToolkit\DTO\RelationshipToOne;
7+
8+
class TwoRelationships extends AbstractRelationships
9+
{
10+
public RelationshipToOne $two;
11+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
namespace FreeElephants\JsonApiToolkit\DTO\Reflection;
4+
5+
use FreeElephants\JsonApiToolkit\AbstractTestCase;
6+
use FreeElephants\JsonApiToolkit\DTO\AbstractAttributes;
7+
use FreeElephants\JsonApiToolkit\DTO\AbstractRelationships;
8+
use FreeElephants\JsonApiToolkit\DTO\AbstractResourceObject;
9+
use FreeElephants\JsonApiToolkit\DTO\RelationshipToOne;
10+
use ReflectionProperty;
11+
12+
class SuitableRelationshipsTypeDetectorTestPHP8 extends AbstractTestCase
13+
{
14+
15+
public function testDetect()
16+
{
17+
$detector = new SuitableRelationshipsTypeDetector();
18+
$relationshipsProperty = new ReflectionProperty(ResourceWithUnionTypedRelationships::class, 'relationships');
19+
$relationshipsPropertyUnionType = $relationshipsProperty->getType();
20+
$detectedOne = $detector->detect($relationshipsPropertyUnionType, [
21+
'fuzz' => [
22+
'data' => [
23+
'id' => 'fuzz',
24+
'type' => 'fuzz',
25+
],
26+
],
27+
]);
28+
$this->assertSame(FuzzRelationships::class, $detectedOne);
29+
30+
$detectedTwo = $detector->detect($relationshipsPropertyUnionType, [
31+
'bar' => [
32+
'data' => [
33+
'id' => 'bazz',
34+
'type' => 'bazz',
35+
],
36+
],
37+
]);
38+
$this->assertSame(BarRelationships::class, $detectedTwo);
39+
}
40+
}
41+
42+
class ResourceWithUnionTypedRelationships extends AbstractResourceObject
43+
{
44+
public BazzAttributes $attributes;
45+
public FuzzRelationships|BarRelationships $relationships;
46+
}
47+
48+
class BazzAttributes extends AbstractAttributes
49+
{
50+
public string $bazz;
51+
}
52+
53+
class FuzzRelationships extends AbstractRelationships
54+
{
55+
public RelationshipToOne $fuzz;
56+
}
57+
58+
class BarRelationships extends AbstractRelationships
59+
{
60+
public RelationshipToOne $bar;
61+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
namespace FreeElephants\JsonApiToolkit\DTO;
4+
5+
use FreeElephants\JsonApiToolkit\AbstractTestCase;
6+
7+
class ResourceObjectTest extends AbstractTestCase
8+
{
9+
public function testRelationshipTypes()
10+
{
11+
$resourceObject = new class([
12+
'id' => 'id',
13+
'type' => 'type',
14+
'attributes' => [
15+
'foo' => 'bar',
16+
],
17+
'relationships' => [
18+
'one' => [
19+
'data' => [
20+
'type' => 'one',
21+
'id' => 'one',
22+
],
23+
],
24+
],
25+
]) extends AbstractResourceObject {
26+
public Example\Attributes $attributes;
27+
public Example\OneRelationships $relationships;
28+
};
29+
30+
$this->assertInstanceOf(Example\OneRelationships::class, $resourceObject->relationships);
31+
$this->assertSame('one', $resourceObject->relationships->one->data->type);
32+
}
33+
}
34+
35+
class Attributes extends AbstractAttributes
36+
{
37+
public string $foo;
38+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
namespace FreeElephants\JsonApiToolkit\DTO;
4+
5+
use FreeElephants\JsonApiToolkit\AbstractTestCase;
6+
7+
class ResourceObjectTestPHP8 extends AbstractTestCase
8+
{
9+
public function testUnionTypes()
10+
{
11+
$resourceObject = new class([
12+
'id' => 'id',
13+
'type' => 'type',
14+
'attributes' => [
15+
'foo' => 'bar',
16+
],
17+
'relationships' => [
18+
'one' => [
19+
'data' => [
20+
'type' => 'one',
21+
'id' => 'one',
22+
],
23+
],
24+
],
25+
]) extends AbstractResourceObject{
26+
public Example\Attributes $attributes;
27+
public Example\OneRelationships|Example\TwoRelationships $relationships;
28+
};
29+
30+
$this->assertSame('one', $resourceObject->relationships->one->data->type);
31+
}
32+
}
33+

tests/FreeElephants/JsonApiToolkit/JsonApi/AttributesClassBuilderTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ class AttributesClassBuilderTest extends AbstractTestCase
1111

1212
public function testCollect()
1313
{
14-
$openapi = (new YamlFileParser())->parse(self::FIXTERE_PATH . '/json-api-simple-attributes-mapping-example.yml');
14+
$openapi = (new YamlFileParser())->parse(self::FIXTURE_PATH . '/json-api-simple-attributes-mapping-example.yml');
1515
$collector = new AttributesClassBuilder();
1616
$attributesClass = $collector->buildClass($openapi, 'articles');
1717
$expectedAttributesSourceCode = <<<PHP

tests/FreeElephants/JsonApiToolkit/JsonApi/AttributesSchemaBuilderTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ class AttributesSchemaBuilderTest extends AbstractTestCase
1010

1111
public function testBuild()
1212
{
13-
$openapi = (new YamlFileParser())->parse(self::FIXTERE_PATH . '/json-api-simple-attributes-mapping-example.yml');
13+
$openapi = (new YamlFileParser())->parse(self::FIXTURE_PATH . '/json-api-simple-attributes-mapping-example.yml');
1414
$attributesSchemaBuilder = new AttributesSchemaBuilder();
1515
$schema = $attributesSchemaBuilder->build($openapi, 'articles');
1616
$this->assertTrue($schema->hasAttribute('title'));
@@ -20,7 +20,7 @@ public function testBuild()
2020
public function testBuildFromAllOf()
2121
{
2222
$this->markTestIncomplete();
23-
$openapi = (new YamlFileParser())->parse(self::FIXTERE_PATH . '/json-api.yml');
23+
$openapi = (new YamlFileParser())->parse(self::FIXTURE_PATH . '/json-api.yml');
2424
$attributesSchemaBuilder = new AttributesSchemaBuilder();
2525
$schema = $attributesSchemaBuilder->build($openapi, 'people');
2626
$this->assertTrue($schema->hasAttribute('first-name'));

tests/FreeElephants/JsonApiToolkit/JsonApi/DataTransferObjectClassSourceCodeGeneratorTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ class DataTransferObjectClassSourceCodeGeneratorTest extends AbstractTestCase
1111
public function testGenerate()
1212
{
1313
$generator = new DataTransferObjectClassSourceCodeGenerator();
14-
$openapi = (new YamlFileParser())->parse(self::FIXTERE_PATH . '/json-api.yml');
14+
$openapi = (new YamlFileParser())->parse(self::FIXTURE_PATH . '/json-api.yml');
1515

1616
$set = $generator->generate($openapi, 'articles');
1717
$expectedDocumentSourceCode = <<<PHP

tests/FreeElephants/JsonApiToolkit/Middleware/SwaggerSpecificationRequestValidatorTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
class SwaggerSpecificationRequestValidatorTest extends AbstractHttpTestCase
99
{
1010

11-
private const SWAGGER_FILE = self::FIXTERE_PATH . '/swagger-example-for-request-validation.yml';
11+
private const SWAGGER_FILE = self::FIXTURE_PATH . '/swagger-example-for-request-validation.yml';
1212

1313
public function testProcessSuccess()
1414
{

tests/FreeElephants/JsonApiToolkit/OasToolsAdapter/JsonFileParserTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ class JsonFileParserTest extends AbstractTestCase
1010
public function testParse()
1111
{
1212
$parser = new JsonFileParser();
13-
$openapi = $parser->parse(self::FIXTERE_PATH . '/petstore.json');
13+
$openapi = $parser->parse(self::FIXTURE_PATH . '/petstore.json');
1414
$this->assertSame('Swagger Petstore', $openapi->info->title);
1515
}
1616
}

tests/FreeElephants/JsonApiToolkit/OasToolsAdapter/YamlFileParserTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ class YamlFileParserTest extends AbstractTestCase
1010
public function testParse()
1111
{
1212
$parser = new YamlFileParser();
13-
$openapi = $parser->parse(self::FIXTERE_PATH . '/petstore.yaml');
13+
$openapi = $parser->parse(self::FIXTURE_PATH . '/petstore.yaml');
1414
$this->assertSame('Swagger Petstore', $openapi->info->title);
1515
}
1616
}

0 commit comments

Comments
 (0)