normalizeToList(null)); } public function testNormalizeToListWithScalarWrapsIntoList(): void { self::assertSame(['foo'], self::adapter()->normalizeToList('foo')); self::assertSame([42], self::adapter()->normalizeToList(42)); } public function testNormalizeToListWithObjectWrapsIntoList(): void { $obj = new stdClass(); $result = self::adapter()->normalizeToList($obj); self::assertCount(1, $result); self::assertSame($obj, $result[0]); } public function testNormalizeToListWithListReturnsListUnchanged(): void { $input = ['a', 'b', 'c']; self::assertSame($input, self::adapter()->normalizeToList($input)); } public function testNormalizeToListWithAssociativeArrayDiscardsKeys(): void { $input = ['x' => 'a', 'y' => 'b']; $result = self::adapter()->normalizeToList($input); self::assertSame(['a', 'b'], $result); } #[DataProvider('toNullableStringProvider')] public function testToNullableString(mixed $input, ?string $expected): void { self::assertSame($expected, self::adapter()->toNullableString($input)); } // ---------- toNullableString ---------- /** @return iterable */ public static function toNullableStringProvider(): iterable { yield 'null' => [null, null]; yield 'empty string' => ['', null]; yield 'whitespace only' => [' ', null]; yield 'plain' => ['abc', 'abc']; yield 'trimmed' => [' abc ', 'abc']; yield 'int coerced to string' => [42, '42']; yield 'zero preserved' => ['0', '0']; } #[DataProvider('toNullableIntProvider')] public function testToNullableInt(mixed $input, ?int $expected): void { self::assertSame($expected, self::adapter()->toNullableInt($input)); } // ---------- toNullableInt ---------- /** @return iterable */ public static function toNullableIntProvider(): iterable { yield 'null' => [null, null]; yield 'int passthrough' => [42, 42]; yield 'zero int' => [0, 0]; yield 'numeric string' => ['42', 42]; yield 'negative string' => ['-7', -7]; yield 'float-like string' => ['3.14', 3]; yield 'non-numeric' => ['abc', null]; yield 'empty string' => ['', null]; } #[DataProvider('toNullableBoolProvider')] public function testToNullableBool(mixed $input, ?bool $expected): void { self::assertSame($expected, self::adapter()->toNullableBool($input)); } // ---------- toNullableBool ---------- /** @return iterable */ public static function toNullableBoolProvider(): iterable { yield 'null' => [null, null]; yield 'true' => [true, true]; yield 'false' => [false, false]; yield 'string 1' => ['1', true]; yield 'string 0' => ['0', false]; yield 'empty string' => ['', false]; yield 'int 1' => [1, true]; yield 'int 0' => [0, false]; } // ---------- toNullableDate ---------- public function testToNullableDateFromIsoString(): void { $result = self::adapter()->toNullableDate('2026-04-21'); self::assertEquals(new DateTimeImmutable('2026-04-21'), $result); } public function testToNullableDateFromDateTimeString(): void { $result = self::adapter()->toNullableDate('2026-04-21T10:30:00+02:00'); self::assertEquals(new DateTimeImmutable('2026-04-21T10:30:00+02:00'), $result); } #[DataProvider('toNullableDateFallbackProvider')] public function testToNullableDateReturnsNullOnInvalidInput(mixed $input): void { self::assertNull(self::adapter()->toNullableDate($input)); } /** @return iterable */ public static function toNullableDateFallbackProvider(): iterable { yield 'null' => [null]; yield 'empty string' => ['']; yield 'whitespace' => [' ']; yield 'non-string int' => [42]; yield 'non-string array' => [[]]; yield 'invalid date string' => ['not-a-date']; } /** * Adapter anonyme : re-expose chaque helper protected en public pour le test, * sans jamais instancier un vrai mapper (ceux-ci ont des dépendances DI). */ private static function adapter(): object { return new class { use BovinNodeMappingTrait { normalizeToList as public; toNullableString as public; toNullableInt as public; toNullableBool as public; toNullableDate as public; } }; } }