Skip to content
This repository has been archived by the owner on Nov 1, 2022. It is now read-only.

Commit

Permalink
Merge pull request #117 from morrislaptop/cast-nested-collections
Browse files Browse the repository at this point in the history
Cast nested collections
  • Loading branch information
brendt committed Jan 21, 2021
2 parents 64555c0 + 710045f commit dd12904
Show file tree
Hide file tree
Showing 8 changed files with 239 additions and 33 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

All notable changes to `data-transfer-object` will be documented in this file

## 2.7.0 - 2021-01-21

- Cast nested collections (#117)

## 2.6.0 - 2020-11-26

- Support PHP 8
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ Alternatively you can also use a phpdoc for this:
use Spatie\DataTransferObject\DataTransferObjectCollection;

/**
* @method PostData current
* @method \App\DataTransferObjects\PostData current
*/
class PostCollection extends DataTransferObjectCollection
{
Expand Down
5 changes: 5 additions & 0 deletions src/DataTransferObjectError.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,9 @@ public static function immutable(string $property): DataTransferObjectError
{
return new self("Cannot change the value of property {$property} on an immutable data transfer object");
}

public static function untypedCollection(string $class): DataTransferObjectError
{
return new self("Collection class `{$class}` has no defined array type.");
}
}
30 changes: 15 additions & 15 deletions src/DocblockFieldValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,28 @@

namespace Spatie\DataTransferObject;

use RecursiveArrayIterator;
use RecursiveIteratorIterator;

class DocblockFieldValidator extends FieldValidator
{
public const DOCBLOCK_REGEX = <<<REGEXP
/
@var ( # Starting with `@var `, we'll capture the definition the follows
(?: # Not explicitly capturing this group,
# which contains repeated sets of type definitions
(?: # Not explicitly capturing this group
[\w?|\\\\<>,\s] # Matches type definitions like `int|string|\My\Object|array<int, string>`
)+ # These definitions can be repeated
(?: # Not explicitly capturing this group
\[] # Matches array definitions like `int[]`
)? # Array definitions are optional though
)+ # Repeated sets of type definitions
) # The whole definition after `@var ` is captured in one group
/x
REGEXP;
Expand Down Expand Up @@ -85,7 +88,8 @@ private function resolveAllowedTypes(string $definition): array

private function resolveAllowedArrayTypes(string $definition): array
{
return $this->normaliseTypes(...array_map(
// Iterators flatten the array for multiple return types from DataTransferObjectCollection::current
return $this->normaliseTypes(...(new RecursiveIteratorIterator(new RecursiveArrayIterator(array_map(
function (string $type) {
if (! $type) {
return;
Expand All @@ -101,10 +105,14 @@ function (string $type) {
return $matches[1];
}

if (is_subclass_of($type, DataTransferObjectCollection::class)) {
return $this->resolveAllowedArrayTypesFromCollection($type);
}

return null;
},
explode('|', $definition)
));
)))));
}

private function resolveAllowedArrayKeyTypes(string $definition): array
Expand All @@ -122,12 +130,4 @@ function (string $type) {
explode('|', $definition)
));
}

private function normaliseTypes(?string ...$types): array
{
return array_filter(array_map(
fn (?string $type) => self::$typeMapping[$type] ?? $type,
$types
));
}
}
60 changes: 60 additions & 0 deletions src/FieldValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Spatie\DataTransferObject;

use ReflectionClass;
use ReflectionProperty;

abstract class FieldValidator
Expand Down Expand Up @@ -148,4 +149,63 @@ private function assertValidType(string $type, $value): bool
{
return $value instanceof $type || gettype($value) === $type;
}

protected function resolveAllowedArrayTypesFromCollection(string $type): array
{
if (! is_subclass_of($type, DataTransferObjectCollection::class)) {
return [];
}

$class = new ReflectionClass($type);

$currentReturnType = $class->getMethod('current')->getReturnType();

// We cast to array to support future union types in PHP 8
$currentReturnTypes = [];
if ($currentReturnType) {
$currentReturnTypes[] = $currentReturnType->getName();
}

$docblockReturnTypes = $class->getDocComment()
? $this->getCurrentReturnTypesFromDocblock($class->getDocComment())
: [];

$types = [...$currentReturnTypes, ...$docblockReturnTypes];

if (! $types) {
throw DataTransferObjectError::untypedCollection($type);
}

return $this->normaliseTypes(...$types);
}

/**
* @return string[]
*/
private function getCurrentReturnTypesFromDocblock(string $definition): array
{
$DOCBLOCK_REGEX = '/@method ((?:(?:[\w?|\\\\<>])+(?:\[])?)+) current/';

preg_match(
$DOCBLOCK_REGEX,
$definition,
$matches
);

$type = $matches[1] ?? null;

if (! $type) {
return null;
}

return explode('|', $type);
}

protected function normaliseTypes(?string ...$types): array
{
return array_filter(array_map(
fn (?string $type) => self::$typeMapping[$type] ?? $type,
$types
));
}
}
25 changes: 11 additions & 14 deletions src/PropertyFieldValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

use ReflectionNamedType;
use ReflectionProperty;
use ReflectionType;

class PropertyFieldValidator extends FieldValidator
{
Expand All @@ -18,7 +17,7 @@ public function __construct(ReflectionProperty $property)
$this->isMixed = $this->resolveIsMixed($property);
$this->isMixedArray = $this->resolveIsMixedArray($property);
$this->allowedTypes = $this->resolveAllowedTypes($property);
$this->allowedArrayTypes = [];
$this->allowedArrayTypes = $this->resolveAllowedArrayTypes($property);
$this->allowedArrayKeyTypes = [];
}

Expand Down Expand Up @@ -59,22 +58,20 @@ private function resolveIsMixedArray(ReflectionProperty $property): bool
private function resolveAllowedTypes(ReflectionProperty $property): array
{
// We cast to array to support future union types in PHP 8
$types = [$property->getType()];
$types = [$property->getType()
? $property->getType()->getName()
: null, ];

return $this->normaliseTypes(...$types);
}

private function normaliseTypes(?ReflectionType ...$types): array
private function resolveAllowedArrayTypes(ReflectionProperty $property): array
{
return array_filter(array_map(
function (?ReflectionType $type) {
if ($type instanceof ReflectionNamedType) {
$type = $type->getName();
}

return self::$typeMapping[$type] ?? $type;
},
$types
));
// We cast to array to support future union types in PHP 8
$types = $property->getType()
? $this->resolveAllowedArrayTypesFromCollection($property->getType()->getName())
: [];

return $this->normaliseTypes(...$types);
}
}
22 changes: 19 additions & 3 deletions src/ValueCaster.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,14 @@ class ValueCaster
{
public function cast($value, FieldValidator $validator)
{
return $this->shouldBeCastToCollection($value)
? $this->castCollection($value, $validator->allowedArrayTypes)
: $this->castValue($value, $validator->allowedTypes);
if (! $this->shouldBeCastToCollection($value)) {
return $this->castValue($value, $validator->allowedTypes);
}

$values = $this->castCollection($value, $validator->allowedArrayTypes);
$collectionType = $this->collectionType($validator->allowedTypes);

return $collectionType ? new $collectionType($values) : $values;
}

public function castValue($value, array $allowedTypes)
Expand Down Expand Up @@ -59,6 +64,17 @@ public function castCollection($values, array $allowedArrayTypes)
return $casts;
}

public function collectionType(array $types): string
{
foreach ($types as $type) {
if (is_subclass_of($type, DataTransferObjectCollection::class)) {
return $type;
}
}

return false;
}

public function shouldBeCastToCollection(array $values): bool
{
if (empty($values)) {
Expand Down
124 changes: 124 additions & 0 deletions tests/NestedDataTransferObjectCollectionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
<?php

declare(strict_types=1);

namespace Spatie\DataTransferObject\Tests;

use Spatie\DataTransferObject\DataTransferObject;
use Spatie\DataTransferObject\DataTransferObjectCollection;
use Spatie\DataTransferObject\DataTransferObjectError;

class NestedDataTransferObjectCollectionTest extends TestCase
{
/** @test */
public function with_typed_properties()
{
$a = new DtoParentPropertyType([
'children' => [
['name' => 'test'],
['name' => 'test2'],
['name' => 'test3'],
],
]);

$this->assertEquals('test', $a->children[0]->name);
$this->assertEquals('test2', $a->children[1]->name);
$this->assertEquals('test3', $a->children[2]->name);
}

/** @test */
public function with_parent_docblock_property()
{
$a = new DtoParentPropertyTypeUsingDocblock([
'children' => [
['name' => 'test'],
['name' => 'test2'],
['name' => 'test3'],
],
]);

$this->assertEquals('test', $a->children[0]->name);
$this->assertEquals('test2', $a->children[1]->name);
$this->assertEquals('test3', $a->children[2]->name);
}

/** @test */
public function with_collection_docblock()
{
$a = new DtoParentPropertyTypeWithCollectionDocblock([
'children' => [
['name' => 'test'],
['name' => 'test2'],
['name' => 'test3'],
],
]);

$this->assertEquals('test', $a->children[0]->name);
$this->assertEquals('test2', $a->children[1]->name);
$this->assertEquals('test3', $a->children[2]->name);
}

/** @test */
public function with_collection_no_type()
{
$this->expectException(DataTransferObjectError::class);
$this->expectExceptionMessage('Collection class `Spatie\DataTransferObject\Tests\DtoChildPropertyTypeCollectionWithNoType` has no defined array type.');

$a = new DtoParentPropertyTypeWithCollectionNoType([
'children' => [
['name' => 'test'],
['name' => 'test2'],
['name' => 'test3'],
],
]);
}
}

// Scenario 1
class DtoParentPropertyType extends DataTransferObject
{
public DtoChildPropertyTypeCollection $children;
}

class DtoChildPropertyType extends DataTransferObject
{
public string $name;
}

class DtoChildPropertyTypeCollection extends DataTransferObjectCollection
{
public function current(): DtoChildPropertyType
{
return parent::current();
}
}

// Scenario 2
class DtoParentPropertyTypeUsingDocblock extends DataTransferObject
{
/** @var \Spatie\DataTransferObject\Tests\DtoChildPropertyTypeCollection */
public $children;
}

// Scenario 3
class DtoParentPropertyTypeWithCollectionDocblock extends DataTransferObject
{
public DtoChildPropertyTypeCollectionWithDocblock $children;
}

/**
* @method \Spatie\DataTransferObject\Tests\DtoChildPropertyType current
*/
class DtoChildPropertyTypeCollectionWithDocblock extends DataTransferObjectCollection
{
}

// Scenario 4
class DtoParentPropertyTypeWithCollectionNoType extends DataTransferObject
{
public DtoChildPropertyTypeCollectionWithNoType $children;
}

class DtoChildPropertyTypeCollectionWithNoType extends DataTransferObjectCollection
{
}

0 comments on commit dd12904

Please sign in to comment.