From 30a93ac88a85066ee12c2730dd9d74847ee252a1 Mon Sep 17 00:00:00 2001
From: Alexander Schnitzler <git@alexanderschnitzler.de>
Date: Sun, 5 Mar 2023 13:06:40 +0100
Subject: [PATCH] [BUGFIX] Respect TCA field foreign_default_sortby by extbase

Inside the extbase context child relations are always ordered
by the foreign_sortby field if set or by order of creation.
Even if the field foreign_default_sortby is set, it only
impacts the order of the backend view.

This patch brings the order of extbase results inline with
the order of the backend view.

Resolves: #64197
Releases: main, 11.5
Change-Id: I582576b5ac3741e2bbc4107664140fd9a2f63a16
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/77971
Tested-by: Benni Mack <benni@typo3.org>
Reviewed-by: Benni Mack <benni@typo3.org>
Tested-by: core-ci <typo3@b13.com>
---
 Build/phpstan/phpstan-baseline.neon           |  10 -
 .../Persistence/Generic/Mapper/ColumnMap.php  |  20 +
 .../Generic/Mapper/DataMapFactory.php         |   3 +-
 .../Persistence/Generic/Mapper/DataMapper.php |  43 +-
 .../TCA/tx_blogexample_domain_model_post.php  |   1 +
 .../Persistence/Fixtures/comments.csv         |   7 +
 .../Generic/Mapper/DataMapperTest.php         |  27 +
 .../Generic/Mapper/DataMapperTest.php         | 578 +++++-------------
 8 files changed, 243 insertions(+), 446 deletions(-)
 create mode 100644 typo3/sysext/extbase/Tests/Functional/Persistence/Fixtures/comments.csv

diff --git a/Build/phpstan/phpstan-baseline.neon b/Build/phpstan/phpstan-baseline.neon
index c1d225452636..2241f6af217b 100644
--- a/Build/phpstan/phpstan-baseline.neon
+++ b/Build/phpstan/phpstan-baseline.neon
@@ -3000,16 +3000,6 @@ parameters:
 			count: 1
 			path: ../../typo3/sysext/extbase/Tests/Unit/Object/Container/Fixtures/testclasses/t3lib_object_tests_cyclic2.php
 
-		-
-			message: "#^Call to static method PHPUnit\\\\Framework\\\\Assert\\:\\:assertNull\\(\\) with DateTime&PHPUnit\\\\Framework\\\\MockObject\\\\MockObject&TYPO3\\\\TestingFramework\\\\Core\\\\AccessibleObjectInterface will always evaluate to false\\.$#"
-			count: 1
-			path: ../../typo3/sysext/extbase/Tests/Unit/Persistence/Generic/Mapper/DataMapperTest.php
-
-		-
-			message: "#^Parameter \\#2 \\$identifier of method TYPO3\\\\CMS\\\\Extbase\\\\Persistence\\\\Generic\\\\Session\\:\\:registerObject\\(\\) expects string, int given\\.$#"
-			count: 1
-			path: ../../typo3/sysext/extbase/Tests/Unit/Persistence/Generic/Mapper/DataMapperTest.php
-
 		-
 			message: "#^Call to static method PHPUnit\\\\Framework\\\\Assert\\:\\:assertNull\\(\\) with object will always evaluate to false\\.$#"
 			count: 1
diff --git a/typo3/sysext/extbase/Classes/Persistence/Generic/Mapper/ColumnMap.php b/typo3/sysext/extbase/Classes/Persistence/Generic/Mapper/ColumnMap.php
index bb97d7b3538a..fe0ff6fe9e38 100644
--- a/typo3/sysext/extbase/Classes/Persistence/Generic/Mapper/ColumnMap.php
+++ b/typo3/sysext/extbase/Classes/Persistence/Generic/Mapper/ColumnMap.php
@@ -82,6 +82,13 @@ class ColumnMap
      */
     private $childTableName;
 
+    /**
+     * The name of the fields with direction the results from the child's table are sorted by default
+     *
+     * @see https://docs.typo3.org/m/typo3/reference-tca/main/en-us/ColumnsConfig/Type/Inline/Properties/ForeignDefaultSortby.html
+     */
+    private ?string $childTableDefaultSortings = null;
+
     /**
      * todo: Check if this property should support null. If not, set default value.
      * The name of the field the results from the child's table are sorted by
@@ -249,6 +256,19 @@ class ColumnMap
         return $this->childTableName;
     }
 
+    /**
+     * @param string|null $childTableDefaultSortings
+     */
+    public function setChildTableDefaultSortings(?string $childTableDefaultSortings): void
+    {
+        $this->childTableDefaultSortings = $childTableDefaultSortings;
+    }
+
+    public function getChildTableDefaultSortings(): ?string
+    {
+        return $this->childTableDefaultSortings;
+    }
+
     /**
      * @param string|null $childSortByFieldName
      */
diff --git a/typo3/sysext/extbase/Classes/Persistence/Generic/Mapper/DataMapFactory.php b/typo3/sysext/extbase/Classes/Persistence/Generic/Mapper/DataMapFactory.php
index f738df496b7c..c3dae6c06ab3 100644
--- a/typo3/sysext/extbase/Classes/Persistence/Generic/Mapper/DataMapFactory.php
+++ b/typo3/sysext/extbase/Classes/Persistence/Generic/Mapper/DataMapFactory.php
@@ -397,7 +397,7 @@ class DataMapFactory implements SingletonInterface
      * @param array|null $columnConfiguration The column configuration from $TCA
      * @return ColumnMap
      */
-    protected function setOneToManyRelation(ColumnMap $columnMap, array $columnConfiguration = null): ColumnMap
+    public function setOneToManyRelation(ColumnMap $columnMap, array $columnConfiguration = null): ColumnMap
     {
         // todo: this method should only be called with proper arguments which means that the TCA integrity check should
         // todo: take place outside this method.
@@ -409,6 +409,7 @@ class DataMapFactory implements SingletonInterface
         }
         // todo: don't update column map if value(s) isn't/aren't set.
         $columnMap->setChildSortByFieldName($columnConfiguration['foreign_sortby'] ?? null);
+        $columnMap->setChildTableDefaultSortings($columnConfiguration['foreign_default_sortby'] ?? null);
         $columnMap->setParentKeyFieldName($columnConfiguration['foreign_field'] ?? null);
         $columnMap->setParentTableFieldName($columnConfiguration['foreign_table_field'] ?? null);
         if (isset($columnConfiguration['foreign_match_fields']) && is_array($columnConfiguration['foreign_match_fields'])) {
diff --git a/typo3/sysext/extbase/Classes/Persistence/Generic/Mapper/DataMapper.php b/typo3/sysext/extbase/Classes/Persistence/Generic/Mapper/DataMapper.php
index 6c43585f6031..d48b9dcecad1 100644
--- a/typo3/sysext/extbase/Classes/Persistence/Generic/Mapper/DataMapper.php
+++ b/typo3/sysext/extbase/Classes/Persistence/Generic/Mapper/DataMapper.php
@@ -410,8 +410,8 @@ class DataMapper
         }
 
         if ($columnMap->getTypeOfRelation() === ColumnMap::RELATION_HAS_MANY) {
-            if ($columnMap->getChildSortByFieldName() !== null) {
-                $query->setOrderings([$columnMap->getChildSortByFieldName() => QueryInterface::ORDER_ASCENDING]);
+            if (null !== $orderings = $this->getOrderingsForColumnMap($columnMap)) {
+                $query->setOrderings($orderings);
             }
         } elseif ($columnMap->getTypeOfRelation() === ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY) {
             $query->setSource($this->getSource($parentObject, $propertyName));
@@ -423,6 +423,45 @@ class DataMapper
         return $query;
     }
 
+    /**
+     * Get orderings array for extbase query by columnMap
+     *
+     * @return array<string, string>|null
+     */
+    public function getOrderingsForColumnMap(ColumnMap $columnMap): ?array
+    {
+        if ($columnMap->getChildSortByFieldName() !== null) {
+            return [$columnMap->getChildSortByFieldName() => QueryInterface::ORDER_ASCENDING];
+        }
+
+        if ($columnMap->getChildTableDefaultSortings() === null) {
+            return null;
+        }
+
+        $orderings = [];
+        $fields = QueryHelper::parseOrderBy($columnMap->getChildTableDefaultSortings());
+        foreach ($fields as $field) {
+            $fieldName = $field[0] ?? null;
+            if ($fieldName === null) {
+                continue;
+            }
+
+            if (($fieldOrdering = $field[1] ?? null) === null) {
+                $orderings[$fieldName] = QueryInterface::ORDER_ASCENDING;
+                continue;
+            }
+
+            $fieldOrdering = strtoupper($fieldOrdering);
+            if (!in_array($fieldOrdering, [QueryInterface::ORDER_ASCENDING, QueryInterface::ORDER_DESCENDING], true)) {
+                $orderings[$fieldName] = QueryInterface::ORDER_ASCENDING;
+                continue;
+            }
+
+            $orderings[$fieldName] = $fieldOrdering;
+        }
+        return $orderings !== [] ? $orderings : null;
+    }
+
     /**
      * Builds and returns the constraint for multi value properties.
      *
diff --git a/typo3/sysext/extbase/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TCA/tx_blogexample_domain_model_post.php b/typo3/sysext/extbase/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TCA/tx_blogexample_domain_model_post.php
index 6a6be2309e28..bf82be4e93f4 100644
--- a/typo3/sysext/extbase/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TCA/tx_blogexample_domain_model_post.php
+++ b/typo3/sysext/extbase/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TCA/tx_blogexample_domain_model_post.php
@@ -196,6 +196,7 @@ return [
                 'type' => 'inline',
                 'foreign_table' => 'tx_blogexample_domain_model_comment',
                 'foreign_field' => 'post',
+                'foreign_default_sortby' => 'uid desc',
                 'size' => 10,
                 'autoSizeMax' => 30,
                 'multiple' => 0,
diff --git a/typo3/sysext/extbase/Tests/Functional/Persistence/Fixtures/comments.csv b/typo3/sysext/extbase/Tests/Functional/Persistence/Fixtures/comments.csv
new file mode 100644
index 000000000000..45ca83f8d01f
--- /dev/null
+++ b/typo3/sysext/extbase/Tests/Functional/Persistence/Fixtures/comments.csv
@@ -0,0 +1,7 @@
+tx_blogexample_domain_model_comment,,,,,
+,uid,pid,post,author,content,date
+,1,0,1,1,Comment1,"2023-01-05 00:00:00"
+,2,0,1,2,Comment2,"2023-01-06 00:00:00"
+,3,0,1,1,Comment3,"2023-02-23 00:00:00"
+,4,0,1,3,Comment4,"2023-02-25 00:00:00"
+,5,0,1,2,Comment5,"2023-03-08 00:00:00"
diff --git a/typo3/sysext/extbase/Tests/Functional/Persistence/Generic/Mapper/DataMapperTest.php b/typo3/sysext/extbase/Tests/Functional/Persistence/Generic/Mapper/DataMapperTest.php
index 14bcc6788e47..a54a1eb47840 100644
--- a/typo3/sysext/extbase/Tests/Functional/Persistence/Generic/Mapper/DataMapperTest.php
+++ b/typo3/sysext/extbase/Tests/Functional/Persistence/Generic/Mapper/DataMapperTest.php
@@ -17,9 +17,12 @@ declare(strict_types=1);
 
 namespace TYPO3\CMS\Extbase\Tests\Functional\Persistence\Generic\Mapper;
 
+use ExtbaseTeam\BlogExample\Domain\Model\Comment;
 use ExtbaseTeam\BlogExample\Domain\Model\DateExample;
 use ExtbaseTeam\BlogExample\Domain\Model\DateTimeImmutableExample;
+use ExtbaseTeam\BlogExample\Domain\Model\Post;
 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
+use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper;
 use TYPO3\CMS\Extbase\Persistence\Generic\PersistenceManager;
 use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
 
@@ -161,4 +164,28 @@ class DataMapperTest extends FunctionalTestCase
 
         self::assertSame($date->getTimestamp(), $subject->getDatetimeImmutableDatetime()->getTimestamp());
     }
+
+    /**
+     * @test
+     */
+    public function fetchRelatedRespectsForeignDefaultSortByTCAConfiguration(): void
+    {
+        // Arrange
+        $this->importCSVDataSet('typo3/sysext/extbase/Tests/Functional/Persistence/Fixtures/posts.csv');
+        $this->importCSVDataSet('typo3/sysext/extbase/Tests/Functional/Persistence/Fixtures/comments.csv');
+
+        $dataMapper = $this->get(DataMapper::class);
+
+        $post = new Post();
+        $post->_setProperty('uid', 1);
+
+        // Act
+        $comments = $dataMapper->fetchRelated($post, 'comments', '5', false)->toArray();
+
+        // Assert
+        self::assertSame(
+            [5, 4, 3, 2, 1], // foreign_default_sortby is set to uid desc, see
+            array_map(fn (Comment $comment) => $comment->getUid(), $comments)
+        );
+    }
 }
diff --git a/typo3/sysext/extbase/Tests/Unit/Persistence/Generic/Mapper/DataMapperTest.php b/typo3/sysext/extbase/Tests/Unit/Persistence/Generic/Mapper/DataMapperTest.php
index 82f1ba515d41..950c31d13973 100644
--- a/typo3/sysext/extbase/Tests/Unit/Persistence/Generic/Mapper/DataMapperTest.php
+++ b/typo3/sysext/extbase/Tests/Unit/Persistence/Generic/Mapper/DataMapperTest.php
@@ -17,523 +17,235 @@ declare(strict_types=1);
 
 namespace TYPO3\CMS\Extbase\Tests\Unit\Persistence\Generic\Mapper;
 
-use PHPUnit\Framework\MockObject\MockObject;
-use Prophecy\PhpUnit\ProphecyTrait;
-use Psr\Container\ContainerInterface;
 use Psr\EventDispatcher\EventDispatcherInterface;
-use TYPO3\CMS\Core\Cache\Frontend\NullFrontend;
-use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
-use TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface;
-use TYPO3\CMS\Extbase\Object\Container\Container;
-use TYPO3\CMS\Extbase\Object\ObjectManagerInterface;
-use TYPO3\CMS\Extbase\Persistence\Generic\Exception\UnexpectedTypeException;
-use TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy;
+use TYPO3\CMS\Core\Cache\CacheManager;
+use TYPO3\CMS\Extbase\Configuration\ConfigurationManager;
+use TYPO3\CMS\Extbase\Object\ObjectManager;
+use TYPO3\CMS\Extbase\Persistence\ClassesConfiguration;
 use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap;
-use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMap;
 use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapFactory;
 use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper;
-use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\Exception\UnknownPropertyTypeException;
 use TYPO3\CMS\Extbase\Persistence\Generic\Qom\QueryObjectModelFactory;
-use TYPO3\CMS\Extbase\Persistence\Generic\QueryFactoryInterface;
+use TYPO3\CMS\Extbase\Persistence\Generic\QueryFactory;
 use TYPO3\CMS\Extbase\Persistence\Generic\Session;
-use TYPO3\CMS\Extbase\Reflection\ClassSchema;
+use TYPO3\CMS\Extbase\Persistence\QueryInterface;
 use TYPO3\CMS\Extbase\Reflection\ReflectionService;
-use TYPO3\CMS\Extbase\Tests\Unit\Persistence\Generic\Mapper\Fixture\DummyChildEntity;
-use TYPO3\CMS\Extbase\Tests\Unit\Persistence\Generic\Mapper\Fixture\DummyEntity;
-use TYPO3\CMS\Extbase\Tests\Unit\Persistence\Generic\Mapper\Fixture\DummyParentEntity;
-use TYPO3\TestingFramework\Core\AccessibleObjectInterface;
 use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
 
 class DataMapperTest extends UnitTestCase
 {
-    use ProphecyTrait;
+    protected ColumnMap $columnMap;
+    protected DataMapFactory $dataMapFactory;
+    protected DataMapper $dataMapper;
 
-    /**
-     * This test does not actually test anything rather than map calls both mocked methods getTargetType and mapSingleRow
-     * while completely ignoring the result of the method.
-     * @todo: Cover this functionality by a functional test
-     *
-     * @test
-     */
-    public function mapMapsArrayToObjectByCallingmapToObject(): void
+    protected function setUp(): void
     {
-        $rows = [['uid' => '1234']];
-        $object = new \stdClass();
-
-        $dataMapper = $this->getMockBuilder(DataMapper::class)
-            ->setConstructorArgs([
-                $this->createMock(ReflectionService::class),
-                $this->createMock(QueryObjectModelFactory::class),
-                $this->createMock(Session::class),
-                $this->createMock(DataMapFactory::class),
-                $this->createMock(QueryFactoryInterface::class),
-                $this->createMock(ObjectManagerInterface::class),
-                $this->createMock(EventDispatcherInterface::class),
-            ])
-            ->onlyMethods(['mapSingleRow', 'getTargetType'])
-            ->getMock();
-
-        $dataMapper->method('getTargetType')->willReturnArgument(1);
-        $dataMapper->expects(self::once())->method('mapSingleRow')->with($rows[0])->willReturn($object);
-
-        $dataMapper->map(get_class($object), $rows);
+        parent::setUp();
+
+        $this->columnMap = new ColumnMap('foo', 'foo');
+
+        $this->dataMapFactory = new DataMapFactory(
+            $this->createMock(ReflectionService::class),
+            $this->createMock(ConfigurationManager::class),
+            $this->createMock(CacheManager::class),
+            $this->createMock(ClassesConfiguration::class),
+            'foo'
+        );
+
+        $this->dataMapper = new DataMapper(
+            $this->createMock(ReflectionService::class),
+            $this->createMock(QueryObjectModelFactory::class),
+            $this->createMock(Session::class),
+            $this->dataMapFactory,
+            $this->createMock(QueryFactory::class),
+            $this->createMock(ObjectManager::class),
+            $this->createMock(EventDispatcherInterface::class),
+        );
     }
 
     /**
-     * This test does not actually test anything rather than mapSingleRow delegates functionality to
-     * the persistence session which is a mock itself.
-     * @todo: Cover this functionality by a functional test
-     *
      * @test
      */
-    public function mapSingleRowReturnsObjectFromPersistenceSessionIfAvailable(): void
+    public function getOrderingsForColumnMapReturnsNullIfNeitherForeignSortByNorForeignDefaultSortByAreSet(): void
     {
-        $row = ['uid' => '1234'];
-        $object = new \stdClass();
-        $persistenceSession = $this->createMock(Session::class);
-        $persistenceSession->expects(self::once())->method('hasIdentifier')->with('1234')->willReturn(true);
-        $persistenceSession->expects(self::once())->method('getObjectByIdentifier')->with('1234')->willReturn($object);
-
-        $dataMapper = $this->getAccessibleMock(
-            DataMapper::class,
-            ['dummy'],
+        // Arrange
+        $this->dataMapFactory->setOneToManyRelation(
+            $this->columnMap,
             [
-                $this->createMock(ReflectionService::class),
-                $this->createMock(QueryObjectModelFactory::class),
-                $persistenceSession,
-                $this->createMock(DataMapFactory::class),
-                $this->createMock(QueryFactoryInterface::class),
-                $this->createMock(ObjectManagerInterface::class),
-                $this->createMock(EventDispatcherInterface::class),
+                'foreign_table' => 'tx_myextension_bar',
             ]
         );
 
-        $dataMapper->_call('mapSingleRow', get_class($object), $row);
+        // Act
+        $orderings = $this->dataMapper->getOrderingsForColumnMap($this->columnMap);
+
+        // Assert
+        self::assertNull($orderings);
     }
 
     /**
-     * This test has a far too complex setup to test a single unit. This actually is a functional test, accomplished
-     * by mocking the whole dependency chain. This test only tests code structure while it should test functionality.
-     * @todo: Cover this functionality by a functional test
-     *
      * @test
      */
-    public function thawPropertiesSetsPropertyValues(): void
+    public function getOrderingsForColumnMapReturnsNullIfForeignDefaultSortByIsEmpty(): void
     {
-        $className = DummyEntity::class;
-        $object = new DummyEntity();
-        $row = [
-            'uid' => '1234',
-            'firstProperty' => 'firstValue',
-            'secondProperty' => 1234,
-            'thirdProperty' => 1.234,
-            'fourthProperty' => false,
-            'uninitializedStringProperty' => 'foo',
-            'uninitializedDateTimeProperty' => 0,
-            'uninitializedMandatoryDateTimeProperty' => 0,
-            'initializedDateTimeProperty' => 0,
-        ];
-        $columnMaps = [
-            'uid' => new ColumnMap('uid', 'uid'),
-            'pid' => new ColumnMap('pid', 'pid'),
-            'firstProperty' => new ColumnMap('firstProperty', 'firstProperty'),
-            'secondProperty' => new ColumnMap('secondProperty', 'secondProperty'),
-            'thirdProperty' => new ColumnMap('thirdProperty', 'thirdProperty'),
-            'fourthProperty' => new ColumnMap('fourthProperty', 'fourthProperty'),
-            'uninitializedStringProperty' => new ColumnMap('uninitializedStringProperty', 'uninitializedStringProperty'),
-            'uninitializedDateTimeProperty' => new ColumnMap('uninitializedDateTimeProperty', 'uninitializedDateTimeProperty'),
-            'uninitializedMandatoryDateTimeProperty' => new ColumnMap('uninitializedMandatoryDateTimeProperty', 'uninitializedMandatoryDateTimeProperty'),
-            'initializedDateTimeProperty' => new ColumnMap('initializedDateTimeProperty', 'initializedDateTimeProperty'),
-        ];
-        $dataMap = $this->getAccessibleMock(DataMap::class, ['dummy'], [$className, $className]);
-        $dataMap->_set('columnMaps', $columnMaps);
-        $dataMaps = [
-            $className => $dataMap,
-        ];
-        $classSchema = new ClassSchema($className);
-        $mockReflectionService = $this->getMockBuilder(ReflectionService::class)
-            ->setConstructorArgs([new NullFrontend('extbase'), 'ClassSchemata'])
-            ->onlyMethods(['getClassSchema'])
-            ->getMock();
-        $mockReflectionService->method('getClassSchema')->willReturn($classSchema);
-        $dataMapFactory = $this->getAccessibleMock(DataMapFactory::class, ['dummy'], [], '', false);
-        $dataMapFactory->_set('dataMaps', $dataMaps);
-        $dataMapper = $this->getAccessibleMock(
-            DataMapper::class,
-            ['dummy'],
+        // Arrange
+        $this->dataMapFactory->setOneToManyRelation(
+            $this->columnMap,
             [
-                $mockReflectionService,
-                $this->createMock(QueryObjectModelFactory::class),
-                $this->createMock(Session::class),
-                $dataMapFactory,
-                $this->createMock(QueryFactoryInterface::class),
-                $this->createMock(ObjectManagerInterface::class),
-                $this->createMock(EventDispatcherInterface::class),
+                'foreign_table' => 'tx_myextension_bar',
+                'foreign_default_sortby' => '',
             ]
         );
-        $dataMapper->_call('thawProperties', $object, $row);
-
-        self::assertEquals('firstValue', $object->firstProperty);
-        self::assertEquals(1234, $object->secondProperty);
-        self::assertEquals(1.234, $object->thirdProperty);
-        self::assertFalse($object->fourthProperty);
-        self::assertSame('foo', $object->uninitializedStringProperty);
-        self::assertNull($object->uninitializedDateTimeProperty);
-        self::assertFalse(isset($object->uninitializedMandatoryDateTimeProperty));
-
-        // Property is initialized with "null", so isset would return false.
-        // Test, if property was "really" initialized.
-        $reflectionProperty = new \ReflectionProperty($object, 'initializedDateTimeProperty');
-        self::assertTrue($reflectionProperty->isInitialized($object));
+
+        // Act
+        $orderings = $this->dataMapper->getOrderingsForColumnMap($this->columnMap);
+
+        // Assert
+        self::assertNull($orderings);
     }
 
     /**
      * @test
      */
-    public function thawPropertiesThrowsExceptionOnUnknownPropertyType(): void
+    public function getOrderingsForColumnMapFallBackToAscendingOrdering(): void
     {
-        $className = DummyEntity::class;
-        $object = new DummyEntity();
-        $row = [
-            'uid' => '1234',
-            'unknownType' => 'What am I?',
-        ];
-        $columnMaps = [
-            'unknownType' => new ColumnMap('unknownType', 'unknownType'),
-        ];
-        $dataMap = $this->getAccessibleMock(DataMap::class, ['dummy'], [$className, $className]);
-        $dataMap->_set('columnMaps', $columnMaps);
-        $dataMaps = [
-            $className => $dataMap,
-        ];
-        $classSchema = new ClassSchema($className);
-        $mockReflectionService = $this->getMockBuilder(ReflectionService::class)
-            ->setConstructorArgs([new NullFrontend('extbase'), 'ClassSchemata'])
-            ->onlyMethods(['getClassSchema'])
-            ->getMock();
-        $mockReflectionService->method('getClassSchema')->willReturn($classSchema);
-        $dataMapFactory = $this->getAccessibleMock(DataMapFactory::class, ['dummy'], [], '', false);
-        $dataMapFactory->_set('dataMaps', $dataMaps);
-        $dataMapper = $this->getAccessibleMock(
-            DataMapper::class,
-            ['dummy'],
+        // Arrange
+        $this->dataMapFactory->setOneToManyRelation(
+            $this->columnMap,
             [
-                $mockReflectionService,
-                $this->createMock(QueryObjectModelFactory::class),
-                $this->createMock(Session::class),
-                $dataMapFactory,
-                $this->createMock(QueryFactoryInterface::class),
-                $this->createMock(ObjectManagerInterface::class),
-                $this->createMock(EventDispatcherInterface::class),
+                'foreign_table' => 'tx_myextension_bar',
+                'foreign_default_sortby' => 'pid invalid',
             ]
         );
-        $this->expectException(UnknownPropertyTypeException::class);
-        $dataMapper->_call('thawProperties', $object, $row);
-    }
 
-    /**
-     * Test if fetchRelatedEager method returns NULL when $fieldValue = '' and relation type == RELATION_HAS_ONE
-     *
-     * This is actually a functional test as it tests multiple units along with a very specific setup of dependencies.
-     * @todo: Cover this functionality by a functional test
-     *
-     * @test
-     */
-    public function fetchRelatedEagerReturnsNullForEmptyRelationHasOne(): void
-    {
-        $columnMap = new ColumnMap('columnName', 'propertyName');
-        $columnMap->setTypeOfRelation(ColumnMap::RELATION_HAS_ONE);
-        $dataMap = $this->getMockBuilder(DataMap::class)
-            ->onlyMethods(['getColumnMap'])
-            ->disableOriginalConstructor()
-            ->getMock();
-        $dataMap->method('getColumnMap')->willReturn($columnMap);
-        $dataMapper = $this->getAccessibleMock(DataMapper::class, ['getDataMap'], [], '', false);
-        $dataMapper->method('getDataMap')->willReturn($dataMap);
-        $result = $dataMapper->_call('fetchRelatedEager', $this->createMock(AbstractEntity::class), 'SomeName', '');
-        self::assertNull($result);
-    }
+        // Act
+        $orderings = $this->dataMapper->getOrderingsForColumnMap($this->columnMap);
 
-    /**
-     * Test if fetchRelatedEager method returns empty array when $fieldValue = '' and relation type != RELATION_HAS_ONE
-     *
-     * This is actually a functional test as it tests multiple units along with a very specific setup of dependencies.
-     * @todo: Cover this functionality by a functional test
-     *
-     * @test
-     */
-    public function fetchRelatedEagerReturnsEmptyArrayForEmptyRelationNotHasOne(): void
-    {
-        $columnMap = new ColumnMap('columnName', 'propertyName');
-        $columnMap->setTypeOfRelation(ColumnMap::RELATION_BELONGS_TO_MANY);
-        $dataMap = $this->getMockBuilder(DataMap::class)
-            ->onlyMethods(['getColumnMap'])
-            ->disableOriginalConstructor()
-            ->getMock();
-        $dataMap->method('getColumnMap')->willReturn($columnMap);
-        $dataMapper = $this->getAccessibleMock(DataMapper::class, ['getDataMap'], [], '', false);
-        $dataMapper->method('getDataMap')->willReturn($dataMap);
-        $result = $dataMapper->_call('fetchRelatedEager', $this->createMock(AbstractEntity::class), 'SomeName', '');
-        self::assertEquals([], $result);
-    }
-
-    /**
-     * Test if fetchRelatedEager method returns NULL when $fieldValue = ''
-     * and relation type == RELATION_HAS_ONE without calling fetchRelated
-     *
-     * This is actually a functional test as it tests multiple units along with a very specific setup of dependencies.
-     * @todo: Cover this functionality by a functional test
-     *
-     * @test
-     */
-    public function MapObjectToClassPropertyReturnsNullForEmptyRelationHasOne(): void
-    {
-        $columnMap = new ColumnMap('columnName', 'propertyName');
-        $columnMap->setTypeOfRelation(ColumnMap::RELATION_HAS_ONE);
-        $dataMap = $this->getMockBuilder(DataMap::class)
-            ->onlyMethods(['getColumnMap'])
-            ->disableOriginalConstructor()
-            ->getMock();
-        $dataMap->method('getColumnMap')->willReturn($columnMap);
-        $dataMapper = $this->getAccessibleMock(DataMapper::class, ['getDataMap', 'fetchRelated'], [], '', false);
-        $dataMapper->method('getDataMap')->willReturn($dataMap);
-        $dataMapper->expects(self::never())->method('fetchRelated');
-        $result = $dataMapper->_call('mapObjectToClassProperty', $this->createMock(AbstractEntity::class), 'SomeName', '');
-        self::assertNull($result);
+        // Assert
+        self::assertSame(
+            ['pid' => QueryInterface::ORDER_ASCENDING],
+            $orderings
+        );
     }
 
     /**
-     * Test if mapObjectToClassProperty method returns objects
-     * that are already registered in the persistence session
-     * without query it from the persistence layer
-     *
-     * This is actually a functional test as it tests multiple units along with a very specific setup of dependencies.
-     * @todo: Cover this functionality by a functional test
-     *
      * @test
      */
-    public function mapObjectToClassPropertyReturnsExistingObjectWithoutCallingFetchRelated(): void
+    public function setOneToManyRelationDetectsForeignSortBy(): void
     {
-        $columnMap = new ColumnMap('columnName', 'propertyName');
-        $columnMap->setTypeOfRelation(ColumnMap::RELATION_HAS_ONE);
-        $dataMap = $this->getMockBuilder(DataMap::class)
-            ->onlyMethods(['getColumnMap'])
-            ->disableOriginalConstructor()
-            ->getMock();
-
-        $object = new DummyParentEntity();
-        $child = new DummyChildEntity();
-
-        $classSchema1 = new ClassSchema(DummyParentEntity::class);
-        $identifier = 1;
-
-        $psrContainer = $this->getMockBuilder(ContainerInterface::class)
-            ->onlyMethods(['has', 'get'])
-            ->disableOriginalConstructor()
-            ->getMock();
-        $psrContainer->method('has')->willReturn(false);
-        $container = new Container($psrContainer);
-
-        $session = new Session($container);
-        $session->registerObject($child, $identifier);
-
-        $mockReflectionService = $this->getMockBuilder(ReflectionService::class)
-            ->onlyMethods(['getClassSchema'])
-            ->disableOriginalConstructor()
-            ->getMock();
-        $mockReflectionService->method('getClassSchema')->willReturn($classSchema1);
-
-        $dataMap->method('getColumnMap')->willReturn($columnMap);
-
-        $dataMapper = $this->getAccessibleMock(
-            DataMapper::class,
-            ['getDataMap', 'getNonEmptyRelationValue'],
+        // Arrange
+        $this->dataMapFactory->setOneToManyRelation(
+            $this->columnMap,
             [
-                $mockReflectionService,
-                $this->createMock(QueryObjectModelFactory::class),
-                $session,
-                $this->createMock(DataMapFactory::class),
-                $this->createMock(QueryFactoryInterface::class),
-                $this->createMock(ObjectManagerInterface::class),
-                $this->createMock(EventDispatcherInterface::class),
+                'foreign_table' => 'tx_myextension_bar',
+                'foreign_sortby' => 'uid',
             ]
         );
-        $dataMapper->method('getDataMap')->willReturn($dataMap);
-        $dataMapper->expects(self::never())->method('getNonEmptyRelationValue');
-        $result = $dataMapper->_call('mapObjectToClassProperty', $object, 'relationProperty', $identifier);
-        self::assertEquals($child, $result);
-    }
 
-    /**
-     * Data provider for date checks. Date will be stored based on UTC in
-     * the database. That's why it's not possible to check for explicit date
-     * strings but using the date('c') conversion instead, which considers the
-     * current local timezone setting.
-     *
-     * @return array
-     */
-    public function mapDateTimeHandlesDifferentFieldEvaluationsDataProvider(): array
-    {
-        return [
-            'nothing' => [null, null, null],
-            'timestamp' => [1, null, date('c', 1)],
-            'invalid date' => ['0000-00-00', 'date', null],
-            'valid date' => ['2013-01-01', 'date', date('c', strtotime('2013-01-01 00:00:00'))],
-            'invalid datetime' => ['0000-00-00 00:00:00', 'datetime', null],
-            'valid datetime' => ['2013-01-01 01:02:03', 'datetime', date('c', strtotime('2013-01-01 01:02:03'))],
-        ];
+        // Act
+        $orderings = $this->dataMapper->getOrderingsForColumnMap($this->columnMap);
+
+        // Assert
+        self::assertSame(
+            ['uid' => QueryInterface::ORDER_ASCENDING],
+            $orderings
+        );
     }
 
     /**
-     * @param string|int|null $value
-     * @param string|null $storageFormat
-     * @param string|null $expectedValue
      * @test
-     * @dataProvider mapDateTimeHandlesDifferentFieldEvaluationsDataProvider
      */
-    public function mapDateTimeHandlesDifferentFieldEvaluations($value, $storageFormat, $expectedValue): void
+    public function setOneToManyRelationDetectsForeignSortByWithForeignDefaultSortBy(): void
     {
-        $accessibleDataMapFactory = $this->getAccessibleMock(DataMapper::class, ['dummy'], [], '', false);
-
-        $dateTime = $accessibleDataMapFactory->_call('mapDateTime', $value, $storageFormat);
-
-        if ($expectedValue === null) {
-            self::assertNull($dateTime);
-        } else {
-            self::assertEquals($expectedValue, $dateTime->format('c'));
-        }
-    }
+        // Arrange
+        $this->dataMapFactory->setOneToManyRelation(
+            $this->columnMap,
+            [
+                'foreign_table' => 'tx_myextension_bar',
+                'foreign_sortby' => 'uid',
+                'foreign_default_sortby' => 'pid',
+            ]
+        );
 
-    /**
-     * @return array
-     */
-    public function mapDateTimeHandlesDifferentFieldEvaluationsWithTimeZoneDataProvider(): array
-    {
-        return [
-            'nothing' => [null, null, null],
-            'timestamp' => [1, null, '@1'],
-            'invalid date' => ['0000-00-00', 'date', null],
-            'valid date' => ['2013-01-01', 'date', '2013-01-01T00:00:00'],
-            'invalid datetime' => ['0000-00-00 00:00:00', 'datetime', null],
-            'valid datetime' => ['2013-01-01 01:02:03', 'datetime', '2013-01-01T01:02:03'],
-        ];
-    }
+        // Act
+        $orderings = $this->dataMapper->getOrderingsForColumnMap($this->columnMap);
 
-    /**
-     * @param string|int|null $value
-     * @param string|null $storageFormat
-     * @param string|null $expectedValue
-     * @test
-     * @dataProvider mapDateTimeHandlesDifferentFieldEvaluationsWithTimeZoneDataProvider
-     */
-    public function mapDateTimeHandlesDifferentFieldEvaluationsWithTimeZone($value, ?string $storageFormat, ?string $expectedValue): void
-    {
-        $originalTimeZone = date_default_timezone_get();
-        date_default_timezone_set('America/Chicago');
-        $usedTimeZone = date_default_timezone_get();
-        $accessibleDataMapFactory = $this->getAccessibleMock(DataMapper::class, ['dummy'], [], '', false);
-
-        /** @var \DateTime|MockObject|AccessibleObjectInterface $dateTime */
-        $dateTime = $accessibleDataMapFactory->_call('mapDateTime', $value, $storageFormat);
-
-        if ($expectedValue === null) {
-            self::assertNull($dateTime);
-        } else {
-            self::assertEquals(new \DateTime($expectedValue, new \DateTimeZone($usedTimeZone)), $dateTime);
-        }
-        // Restore the systems current timezone
-        date_default_timezone_set($originalTimeZone);
+        // Assert
+        self::assertSame(
+            ['uid' => QueryInterface::ORDER_ASCENDING],
+            $orderings
+        );
     }
 
     /**
      * @test
      */
-    public function mapDateTimeHandlesSubclassesOfDateTime(): void
+    public function setOneToManyRelationDetectsForeignDefaultSortByWithoutDirection(): void
     {
-        $accessibleDataMapFactory = $this->getAccessibleMock(DataMapper::class, ['dummy'], [], '', false);
-        $targetType = 'TYPO3\CMS\Extbase\Tests\Unit\Persistence\Fixture\Model\CustomDateTime';
-        $date = '2013-01-01 01:02:03';
-        $storageFormat = 'datetime';
-
-        /** @var \DateTime|MockObject|AccessibleObjectInterface $dateTime */
-        $dateTime = $accessibleDataMapFactory->_call('mapDateTime', $date, $storageFormat, $targetType);
+        // Arrange
+        $this->dataMapFactory->setOneToManyRelation(
+            $this->columnMap,
+            [
+                'foreign_table' => 'tx_myextension_bar',
+                'foreign_default_sortby' => 'pid',
+            ]
+        );
 
-        self::assertInstanceOf($targetType, $dateTime);
-    }
+        // Act
+        $orderings = $this->dataMapper->getOrderingsForColumnMap($this->columnMap);
 
-    /**
-     * @test
-     */
-    public function getPlainValueReturnsCorrectDateTimeFormat(): void
-    {
-        $subject = $this->createPartialMock(DataMapper::class, []);
-
-        $columnMap = new ColumnMap('column_name', 'propertyName');
-        $columnMap->setDateTimeStorageFormat('datetime');
-        $input = new \DateTime('2013-04-15 09:30:00');
-        self::assertEquals('2013-04-15 09:30:00', $subject->getPlainValue($input, $columnMap));
-        $columnMap->setDateTimeStorageFormat('date');
-        self::assertEquals('2013-04-15', $subject->getPlainValue($input, $columnMap));
+        // Assert
+        self::assertSame(
+            ['pid' => QueryInterface::ORDER_ASCENDING],
+            $orderings
+        );
     }
 
     /**
      * @test
-     * @dataProvider getPlainValueReturnsExpectedValuesDataProvider
      */
-    public function getPlainValueReturnsExpectedValues($expectedValue, $input): void
+    public function setOneToManyRelationDetectsForeignDefaultSortByWithDirection(): void
     {
-        $dataMapper = $this->createPartialMock(DataMapper::class, []);
+        // Arrange
+        $this->dataMapFactory->setOneToManyRelation(
+            $this->columnMap,
+            [
+                'foreign_table' => 'tx_myextension_bar',
+                'foreign_default_sortby' => 'pid desc',
+            ]
+        );
 
-        self::assertSame($expectedValue, $dataMapper->getPlainValue($input));
-    }
+        // Act
+        $orderings = $this->dataMapper->getOrderingsForColumnMap($this->columnMap);
 
-    /**
-     * @return array
-     */
-    public function getPlainValueReturnsExpectedValuesDataProvider(): array
-    {
-        $traversableDomainObject = $this->prophesize()
-            ->willImplement(\Iterator::class)
-            ->willImplement(DomainObjectInterface::class);
-        $traversableDomainObject->getUid()->willReturn(1);
-
-        return [
-            'datetime to timestamp' => ['1365866253', new \DateTime('@1365866253')],
-            'boolean true to 1' => [1, true],
-            'boolean false to 0' => [0, false],
-            'NULL is handled as string' => ['NULL', null],
-            'string value is returned unchanged' => ['RANDOM string', 'RANDOM string'],
-            'array is flattened' => ['a,b,c', ['a', 'b', 'c']],
-            'deep array is flattened' => ['a,b,c', [['a', 'b'], 'c']],
-            'traversable domain object to identifier' => [1, $traversableDomainObject->reveal()],
-            'integer value is returned unchanged' => [1234, 1234],
-            'float is converted to string' => ['1234.56', 1234.56],
-        ];
+        // Assert
+        self::assertSame(
+            ['pid' => QueryInterface::ORDER_DESCENDING],
+            $orderings
+        );
     }
 
     /**
      * @test
      */
-    public function getPlainValueCallsGetRealInstanceOnInputIfInputIsInstanceOfLazyLoadingProxy(): void
+    public function setOneToManyRelationDetectsMultipleForeignDefaultSortByWithAndWithoutDirection(): void
     {
-        $this->expectException(UnexpectedTypeException::class);
-        $this->expectExceptionCode(1274799934);
-
-        $dataMapper = $this->createPartialMock(DataMapper::class, []);
-        $input = $this->createMock(LazyLoadingProxy::class);
-        $input->expects(self::once())->method('_loadRealInstance')->willReturn($dataMapper);
-        $dataMapper->getPlainValue($input);
-    }
+        // Arrange
+        $this->dataMapFactory->setOneToManyRelation(
+            $this->columnMap,
+            [
+                'foreign_table' => 'tx_myextension_bar',
+                'foreign_default_sortby' => 'pid desc, title, uid asc',
+            ]
+        );
 
-    /**
-     * @test
-     */
-    public function getPlainValueCallsGetUidOnDomainObjectInterfaceInput(): void
-    {
-        $dataMapper = $this->createPartialMock(DataMapper::class, []);
-        $input = $this->createMock(DomainObjectInterface::class);
+        // Act
+        $orderings = $this->dataMapper->getOrderingsForColumnMap($this->columnMap);
 
-        $input->expects(self::once())->method('getUid')->willReturn(23);
-        self::assertSame(23, $dataMapper->getPlainValue($input));
+        // Assert
+        self::assertSame(
+            ['pid' => QueryInterface::ORDER_DESCENDING, 'title' => QueryInterface::ORDER_ASCENDING, 'uid' => QueryInterface::ORDER_ASCENDING],
+            $orderings
+        );
     }
 }
-- 
GitLab