['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 $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 $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 $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 */ 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> */ 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 $settings */ public function __construct( public string $projectName, public array $settings, ) {} /** * @return array> */ 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 */ 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 ]); });