data-transfer-object/tests/DataObject/DataObjectTest.php
2026-03-07 11:27:12 -03:00

400 lines
10 KiB
PHP

<?php
namespace Tests\DataObject;
use Icefox\Data\Attributes\FromInput;
use Icefox\Data\Attributes\FromRouteParameter;
use Icefox\Data\Factories\DataObjectFactory;
use Illuminate\Support\Collection;
use Psr\Log\NullLogger;
readonly class Element
{
public function __construct(public int $value) {}
}
readonly class Node
{
public function __construct(public Element $element) {}
}
test('basic nested object', function () {
$input = DataObjectFactory::mapInput(Node::class, ['element' => ['value' => 1 ] ], [], new NullLogger());
expect($input)->toBe(['element' => ['value' => 1]]);
});
readonly class MappedElement
{
public function __construct(#[FromInput('name')] public int $value) {}
}
readonly class MappedNode
{
public function __construct(public MappedElement $element) {}
}
test('basic nested input map', function () {
$input = DataObjectFactory::mapInput(MappedNode::class, ['element' => ['name' => 1 ] ], [], new NullLogger());
expect($input)->toBe(['element' => ['value' => 1]]);
});
readonly class MappedCollectionItem
{
public function __construct(#[FromInput('id_item')] public int $idItem) {}
}
readonly class MappedCollectionRoot
{
/**
* @param Collection<int, MappedCollectionItem> $items
*/
public function __construct(public string $text, #[FromInput('data')] public Collection $items) {}
}
test('using from input nested', function () {
$mapped = DataObjectFactory::mapInput(MappedCollectionRoot::class, [
'text' => 'abc',
'data' => [
[ 'id_item' => 1 ],
[ 'id_item' => 2 ],
[ 'id_item' => 4 ],
[ 'id_item' => 8 ],
],
], [], new NullLogger());
expect($mapped)->toBe([
'text' => 'abc',
'items' => [
[ 'idItem' => 1 ],
[ 'idItem' => 2 ],
[ 'idItem' => 4 ],
[ 'idItem' => 8 ],
],
]);
});
readonly class CollectionRoot
{
/**
* @param Collection<int, MappedCollectionItem> $items
*/
public function __construct(public string $text, public Collection $items) {}
}
test('using from input', function () {
$mapped = DataObjectFactory::mapInput(MappedCollectionRoot::class, [
'text' => 'abc',
'items' => [
[ 'id_item' => 1 ],
[ 'id_item' => 2 ],
[ 'id_item' => 4 ],
[ 'id_item' => 8 ],
],
], [], new NullLogger());
expect($mapped)->toBe([
'text' => 'abc',
'items' => [
[ 'idItem' => 1 ],
[ 'idItem' => 2 ],
[ 'idItem' => 4 ],
[ 'idItem' => 8 ],
],
]);
});
readonly class AnnotatedArrayItem
{
public function __construct(#[FromInput('name')] public float $value) {}
}
readonly class AnnotatedArray
{
/**
* @param array<int,AnnotatedArrayItem> $items
*/
public function __construct(public array $items) {}
}
test('annotated array', function () {
$mapped = DataObjectFactory::mapInput(
AnnotatedArray::class,
['items' => [['name' => 1], ['name' => 2]]],
[],
new NullLogger(),
);
expect($mapped)->toBe(['items' => [['value' => 1], ['value' => 2]]]);
});
test('route parameter priority over from input', function () {
$dto = new class (123) {
public function __construct(
#[FromRouteParameter('user_id')]
#[FromInput('user_id')]
public int $id,
) {}
};
$result = DataObjectFactory::mapInput(
$dto::class,
['user_id' => 456],
['user_id' => 123],
new \Psr\Log\NullLogger(),
);
expect($result['id'])->toBe(123);
});
test('multiple route parameters', function () {
$dto = new class (45, 89, 'A') {
public function __construct(
#[FromRouteParameter('course_id')]
public int $courseId,
#[FromRouteParameter('student_id')]
public int $studentId,
#[FromInput('grade')]
public string $grade,
) {}
};
$result = DataObjectFactory::mapInput(
$dto::class,
['grade' => 'A'],
['course_id' => 45, 'student_id' => 89],
new \Psr\Log\NullLogger(),
);
expect($result['courseId'])->toBe(45)
->and($result['studentId'])->toBe(89)
->and($result['grade'])->toBe('A');
});
test('route parameter with nested object', function () {
$addressDto = new class ('Main St') {
public function __construct(public string $street) {}
};
$dto = new class (123, $addressDto) {
public function __construct(
#[FromRouteParameter('user_id')]
public int $userId,
public $address,
) {}
};
$result = DataObjectFactory::mapInput(
$dto::class,
['user_id' => 456, 'address' => ['street' => 'Main St']],
['user_id' => 123],
new \Psr\Log\NullLogger(),
);
expect($result['userId'])->toBe(123)
->and($result['address']['street'])->toBe('Main St');
});
readonly class SimpleWithDefaults
{
public function __construct(
public string $name,
public int $age,
public string $city = 'Unknown',
) {}
/**
* @return array<string,mixed>
*/
public static function defaults(): array
{
return [
'city' => 'New York',
'age' => 25,
];
}
}
test('defaults with fromArray - basic usage', function () {
$object = DataObjectFactory::fromArray(
SimpleWithDefaults::class,
['name' => 'John'],
[],
);
expect($object->name)->toBe('John')
->and($object->age)->toBe(25)
->and($object->city)->toBe('New York');
});
readonly class NestedWithDefaults
{
public function __construct(
public string $title,
public SimpleWithDefaults $user,
) {}
/**
* @return array<string,array<string,mixed>>
*/
public static function defaults(): array
{
return [
'user' => [
'name' => 'Default User',
'age' => 30,
],
];
}
}
test('defaults with nested objects', function () {
$object = DataObjectFactory::fromArray(
NestedWithDefaults::class,
['title' => 'Admin Dashboard'],
[],
);
expect($object->title)->toBe('Admin Dashboard')
->and($object->user->name)->toBe('Default User')
->and($object->user->age)->toBe(30)
->and($object->user->city)->toBe('Unknown');
});
test('defaults merged with input - input overrides defaults', function () {
$object = DataObjectFactory::fromArray(
SimpleWithDefaults::class,
['name' => 'Alice', 'age' => 40, 'city' => 'Los Angeles'],
[],
);
expect($object->name)->toBe('Alice')
->and($object->age)->toBe(40)
->and($object->city)->toBe('Los Angeles');
});
test('defaults with simple nested structures', function () {
$object = DataObjectFactory::fromArray(
NestedWithDefaults::class,
['title' => 'Simple Project'],
[],
);
expect($object->title)->toBe('Simple Project')
->and($object->user->name)->toBe('Default User')
->and($object->user->age)->toBe(30);
});
readonly class ArrayWithDefaults
{
/**
* @param array<string,mixed> $settings
*/
public function __construct(
public string $projectName,
public array $settings,
) {}
/**
* @return array<string,array<string,mixed>>
*/
public static function defaults(): array
{
return [
'settings' => [
'theme' => 'dark',
'notifications' => true,
'language' => 'en',
],
];
}
}
test('defaults with array structures', function () {
$object = DataObjectFactory::fromArray(
ArrayWithDefaults::class,
['projectName' => 'New Project'],
[],
);
expect($object->projectName)->toBe('New Project')
->and($object->settings)->toBe([
'theme' => 'dark',
'notifications' => true,
'language' => 'en',
]);
});
test('defaults partially overridden by input', function () {
$object = DataObjectFactory::fromArray(
ArrayWithDefaults::class,
[
'projectName' => 'Custom Project',
'settings' => [
'theme' => 'light',
'language' => 'fr',
],
],
[],
);
expect($object->projectName)->toBe('Custom Project')
->and($object->settings)->toBe([
'theme' => 'light',
'notifications' => true, // From defaults
'language' => 'fr',
]);
});
readonly class WithInputMappingAndDefaults
{
public function __construct(
#[FromInput('full_name')]
public string $name,
#[FromInput('user_age')]
public int $age,
public string $role = 'user',
) {}
/**
* @return array<string,mixed>
*/
public static function defaults(): array
{
return [
'name' => 'Default Name',
'role' => 'admin',
'age' => 18,
];
}
}
test('defaults work with FromInput attribute mapping', function () {
$object = DataObjectFactory::fromArray(
WithInputMappingAndDefaults::class,
['full_name' => 'John Doe'],
[],
);
expect($object->name)->toBe('John Doe') // From input mapping
->and($object->age)->toBe(18) // From defaults
->and($object->role)->toBe('admin'); // From defaults
});
test('array_merge_recursive behavior with nested arrays', function () {
$object = DataObjectFactory::fromArray(
ArrayWithDefaults::class,
[
'projectName' => 'Test Project',
'settings' => [
'new_setting' => 'custom_value',
],
],
[],
);
expect($object->settings)->toBe([
'theme' => 'dark', // From defaults
'notifications' => true, // From defaults
'language' => 'en', // From defaults
'new_setting' => 'custom_value', // From input
]);
});