diff --git a/README.md b/README.md index 330bf56..5e6ad30 100644 --- a/README.md +++ b/README.md @@ -396,9 +396,8 @@ use Pixelshaped\FlatMapperBundle\FlatMapper; $flatMapper = new FlatMapper(); // Optional: configure for production -$flatMapper - ->setCacheService($psr6CachePool) // Any PSR-6 cache - ->setValidateMapping(false); // Skip validation checks +$flatMapper->setCacheService($cache); // Any Symfony\Contracts\Cache\CacheInterface implementation +$flatMapper->setValidateMapping(false); // Skip validation checks $result = $flatMapper->map(AuthorDTO::class, $queryResults); ``` diff --git a/src/FlatMapper.php b/src/FlatMapper.php index 10326b2..3430f2a 100644 --- a/src/FlatMapper.php +++ b/src/FlatMapper.php @@ -11,9 +11,12 @@ use Pixelshaped\FlatMapperBundle\Mapping\ReferenceArray; use Pixelshaped\FlatMapperBundle\Mapping\Scalar; use Pixelshaped\FlatMapperBundle\Mapping\ScalarArray; +use ReflectionAttribute; use ReflectionClass; use ReflectionProperty; use Symfony\Contracts\Cache\CacheInterface; +use TypeError; +use function in_array; final class FlatMapper { @@ -52,7 +55,7 @@ public function createMapping(string $dtoClassName): void if(!isset($this->objectsMapping[$dtoClassName])) { if($this->cacheService !== null) { - $cacheKey = strtr($dtoClassName, ['\\' => '_', '-' => '_', ' ' => '_']); + $cacheKey = sha1($dtoClassName); $mappingInfo = $this->cacheService->get('pixelshaped_flat_mapper_'.$cacheKey, function () use ($dtoClassName): array { return $this->createMappingRecursive($dtoClassName); }); @@ -69,10 +72,17 @@ public function createMapping(string $dtoClassName): void * @param class-string $dtoClassName * @param array|null $objectIdentifiers * @param array>|null $objectsMapping + * @param list $mappingPath * @return array{'objectIdentifiers': array, "objectsMapping": array>} */ - private function createMappingRecursive(string $dtoClassName, ?array& $objectIdentifiers = null, ?array& $objectsMapping = null): array + private function createMappingRecursive(string $dtoClassName, ?array& $objectIdentifiers = null, ?array& $objectsMapping = null, array $mappingPath = []): array { + if (in_array($dtoClassName, $mappingPath, true)) { + throw new MappingCreationException('Circular ReferenceArray mapping detected: ' . implode(' -> ', [...$mappingPath, $dtoClassName])); + } + + $mappingPath[] = $dtoClassName; + if($objectIdentifiers === null) $objectIdentifiers = []; if($objectsMapping === null) $objectsMapping = []; @@ -80,6 +90,10 @@ private function createMappingRecursive(string $dtoClassName, ?array& $objectIde $reflectionClass = new ReflectionClass($dtoClassName); + if (!$reflectionClass->isInstantiable()) { + throw new MappingCreationException('Class "' . $dtoClassName . '" is not instantiable.'); + } + $constructor = $reflectionClass->getConstructor(); if($constructor === null) { @@ -92,8 +106,9 @@ private function createMappingRecursive(string $dtoClassName, ?array& $objectIde foreach ($reflectionClass->getAttributes() as $attribute) { switch ($attribute->getName()) { case Identifier::class: - if (isset($attribute->getArguments()[0]) && $attribute->getArguments()[0] !== null) { - $objectIdentifiers[$dtoClassName] = $attribute->getArguments()[0]; + $mappedPropertyName = $this->getAttributeArgument($attribute, 'mappedPropertyName'); + if ($mappedPropertyName !== null && $mappedPropertyName !== '') { + $objectIdentifiers[$dtoClassName] = $mappedPropertyName; $identifiersCount++; } else { throw new MappingCreationException('The Identifier attribute cannot be used without a property name when used as a Class attribute'); @@ -128,19 +143,41 @@ private function createMappingRecursive(string $dtoClassName, ?array& $objectIde throw new MappingCreationException($reflectionClass->getName().': property '.$propertyName.' cannot be readonly as it is non-scalar and '.static::class.' needs to access it after object instantiation.'); } } - $objectsMapping[$dtoClassName][$propertyName] = (string)$attribute->getArguments()[0]; if($attribute->getName() === ReferenceArray::class) { - $this->createMappingRecursive($attribute->getArguments()[0], $objectIdentifiers, $objectsMapping); + $referenceClassName = $this->getAttributeArgument($attribute, 'referenceClassName'); + if($referenceClassName === null || $referenceClassName === '') { + throw new MappingCreationException('Invalid ReferenceArray attribute for '.$dtoClassName.'::$'.$propertyName); + } + if(!class_exists($referenceClassName)) { + throw new MappingCreationException($referenceClassName.' is not a valid class name'); + } + /** @var class-string $referenceClassName */ + $objectsMapping[$dtoClassName][$propertyName] = $referenceClassName; + $this->createMappingRecursive($referenceClassName, $objectIdentifiers, $objectsMapping, $mappingPath); + } else { + $mappedPropertyName = $this->getAttributeArgument($attribute, 'mappedPropertyName'); + if($mappedPropertyName === null || $mappedPropertyName === '') { + throw new MappingCreationException('Invalid ScalarArray attribute for '.$dtoClassName.'::$'.$propertyName); + } + $objectsMapping[$dtoClassName][$propertyName] = $mappedPropertyName; } continue 2; } else if ($attribute->getName() === Identifier::class) { $identifiersCount++; $isIdentifier = true; - if(isset($attribute->getArguments()[0]) && $attribute->getArguments()[0] !== null) { - $columnName = $attribute->getArguments()[0]; + $mappedPropertyName = $this->getAttributeArgument($attribute, 'mappedPropertyName'); + if($mappedPropertyName === '') { + throw new MappingCreationException('Invalid Identifier attribute for '.$dtoClassName.'::$'.$propertyName); + } + if($mappedPropertyName !== null) { + $columnName = $mappedPropertyName; } } else if ($attribute->getName() === Scalar::class) { - $columnName = $attribute->getArguments()[0]; + $mappedPropertyName = $this->getAttributeArgument($attribute, 'mappedPropertyName'); + if($mappedPropertyName === null || $mappedPropertyName === '') { + throw new MappingCreationException('Invalid Scalar attribute for '.$dtoClassName.'::$'.$propertyName); + } + $columnName = $mappedPropertyName; } } @@ -155,7 +192,7 @@ private function createMappingRecursive(string $dtoClassName, ?array& $objectIde if($identifiersCount !== 1) { throw new MappingCreationException($dtoClassName.' does not contain exactly one #[Identifier] attribute.'); } - + $uniqueCheck = []; foreach ($objectIdentifiers as $key => $value) { if (isset($uniqueCheck[$value])) { @@ -171,6 +208,24 @@ private function createMappingRecursive(string $dtoClassName, ?array& $objectIde ]; } + /** + * @param ReflectionAttribute $attribute + */ + private function getAttributeArgument(ReflectionAttribute $attribute, string $argumentName): ?string + { + $arguments = $attribute->getArguments(); + + if (array_key_exists($argumentName, $arguments)) { + return $arguments[$argumentName] === null ? null : (string)$arguments[$argumentName]; + } + + if (array_key_exists(0, $arguments)) { + return $arguments[0] === null ? null : (string)$arguments[0]; + } + + return null; + } + private function transformPropertyName(string $propertyName, NameTransformation $transformation): string { if ($transformation->snakeCaseColumns) { @@ -200,33 +255,49 @@ public function map(string $dtoClassName, iterable $data): array { if (!array_key_exists($identifier, $row)) { throw new MappingException('Identifier not found: ' . $identifier); } - if ($row[$identifier] !== null && !isset($objectsMap[$identifier][$row[$identifier]])) { - $constructorValues = []; - foreach ($this->objectsMapping[$dtoClassName][$objectClass] as $objectProperty => $foreignObjectClassOrIdentifier) { - if($foreignObjectClassOrIdentifier !== null) { - if (isset($this->objectsMapping[$dtoClassName][$foreignObjectClassOrIdentifier])) { - // Handles ReferenceArray attribute - $foreignIdentifier = $this->objectIdentifiers[$dtoClassName][$foreignObjectClassOrIdentifier]; - if($row[$foreignIdentifier] !== null) { - $referencesMap[$objectClass][$row[$identifier]][$objectProperty][$row[$foreignIdentifier]] = $objectsMap[$foreignObjectClassOrIdentifier][$row[$foreignIdentifier]]; - } - } else { - // Handles ScalarArray attribute - if($row[$foreignObjectClassOrIdentifier] !== null) { - $referencesMap[$objectClass][$row[$identifier]][$objectProperty][] = $row[$foreignObjectClassOrIdentifier]; - } + if ($row[$identifier] === null) { + continue; + } + + $objectIdentifier = $row[$identifier]; + $isNewObject = !isset($objectsMap[$objectClass][$objectIdentifier]); + $constructorValues = []; + + foreach ($this->objectsMapping[$dtoClassName][$objectClass] as $objectProperty => $foreignObjectClassOrIdentifier) { + if($foreignObjectClassOrIdentifier !== null) { + if (isset($this->objectsMapping[$dtoClassName][$foreignObjectClassOrIdentifier])) { + // Handles ReferenceArray attribute + $foreignIdentifier = $this->objectIdentifiers[$dtoClassName][$foreignObjectClassOrIdentifier]; + if (!array_key_exists($foreignIdentifier, $row)) { + throw new MappingException('Identifier not found: ' . $foreignIdentifier); + } + if($row[$foreignIdentifier] !== null) { + $referencesMap[$objectClass][$objectIdentifier][$objectProperty][$row[$foreignIdentifier]] = $objectsMap[$foreignObjectClassOrIdentifier][$row[$foreignIdentifier]]; } - $constructorValues[] = []; } else { - if(!array_key_exists($objectProperty, $row)) { - throw new MappingException('Data does not contain required property: ' . $objectProperty); + // Handles ScalarArray attribute + if(!array_key_exists($foreignObjectClassOrIdentifier, $row)) { + throw new MappingException('Data does not contain required property: ' . $foreignObjectClassOrIdentifier); } - $constructorValues[] = $row[$objectProperty]; + if($row[$foreignObjectClassOrIdentifier] !== null) { + $referencesMap[$objectClass][$objectIdentifier][$objectProperty][] = $row[$foreignObjectClassOrIdentifier]; + } + } + if ($isNewObject) { + $constructorValues[] = []; + } + } else if ($isNewObject) { + if(!array_key_exists($objectProperty, $row)) { + throw new MappingException('Data does not contain required property: ' . $objectProperty); } + $constructorValues[] = $row[$objectProperty]; } + } + + if ($isNewObject) { try { - $objectsMap[$objectClass][$row[$identifier]] = new $objectClass(...$constructorValues); - } catch (\TypeError $e) { + $objectsMap[$objectClass][$objectIdentifier] = new $objectClass(...$constructorValues); + } catch (TypeError $e) { throw new MappingException('Cannot construct object: '.$e->getMessage()); } } diff --git a/tests/Examples/Invalid/Circular/CycleLeafDTO.php b/tests/Examples/Invalid/Circular/CycleLeafDTO.php new file mode 100644 index 0000000..6e45af6 --- /dev/null +++ b/tests/Examples/Invalid/Circular/CycleLeafDTO.php @@ -0,0 +1,21 @@ + $roots + */ + public function __construct( + #[Identifier] + public int $leafId, + #[ReferenceArray(CycleRootDTO::class)] + public array $roots + ) { + } +} diff --git a/tests/Examples/Invalid/Circular/CycleRootDTO.php b/tests/Examples/Invalid/Circular/CycleRootDTO.php new file mode 100644 index 0000000..a3db76f --- /dev/null +++ b/tests/Examples/Invalid/Circular/CycleRootDTO.php @@ -0,0 +1,21 @@ + $leafs + */ + public function __construct( + #[Identifier] + public int $rootId, + #[ReferenceArray(CycleLeafDTO::class)] + public array $leafs + ) { + } +} diff --git a/tests/Examples/Invalid/RootAbstractDTO.php b/tests/Examples/Invalid/RootAbstractDTO.php new file mode 100644 index 0000000..833293d --- /dev/null +++ b/tests/Examples/Invalid/RootAbstractDTO.php @@ -0,0 +1,17 @@ + $children + */ + public function __construct( + #[Identifier] + #[Scalar('object1_id')] + public int $id, + // @phpstan-ignore-next-line + #[ReferenceArray(invalidArgumentName: LeafDTO::class)] + public array $children, + ) {} +} diff --git a/tests/Examples/Invalid/RootDTOWithInvalidReferenceArrayClass.php b/tests/Examples/Invalid/RootDTOWithInvalidReferenceArrayClass.php new file mode 100644 index 0000000..abb833f --- /dev/null +++ b/tests/Examples/Invalid/RootDTOWithInvalidReferenceArrayClass.php @@ -0,0 +1,23 @@ + $children + */ + public function __construct( + #[Identifier] + #[Scalar('object1_id')] + public int $id, + // @phpstan-ignore-next-line + #[ReferenceArray(referenceClassName: 'This\\Class\\Does\\Not\\Exist')] + public array $children, + ) {} +} diff --git a/tests/Examples/Invalid/RootDTOWithInvalidScalarArrayAttribute.php b/tests/Examples/Invalid/RootDTOWithInvalidScalarArrayAttribute.php new file mode 100644 index 0000000..b0b2b42 --- /dev/null +++ b/tests/Examples/Invalid/RootDTOWithInvalidScalarArrayAttribute.php @@ -0,0 +1,23 @@ + $children + */ + public function __construct( + #[Identifier] + #[Scalar('object1_id')] + public int $id, + // @phpstan-ignore-next-line + #[ScalarArray(invalidArgumentName: 'object2_id')] + public array $children, + ) {} +} diff --git a/tests/Examples/Invalid/RootDTOWithInvalidScalarAttribute.php b/tests/Examples/Invalid/RootDTOWithInvalidScalarAttribute.php new file mode 100644 index 0000000..db4a3b9 --- /dev/null +++ b/tests/Examples/Invalid/RootDTOWithInvalidScalarAttribute.php @@ -0,0 +1,17 @@ + $children + * @param array $tagIds + */ + public function __construct( + #[Scalar(mappedPropertyName: 'parent_name')] + public string $name, + #[ReferenceArray(referenceClassName: NamedArgumentsChildDTO::class)] + public array $children, + #[ScalarArray(mappedPropertyName: 'tag_id')] + public array $tagIds, + ) {} +} diff --git a/tests/FlatMapperCreateMappingTest.php b/tests/FlatMapperCreateMappingTest.php index 0b82594..b333b26 100644 --- a/tests/FlatMapperCreateMappingTest.php +++ b/tests/FlatMapperCreateMappingTest.php @@ -8,9 +8,17 @@ use PHPUnit\Framework\TestCase; use Pixelshaped\FlatMapperBundle\Exception\MappingCreationException; use Pixelshaped\FlatMapperBundle\FlatMapper; +use Pixelshaped\FlatMapperBundle\Tests\Examples\Invalid\Circular\CycleRootDTO; use Pixelshaped\FlatMapperBundle\Tests\Examples\Invalid\NameTransformation\InvalidNameTransformationDTO; use Pixelshaped\FlatMapperBundle\Tests\Examples\Invalid\RootDTO as InvalidRootDTO; use Pixelshaped\FlatMapperBundle\Tests\Examples\Invalid\RootDTOWithEmptyClassIdentifier; +use Pixelshaped\FlatMapperBundle\Tests\Examples\Invalid\RootDTOWithEmptyStringClassIdentifier; +use Pixelshaped\FlatMapperBundle\Tests\Examples\Invalid\RootDTOWithEmptyStringPropertyIdentifier; +use Pixelshaped\FlatMapperBundle\Tests\Examples\Invalid\RootAbstractDTO; +use Pixelshaped\FlatMapperBundle\Tests\Examples\Invalid\RootDTOWithInvalidReferenceArrayAttribute; +use Pixelshaped\FlatMapperBundle\Tests\Examples\Invalid\RootDTOWithInvalidReferenceArrayClass; +use Pixelshaped\FlatMapperBundle\Tests\Examples\Invalid\RootDTOWithInvalidScalarArrayAttribute; +use Pixelshaped\FlatMapperBundle\Tests\Examples\Invalid\RootDTOWithInvalidScalarAttribute; use Pixelshaped\FlatMapperBundle\Tests\Examples\Invalid\RootDTOWithNoIdentifier; use Pixelshaped\FlatMapperBundle\Tests\Examples\Invalid\RootDTOWithoutConstructor; use Pixelshaped\FlatMapperBundle\Tests\Examples\Invalid\RootDTOWithReadonlyClassModifier; @@ -22,6 +30,7 @@ #[CoversMethod(FlatMapper::class, 'createMapping')] #[CoversMethod(FlatMapper::class, 'createMappingRecursive')] +#[CoversMethod(FlatMapper::class, 'getAttributeArgument')] #[CoversMethod(FlatMapper::class, 'setCacheService')] #[CoversClass(MappingCreationException::class)] class FlatMapperCreateMappingTest extends TestCase @@ -113,6 +122,13 @@ public function testCreateMappingWithNoConstructorAsserts(): void (new FlatMapper())->createMapping(RootDTOWithoutConstructor::class); } + public function testCreateMappingWithAbstractClassAsserts(): void + { + $this->expectException(MappingCreationException::class); + $this->expectExceptionMessageMatches('/is not instantiable/'); + (new FlatMapper())->createMapping(RootAbstractDTO::class); + } + public function testCreateMappingWithEmptyClassIdentifierAsserts(): void { $this->expectException(MappingCreationException::class); @@ -120,10 +136,59 @@ public function testCreateMappingWithEmptyClassIdentifierAsserts(): void (new FlatMapper())->createMapping(RootDTOWithEmptyClassIdentifier::class); } + public function testCreateMappingWithEmptyStringClassIdentifierAsserts(): void + { + $this->expectException(MappingCreationException::class); + $this->expectExceptionMessageMatches("/The Identifier attribute cannot be used without a property name when used as a Class attribute/"); + (new FlatMapper())->createMapping(RootDTOWithEmptyStringClassIdentifier::class); + } + + public function testCreateMappingWithEmptyStringPropertyIdentifierAsserts(): void + { + $this->expectException(MappingCreationException::class); + $this->expectExceptionMessageMatches('/Invalid Identifier attribute/'); + (new FlatMapper())->createMapping(RootDTOWithEmptyStringPropertyIdentifier::class); + } + public function testCreateMappingWithInvalidNameTransformationAsserts(): void { $this->expectException(MappingCreationException::class); $this->expectExceptionMessageMatches("/Invalid NameTransformation attribute/"); (new FlatMapper())->createMapping(InvalidNameTransformationDTO::class); } + + public function testCreateMappingWithCircularReferenceArrayAsserts(): void + { + $this->expectException(MappingCreationException::class); + $this->expectExceptionMessageMatches('/Circular ReferenceArray mapping detected/'); + (new FlatMapper())->createMapping(CycleRootDTO::class); + } + + public function testCreateMappingWithInvalidReferenceArrayClassAsserts(): void + { + $this->expectException(MappingCreationException::class); + $this->expectExceptionMessageMatches('/This\\\\Class\\\\Does\\\\Not\\\\Exist is not a valid class name/'); + (new FlatMapper())->createMapping(RootDTOWithInvalidReferenceArrayClass::class); + } + + public function testCreateMappingWithInvalidReferenceArrayAttributeAsserts(): void + { + $this->expectException(MappingCreationException::class); + $this->expectExceptionMessageMatches('/Invalid ReferenceArray attribute/'); + (new FlatMapper())->createMapping(RootDTOWithInvalidReferenceArrayAttribute::class); + } + + public function testCreateMappingWithInvalidScalarArrayAttributeAsserts(): void + { + $this->expectException(MappingCreationException::class); + $this->expectExceptionMessageMatches('/Invalid ScalarArray attribute/'); + (new FlatMapper())->createMapping(RootDTOWithInvalidScalarArrayAttribute::class); + } + + public function testCreateMappingWithInvalidScalarAttributeAsserts(): void + { + $this->expectException(MappingCreationException::class); + $this->expectExceptionMessageMatches('/Invalid Scalar attribute/'); + (new FlatMapper())->createMapping(RootDTOWithInvalidScalarAttribute::class); + } } diff --git a/tests/FlatMapperTest.php b/tests/FlatMapperTest.php index 71e4566..ac5d9ae 100644 --- a/tests/FlatMapperTest.php +++ b/tests/FlatMapperTest.php @@ -19,6 +19,8 @@ use Pixelshaped\FlatMapperBundle\Tests\Examples\Valid\NameTransformation\OrderDTO; use Pixelshaped\FlatMapperBundle\Tests\Examples\Valid\NameTransformation\PersonDTO; use Pixelshaped\FlatMapperBundle\Tests\Examples\Valid\NameTransformation\ProductDTO as NameTransformationProductDTO; +use Pixelshaped\FlatMapperBundle\Tests\Examples\Valid\NamedArguments\NamedArgumentsChildDTO; +use Pixelshaped\FlatMapperBundle\Tests\Examples\Valid\NamedArguments\NamedArgumentsParentDTO; use Pixelshaped\FlatMapperBundle\Tests\Examples\Valid\ReferenceArray\AuthorDTO; use Pixelshaped\FlatMapperBundle\Tests\Examples\Valid\ReferenceArray\BookDTO; use Pixelshaped\FlatMapperBundle\Tests\Examples\Valid\ScalarArray\ScalarArrayDTO; @@ -56,6 +58,30 @@ public function testMappingDataWithMissingForeignIdentifierPropertyAsserts(): vo ((new FlatMapper())->map(AuthorDTO::class, $results)); } + public function testMappingDataWithMissingForeignIdentifierPropertyAssertsWhenRootIdentifierIsCheckedFirst(): void + { + $this->expectException(MappingException::class); + $this->expectExceptionMessageMatches('/Identifier not found: book_id/'); + + $flatMapper = new FlatMapper(); + $flatMapper->createMapping(AuthorDTO::class); + + $objectIdentifiersProperty = (new \ReflectionClass($flatMapper))->getProperty('objectIdentifiers'); + /** @var array> $objectIdentifiers */ + $objectIdentifiers = $objectIdentifiersProperty->getValue($flatMapper); + $objectIdentifiers[AuthorDTO::class] = [ + AuthorDTO::class => 'author_id', + BookDTO::class => 'book_id', + ]; + $objectIdentifiersProperty->setValue($flatMapper, $objectIdentifiers); + + $results = [ + ['author_id' => 1, 'author_name' => 'Alice Brian', 'book_name' => 'Travelling as a group', 'book_publisher_name' => 'TravelBooks'], + ]; + + $flatMapper->map(AuthorDTO::class, $results); + } + public function testMappingDataWithBadlyNamedPropertyAsserts(): void { $this->expectException(MappingException::class); @@ -84,6 +110,18 @@ public function testMappingDataWithMissingPropertyAsserts(): void ((new FlatMapper())->map(AuthorDTO::class, $results)); } + public function testMappingDataWithMissingScalarArrayPropertyAsserts(): void + { + $this->expectException(MappingException::class); + $this->expectExceptionMessageMatches('/Data does not contain required property: object2_id/'); + + $results = [ + ['object1_id' => 1, 'object1_name' => 'Root 1'], + ]; + + ((new FlatMapper())->map(ScalarArrayDTO::class, $results)); + } + public function testMappingDataWithBadConstructorTypeAsserts(): void { $this->expectException(MappingException::class); @@ -122,6 +160,20 @@ public function testMapValidNestedDTOs(): void ); } + public function testMapDoesNotRecreateObjectForDuplicateIdentifierRows(): void + { + $results = [ + ['author_id' => 1, 'author_name' => 'Alice First', 'book_id' => 1, 'book_name' => 'Book A', 'book_publisher_name' => 'Pub A'], + ['author_id' => 1, 'author_name' => 'Alice Overwritten', 'book_id' => 2, 'book_name' => 'Book B', 'book_publisher_name' => 'Pub B'], + ]; + + $flatMapperResults = ((new FlatMapper())->map(AuthorDTO::class, $results)); + + $this->assertArrayHasKey(1, $flatMapperResults); + $this->assertSame('Alice First', $flatMapperResults[1]->name); + $this->assertCount(2, $flatMapperResults[1]->books); + } + public function testMapValidScalarArrayDTO(): void { $results = [ @@ -458,6 +510,30 @@ public function testMapWithNameTransformationBackwardCompatibility(): void ); } + public function testMapWithNamedAttributeArguments(): void + { + $results = [ + ['parent_id' => 1, 'parent_name' => 'Parent 1', 'child_id' => 11, 'child_name' => 'Child 11', 'tag_id' => 500], + ['parent_id' => 1, 'parent_name' => 'Parent 1', 'child_id' => 12, 'child_name' => 'Child 12', 'tag_id' => 501], + ['parent_id' => 2, 'parent_name' => 'Parent 2', 'child_id' => null, 'child_name' => null, 'tag_id' => null], + ]; + + $flatMapperResults = ((new FlatMapper())->map(NamedArgumentsParentDTO::class, $results)); + + $parent1 = new NamedArgumentsParentDTO( + 'Parent 1', + [11 => new NamedArgumentsChildDTO(11, 'Child 11'), 12 => new NamedArgumentsChildDTO(12, 'Child 12')], + [500, 501] + ); + $parent2 = new NamedArgumentsParentDTO('Parent 2', [], []); + $handmadeResult = [1 => $parent1, 2 => $parent2]; + + $this->assertSame( + var_export($flatMapperResults, true), + var_export($handmadeResult, true) + ); + } + /** * @return list> */ diff --git a/tests/Functional/BundleFunctionalTest.php b/tests/Functional/BundleFunctionalTest.php index f49c527..b587fe8 100644 --- a/tests/Functional/BundleFunctionalTest.php +++ b/tests/Functional/BundleFunctionalTest.php @@ -3,10 +3,13 @@ namespace Pixelshaped\FlatMapperBundle\Tests\Functional; +use FilesystemIterator; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; use Pixelshaped\FlatMapperBundle\FlatMapper; use Pixelshaped\FlatMapperBundle\PixelshapedFlatMapperBundle; +use RecursiveDirectoryIterator; +use RecursiveIteratorIterator; use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\HttpKernel\Kernel; use Symfony\Contracts\Cache\CacheInterface; @@ -16,6 +19,29 @@ #[CoversClass(PixelshapedFlatMapperBundle::class)] class BundleFunctionalTest extends TestCase { + protected function setUp(): void + { + parent::setUp(); + $cacheDir = dirname(__DIR__, 2).'/var/cache/test'; + if (!is_dir($cacheDir)) { + return; + } + + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($cacheDir, FilesystemIterator::SKIP_DOTS), + RecursiveIteratorIterator::CHILD_FIRST + ); + /** @var \SplFileInfo $item */ + foreach ($iterator as $item) { + if ($item->isDir()) { + rmdir($item->getPathname()); + continue; + } + unlink($item->getPathname()); + } + rmdir($cacheDir); + } + public function testServiceWiring(): void { $kernel = new PixelshapedFlatMapperTestingKernel('test', true);