'Test Title', 'content' => 'Test Content', ], []); expect($dto)->toBeInstanceOf(SimplePostDTO::class) ->and($dto->title)->toBe('Test Title') ->and($dto->content)->toBe('Test Content'); }); test('failed validation throws validation exception', function () { try { DataObjectFactory::fromArray(SimplePostDTO::class, [ 'title' => 'Test', 'content' => [], ], []); } catch (\Throwable $e) { expect($e)->toBeInstanceOf(ValidationException::class); return; } throw new \Exception('Should have thrown an exception'); }); readonly class PostDTOWithNumeric { public function __construct( public string $title, public int $views, ) {} } test('failed validation returns proper error format', function () { try { DataObjectFactory::fromArray(PostDTOWithNumeric::class, [ 'title' => 'Test', 'views' => 'not-a-number', ], []); } catch (ValidationException $e) { $errors = $e->validator->errors(); expect($errors->has('views'))->toBeTrue() ->and($errors->first('views'))->toContain('number'); } }); test('validation error message format matches laravel', function () { try { DataObjectFactory::fromArray(PostDTOWithNumeric::class, [ 'title' => 'Test', 'views' => 'invalid', ], []); } catch (ValidationException $e) { $errors = $e->validator->errors()->toArray(); expect($errors)->toHaveKey('views') ->and($errors['views'][0])->toContain('views'); } }); readonly class RestrictedDTO { public function __construct(public string $content) {} public static function fails($validator): object { return (object) [ 'error' => 'Access denied', 'details' => $validator->errors()->toArray(), 'status' => 403, ]; } } test('custom fails returns custom response structure', function () { $result = DataObjectFactory::fromArray(RestrictedDTO::class, [ 'content' => [], // invalid - must be string ], []); expect($result->error)->toBe('Access denied') ->and($result->details)->toHaveKey('content') ->and($result->status)->toBe(403); }); readonly class CustomBagDTO { public string $name; public float $price; public function __construct(string $name, float $price) { $this->name = $name; $this->price = $price; } public static function rules(): array { return [ 'name' => ['required', 'min:3'], 'price' => ['required', 'numeric', 'min:0'], ]; } } test('named error bag test', function () { try { DataObjectFactory::fromArray(CustomBagDTO::class, [ 'name' => 'ab', 'price' => -10, ], []); } catch (ValidationException $e) { $errors = $e->validator->errors()->toArray(); expect($errors)->toHaveKey('name') ->and($errors)->toHaveKey('price'); } }); 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'); });