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

Support for iterable type #71

Merged
merged 1 commit into from Nov 28, 2019
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.md
Expand Up @@ -130,6 +130,13 @@ class PostData extends DataTransferObject
*/
public $property;

/**
* Iterator of types:
*
* @var iterator<\App\Models\Author>
*/
public $property;

/**
* Union types:
*
Expand All @@ -151,6 +158,13 @@ class PostData extends DataTransferObject
*/
public $property;

/**
* Any iterator :
*
* @var iterator
*/
public $property;

/**
* No type, which allows everything
*/
Expand Down
34 changes: 30 additions & 4 deletions src/Property.php
Expand Up @@ -87,7 +87,7 @@ protected function resolveTypeDefinition()
return;
}

preg_match('/\@var ((?:(?:[\w|\\\\])+(?:\[\])?)+)/', $docComment, $matches);
preg_match('/\@var ((?:(?:[\w|\\\\<>])+(?:\[\])?)+)/', $docComment, $matches);

if (! count($matches)) {
$this->isNullable = true;
Expand All @@ -102,6 +102,10 @@ protected function resolveTypeDefinition()
$this->types = explode('|', $varDocComment);
$this->arrayTypes = str_replace('[]', '', $this->types);

if (preg_match_all('/iterable<([^|]*)>/', $varDocComment, $matches)) {
$this->arrayTypes = array_merge($this->arrayTypes, $matches[1]);
}

$this->isNullable = strpos($varDocComment, 'null') !== false;
}

Expand Down Expand Up @@ -196,7 +200,11 @@ protected function shouldBeCastToCollection(array $values): bool
protected function assertTypeEquals(string $type, $value): bool
{
if (strpos($type, '[]') !== false) {
return $this->isValidGenericCollection($type, $value);
return $this->isValidArray($type, $value);
}

if ($type === 'iterable' || strpos($type, 'iterable<') === 0) {
return $this->isValidIterable($type, $value);
}

if ($type === 'mixed' && $value !== null) {
Expand All @@ -207,16 +215,34 @@ protected function assertTypeEquals(string $type, $value): bool
|| gettype($value) === (self::$typeMapping[$type] ?? $type);
}

protected function isValidGenericCollection(string $type, $collection): bool
protected function isValidArray(string $type, $collection): bool
{
if (! is_array($collection)) {
return false;
}

$valueType = str_replace('[]', '', $type);

return $this->isValidGenericCollection($valueType, $collection);
}

protected function isValidIterable(string $type, $collection): bool
{
if (! is_iterable($collection)) {
return false;
}

if (preg_match('/^iterable<(.*)>$/', $type, $matches)) {
return $this->isValidGenericCollection($matches[1], $collection);
}

return true;
}

protected function isValidGenericCollection(string $type, $collection): bool
{
foreach ($collection as $value) {
if (! $this->assertTypeEquals($valueType, $value)) {
if (! $this->assertTypeEquals($type, $value)) {
return false;
}
}
Expand Down
62 changes: 61 additions & 1 deletion tests/DataTransferObjectTest.php
Expand Up @@ -4,14 +4,15 @@

namespace Spatie\DataTransferObject\Tests;

use ArrayIterator;
use Spatie\DataTransferObject\DataTransferObject;
use Spatie\DataTransferObject\DataTransferObjectError;
use Spatie\DataTransferObject\Tests\TestClasses\DummyClass;
use Spatie\DataTransferObject\Tests\TestClasses\EmptyChild;
use Spatie\DataTransferObject\Tests\TestClasses\OtherClass;
use Spatie\DataTransferObject\Tests\TestClasses\NestedChild;
use Spatie\DataTransferObject\Tests\TestClasses\NestedParent;
use Spatie\DataTransferObject\Tests\TestClasses\NestedParentOfMany;
use Spatie\DataTransferObject\Tests\TestClasses\OtherClass;

class DataTransferObjectTest extends TestCase
{
Expand Down Expand Up @@ -383,4 +384,63 @@ public function empty_constructor_is_supported()

$this->assertEquals(['foo' => 'abc', 'bar' => null], $valueObject->all());
}

/** @test */
public function iterable_is_supported()
{
new class(['strings' => ['foo', 'bar']]) extends DataTransferObject {
/** @var iterable<string> */
public $strings;
};

new class(['strings' => new ArrayIterator(['foo', 'bar'])]) extends DataTransferObject {
/** @var iterable<string> */
public $strings;
};

new class(['mixeds' => ['foo', 1]]) extends DataTransferObject {
/** @var iterable */
public $mixeds;
};

new class(['mixeds' => new ArrayIterator(['foo', 1])]) extends DataTransferObject {
/** @var iterable */
public $mixeds;
};

$this->markTestSucceeded();
}

/** @test */
public function an_exception_is_thrown_for_incoherent_iterator_type()
{
$this->expectException(DataTransferObjectError::class);

new class(['strings' => ['foo', 1]]) extends DataTransferObject {
/** @var iterable<string> */
public $strings;
};
}

/** @test */
public function nested_dtos_are_automatically_cast_from_arrays_to_objects_with_iterable_syntax()
{
$data = [
'children' => [
['name' => 'Alice'],
['name' => 'Bob'],
],
];

$object = new class($data) extends DataTransferObject {
/** @var iterable<\Spatie\DataTransferObject\Tests\TestClasses\NestedChild>|iterable<string> */
public $children;
};

$this->assertTrue(is_array($object->children));
$this->assertCount(2, $object->children);
$this->assertContainsOnly(NestedChild::class, $object->children);
$this->assertEquals('Alice', $object->children[0]->name);
$this->assertEquals('Bob', $object->children[1]->name);
}
}