400 lines
10 KiB
PHP
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
|
|
]);
|
|
});
|