diff --git a/typo3/sysext/extbase/Classes/Persistence/Generic/Mapper/ColumnMap.php b/typo3/sysext/extbase/Classes/Persistence/Generic/Mapper/ColumnMap.php index 37a7991422dbe3c2174e749d6c25fb1351bf1f08..d21509fc4adc960c201cbc2203246e5dd22ea5a5 100644 --- a/typo3/sysext/extbase/Classes/Persistence/Generic/Mapper/ColumnMap.php +++ b/typo3/sysext/extbase/Classes/Persistence/Generic/Mapper/ColumnMap.php @@ -81,6 +81,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 @@ -224,6 +231,16 @@ class ColumnMap return $this->childTableName; } + public function setChildTableDefaultSortings(?string $childTableDefaultSortings): void + { + $this->childTableDefaultSortings = $childTableDefaultSortings; + } + + public function getChildTableDefaultSortings(): ?string + { + return $this->childTableDefaultSortings; + } + public function setChildSortByFieldName(?string $childSortByFieldName): void { $this->childSortByFieldName = $childSortByFieldName; diff --git a/typo3/sysext/extbase/Classes/Persistence/Generic/Mapper/DataMapFactory.php b/typo3/sysext/extbase/Classes/Persistence/Generic/Mapper/DataMapFactory.php index f9dbaf01f1570a4b2554c198a521317c0c7b83f1..84c031326f78ec8c905ed93d56ebbb1a1ea968af 100644 --- a/typo3/sysext/extbase/Classes/Persistence/Generic/Mapper/DataMapFactory.php +++ b/typo3/sysext/extbase/Classes/Persistence/Generic/Mapper/DataMapFactory.php @@ -369,7 +369,7 @@ class DataMapFactory implements SingletonInterface * @param ColumnMap $columnMap The column map * @param array|null $columnConfiguration The column configuration from $TCA */ - 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. @@ -381,6 +381,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 41d58ab7bbdad821217eeacff669b04723657700..8e75b1aa8df51a8ce3f88984d42e744efec2110c 100644 --- a/typo3/sysext/extbase/Classes/Persistence/Generic/Mapper/DataMapper.php +++ b/typo3/sysext/extbase/Classes/Persistence/Generic/Mapper/DataMapper.php @@ -421,8 +421,8 @@ class DataMapper $query->getQuerySettings()->setLanguageAspect($languageAspect); 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)); @@ -434,6 +434,46 @@ class DataMapper return $query; } + /** + * Get orderings array for extbase query by columnMap + * + * @phpstan-return array<non-empty-string, QueryInterface::ORDER_*>|null + * @return array<string, string>|null + */ + public function getOrderingsForColumnMap(ColumnMap $columnMap): array|null + { + 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 297f613064ae8229a96dd4e56ff76007de764082..f11a8a63b346a2d217110f1f91d2b6725e5adf7b 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 0000000000000000000000000000000000000000..45ca83f8d01f1062d97df98a4949dfef077de294 --- /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 bbb752a96fec0062c9683ab91e9c88ddec4a536c..a68a7003e61da849d72266d844d599ad07ad86a9 100644 --- a/typo3/sysext/extbase/Tests/Functional/Persistence/Generic/Mapper/DataMapperTest.php +++ b/typo3/sysext/extbase/Tests/Functional/Persistence/Generic/Mapper/DataMapperTest.php @@ -18,6 +18,7 @@ declare(strict_types=1); namespace TYPO3\CMS\Extbase\Tests\Functional\Persistence\Generic\Mapper; use ExtbaseTeam\BlogExample\Domain\Model\Blog; +use ExtbaseTeam\BlogExample\Domain\Model\Comment; use ExtbaseTeam\BlogExample\Domain\Model\DateExample; use ExtbaseTeam\BlogExample\Domain\Model\DateTimeImmutableExample; use ExtbaseTeam\BlogExample\Domain\Model\Post; @@ -485,4 +486,28 @@ class DataMapperTest extends FunctionalTestCase self::assertSame(1, $plainValue); } + + /** + * @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 new file mode 100644 index 0000000000000000000000000000000000000000..b086f2b24e7d36abe86cf136740854507e7490c2 --- /dev/null +++ b/typo3/sysext/extbase/Tests/Unit/Persistence/Generic/Mapper/DataMapperTest.php @@ -0,0 +1,249 @@ +<?php + +declare(strict_types=1); + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +namespace TYPO3\CMS\Extbase\Tests\Unit\Persistence\Generic\Mapper; + +use Psr\EventDispatcher\EventDispatcherInterface; +use TYPO3\CMS\Core\Cache\CacheManager; +use TYPO3\CMS\Extbase\Configuration\ConfigurationManager; +use TYPO3\CMS\Extbase\Persistence\ClassesConfiguration; +use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap; +use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapFactory; +use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper; +use TYPO3\CMS\Extbase\Persistence\Generic\Qom\QueryObjectModelFactory; +use TYPO3\CMS\Extbase\Persistence\Generic\QueryFactory; +use TYPO3\CMS\Extbase\Persistence\Generic\Session; +use TYPO3\CMS\Extbase\Persistence\QueryInterface; +use TYPO3\CMS\Extbase\Reflection\ReflectionService; +use TYPO3\TestingFramework\Core\Unit\UnitTestCase; + +class DataMapperTest extends UnitTestCase +{ + protected ColumnMap $columnMap; + protected DataMapFactory $dataMapFactory; + protected DataMapper $dataMapper; + + protected function setUp(): void + { + 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(EventDispatcherInterface::class), + ); + } + + /** + * @test + */ + public function getOrderingsForColumnMapReturnsNullIfNeitherForeignSortByNorForeignDefaultSortByAreSet(): void + { + // Arrange + $this->dataMapFactory->setOneToManyRelation( + $this->columnMap, + [ + 'foreign_table' => 'tx_myextension_bar', + ] + ); + + // Act + $orderings = $this->dataMapper->getOrderingsForColumnMap($this->columnMap); + + // Assert + self::assertNull($orderings); + } + + /** + * @test + */ + public function getOrderingsForColumnMapReturnsNullIfForeignDefaultSortByIsEmpty(): void + { + // Arrange + $this->dataMapFactory->setOneToManyRelation( + $this->columnMap, + [ + 'foreign_table' => 'tx_myextension_bar', + 'foreign_default_sortby' => '', + ] + ); + + // Act + $orderings = $this->dataMapper->getOrderingsForColumnMap($this->columnMap); + + // Assert + self::assertNull($orderings); + } + + /** + * @test + */ + public function getOrderingsForColumnMapFallBackToAscendingOrdering(): void + { + // Arrange + $this->dataMapFactory->setOneToManyRelation( + $this->columnMap, + [ + 'foreign_table' => 'tx_myextension_bar', + 'foreign_default_sortby' => 'pid invalid', + ] + ); + + // Act + $orderings = $this->dataMapper->getOrderingsForColumnMap($this->columnMap); + + // Assert + self::assertSame( + ['pid' => QueryInterface::ORDER_ASCENDING], + $orderings + ); + } + + /** + * @test + */ + public function setOneToManyRelationDetectsForeignSortBy(): void + { + // Arrange + $this->dataMapFactory->setOneToManyRelation( + $this->columnMap, + [ + 'foreign_table' => 'tx_myextension_bar', + 'foreign_sortby' => 'uid', + ] + ); + + // Act + $orderings = $this->dataMapper->getOrderingsForColumnMap($this->columnMap); + + // Assert + self::assertSame( + ['uid' => QueryInterface::ORDER_ASCENDING], + $orderings + ); + } + + /** + * @test + */ + public function setOneToManyRelationDetectsForeignSortByWithForeignDefaultSortBy(): void + { + // Arrange + $this->dataMapFactory->setOneToManyRelation( + $this->columnMap, + [ + 'foreign_table' => 'tx_myextension_bar', + 'foreign_sortby' => 'uid', + 'foreign_default_sortby' => 'pid', + ] + ); + + // Act + $orderings = $this->dataMapper->getOrderingsForColumnMap($this->columnMap); + + // Assert + self::assertSame( + ['uid' => QueryInterface::ORDER_ASCENDING], + $orderings + ); + } + + /** + * @test + */ + public function setOneToManyRelationDetectsForeignDefaultSortByWithoutDirection(): void + { + // Arrange + $this->dataMapFactory->setOneToManyRelation( + $this->columnMap, + [ + 'foreign_table' => 'tx_myextension_bar', + 'foreign_default_sortby' => 'pid', + ] + ); + + // Act + $orderings = $this->dataMapper->getOrderingsForColumnMap($this->columnMap); + + // Assert + self::assertSame( + ['pid' => QueryInterface::ORDER_ASCENDING], + $orderings + ); + } + + /** + * @test + */ + public function setOneToManyRelationDetectsForeignDefaultSortByWithDirection(): void + { + // Arrange + $this->dataMapFactory->setOneToManyRelation( + $this->columnMap, + [ + 'foreign_table' => 'tx_myextension_bar', + 'foreign_default_sortby' => 'pid desc', + ] + ); + + // Act + $orderings = $this->dataMapper->getOrderingsForColumnMap($this->columnMap); + + // Assert + self::assertSame( + ['pid' => QueryInterface::ORDER_DESCENDING], + $orderings + ); + } + + /** + * @test + */ + public function setOneToManyRelationDetectsMultipleForeignDefaultSortByWithAndWithoutDirection(): void + { + // Arrange + $this->dataMapFactory->setOneToManyRelation( + $this->columnMap, + [ + 'foreign_table' => 'tx_myextension_bar', + 'foreign_default_sortby' => 'pid desc, title, uid asc', + ] + ); + + // Act + $orderings = $this->dataMapper->getOrderingsForColumnMap($this->columnMap); + + // Assert + self::assertSame( + ['pid' => QueryInterface::ORDER_DESCENDING, 'title' => QueryInterface::ORDER_ASCENDING, 'uid' => QueryInterface::ORDER_ASCENDING], + $orderings + ); + } +}