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