From 490666c6176f15d87d375e146bba1fcbacd28a82 Mon Sep 17 00:00:00 2001 From: MateuszKolankowski Date: Fri, 16 Jan 2026 13:53:44 +0100 Subject: [PATCH 1/5] Added limits to repository child count queries and subtree operations --- src/bundle/Controller/LocationController.php | 2 +- .../Parser/SubtreeOperations.php | 31 +++++++++++++++---- .../config/ezplatform_default_settings.yaml | 1 + .../admin/content/tab/locations/tab.html.twig | 2 +- .../HasChildren.php | 13 ++++++-- .../Specification/Location/HasChildren.php | 2 +- .../Location/IsWithinCopySubtreeLimit.php | 2 +- src/lib/Tab/LocationView/LocationsTab.php | 6 ++++ .../Subitems/ContentViewParameterSupplier.php | 7 ++++- src/lib/UI/Value/Content/Location.php | 2 +- src/lib/UI/Value/ValueFactory.php | 9 ++++-- 11 files changed, 60 insertions(+), 17 deletions(-) diff --git a/src/bundle/Controller/LocationController.php b/src/bundle/Controller/LocationController.php index 3fc3fb7ace..427ba86eb9 100644 --- a/src/bundle/Controller/LocationController.php +++ b/src/bundle/Controller/LocationController.php @@ -255,7 +255,7 @@ public function swapAction(Request $request): Response $currentLocation = $data->getCurrentLocation(); $newLocation = $data->getNewLocation(); - $childCount = $this->locationService->getLocationChildCount($currentLocation); + $childCount = $this->locationService->getLocationChildCount($currentLocation, 1); $contentType = $newLocation?->getContent()->getContentType(); if (!$contentType?->isContainer() && $childCount) { diff --git a/src/bundle/DependencyInjection/Configuration/Parser/SubtreeOperations.php b/src/bundle/DependencyInjection/Configuration/Parser/SubtreeOperations.php index 7856d5752a..07e86bfc32 100644 --- a/src/bundle/DependencyInjection/Configuration/Parser/SubtreeOperations.php +++ b/src/bundle/DependencyInjection/Configuration/Parser/SubtreeOperations.php @@ -23,6 +23,8 @@ * subtree_operations: * copy_subtree: * limit: 200 + * query_subtree: + * limit: 500 * ``` */ final class SubtreeOperations extends AbstractParser @@ -35,15 +37,25 @@ public function mapConfig( mixed $currentScope, ContextualizerInterface $contextualizer ): void { - if (!isset($scopeSettings['subtree_operations']['copy_subtree']['limit'])) { + if (!isset($scopeSettings['subtree_operations'])) { return; } - $contextualizer->setContextualParameter( - 'subtree_operations.copy_subtree.limit', - $currentScope, - $scopeSettings['subtree_operations']['copy_subtree']['limit'] - ); + if (isset($scopeSettings['subtree_operations']['copy_subtree']['limit'])) { + $contextualizer->setContextualParameter( + 'subtree_operations.copy_subtree.limit', + $currentScope, + $scopeSettings['subtree_operations']['copy_subtree']['limit'] + ); + } + + if (isset($scopeSettings['subtree_operations']['query_subtree']['limit'])) { + $contextualizer->setContextualParameter( + 'subtree_operations.query_subtree.limit', + $currentScope, + $scopeSettings['subtree_operations']['query_subtree']['limit'] + ); + } } public function addSemanticConfig(NodeBuilder $nodeBuilder): void @@ -60,6 +72,13 @@ public function addSemanticConfig(NodeBuilder $nodeBuilder): void ->end() ->end() ->end() + ->arrayNode('query_subtree') + ->children() + ->integerNode('limit') + ->info('Limit the total count of items queried for when calculating the the number of direct children a node has. -1 for no limit.') + ->isRequired() + ->end() + ->end() ->end() ->end(); } diff --git a/src/bundle/Resources/config/ezplatform_default_settings.yaml b/src/bundle/Resources/config/ezplatform_default_settings.yaml index 15bcba7a60..7babb1ba12 100644 --- a/src/bundle/Resources/config/ezplatform_default_settings.yaml +++ b/src/bundle/Resources/config/ezplatform_default_settings.yaml @@ -42,6 +42,7 @@ parameters: # Subtree Operations ibexa.site_access.config.admin_group.subtree_operations.copy_subtree.limit: 100 + ibexa.site_access.config.admin_group.subtree_operations.query_subtree.limit: 500 # Notifications ibexa.site_access.config.admin_group.notification_count.interval: 30000 diff --git a/src/bundle/Resources/views/themes/admin/content/tab/locations/tab.html.twig b/src/bundle/Resources/views/themes/admin/content/tab/locations/tab.html.twig index 72062f6e92..0fa5775eeb 100644 --- a/src/bundle/Resources/views/themes/admin/content/tab/locations/tab.html.twig +++ b/src/bundle/Resources/views/themes/admin/content/tab/locations/tab.html.twig @@ -82,7 +82,7 @@ }]) %} {% set body_row_cols = body_row_cols|merge([ - { content: location.childCount }, + { content: location.childCount is null ? (sub_item_query_limit ~ '+') : location.childCount }, ]) %} {% set body_rows = body_rows|merge([{ cols: body_row_cols }]) %} diff --git a/src/lib/Form/TrashLocationOptionProvider/HasChildren.php b/src/lib/Form/TrashLocationOptionProvider/HasChildren.php index 89231ae576..fa753d00ee 100644 --- a/src/lib/Form/TrashLocationOptionProvider/HasChildren.php +++ b/src/lib/Form/TrashLocationOptionProvider/HasChildren.php @@ -11,6 +11,7 @@ use Ibexa\AdminUi\Specification\Location\HasChildren as HasChildrenSpec; use Ibexa\Contracts\Core\Repository\LocationService; use Ibexa\Contracts\Core\Repository\Values\Content\Location; +use Ibexa\Contracts\Core\SiteAccess\ConfigResolverInterface; use JMS\TranslationBundle\Annotation\Desc; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\FormInterface; @@ -20,7 +21,8 @@ { public function __construct( private LocationService $locationService, - private TranslatorInterface $translator + private TranslatorInterface $translator, + private ConfigResolverInterface $configResolver ) { } @@ -31,10 +33,15 @@ public function supports(Location $location): bool public function addOptions(FormInterface $form, Location $location): void { - $childCount = $this->locationService->getLocationChildCount($location); + $limit = (int) $this->configResolver->getParameter('subtree_operations.query_subtree.limit'); + + $useLimit = $limit > 0; + $childCount = $this->locationService->getLocationChildCount($location, $useLimit ? $limit + 1 : null); $translatorParameters = [ - '%children_count%' => $childCount, + '%children_count%' => ($useLimit && $childCount >= $limit) ? + sprintf('%d+', $limit) : + $childCount, '%content%' => $location->getContent()->getName(), ]; diff --git a/src/lib/Specification/Location/HasChildren.php b/src/lib/Specification/Location/HasChildren.php index c1b5afb9c5..185cb3d73b 100644 --- a/src/lib/Specification/Location/HasChildren.php +++ b/src/lib/Specification/Location/HasChildren.php @@ -22,7 +22,7 @@ public function __construct(private readonly LocationService $locationService) */ public function isSatisfiedBy(mixed $item): bool { - $childCount = $this->locationService->getLocationChildCount($item); + $childCount = $this->locationService->getLocationChildCount($item, 1); return 0 < $childCount; } diff --git a/src/lib/Specification/Location/IsWithinCopySubtreeLimit.php b/src/lib/Specification/Location/IsWithinCopySubtreeLimit.php index f56cdda1c3..78a947997a 100644 --- a/src/lib/Specification/Location/IsWithinCopySubtreeLimit.php +++ b/src/lib/Specification/Location/IsWithinCopySubtreeLimit.php @@ -36,7 +36,7 @@ public function isSatisfiedBy(mixed $item): bool return false; } - return $this->copyLimit >= $this->locationService->getSubtreeSize($item); + return $this->copyLimit >= $this->locationService->getSubtreeSize($item, $this->copyLimit + 1); } private function isContainer(Location $location): bool diff --git a/src/lib/Tab/LocationView/LocationsTab.php b/src/lib/Tab/LocationView/LocationsTab.php index 49012f0fac..8baff3bb24 100644 --- a/src/lib/Tab/LocationView/LocationsTab.php +++ b/src/lib/Tab/LocationView/LocationsTab.php @@ -153,12 +153,18 @@ public function getTemplateParameters(array $contextParameters = []): array ); } + $subItemQueryLimit = $this->configResolver->getParameter('subtree_operations.query_subtree.limit'); + if ($subItemQueryLimit <= 0) { + $subItemQueryLimit = null; + } + $viewParameters = [ 'pager' => $pagination, 'pager_options' => [ 'pageParameter' => sprintf('[%s]', self::PAGINATION_PARAM_NAME), ], 'locations' => $locations, + 'sub_item_query_limit' => $subItemQueryLimit, 'form_content_location_add' => $formLocationAdd->createView(), 'form_content_location_remove' => $formLocationRemove->createView(), 'form_content_location_swap' => $formLocationSwap->createView(), diff --git a/src/lib/UI/Module/Subitems/ContentViewParameterSupplier.php b/src/lib/UI/Module/Subitems/ContentViewParameterSupplier.php index e20c4cfe4e..0edc779ba0 100644 --- a/src/lib/UI/Module/Subitems/ContentViewParameterSupplier.php +++ b/src/lib/UI/Module/Subitems/ContentViewParameterSupplier.php @@ -125,7 +125,12 @@ private function createRestLocation(Location $location): RestLocation { return new RestLocation( $location, - $this->locationService->getLocationChildCount($location) + $this->locationService->getLocationChildCount( + $location, + // For the sub items module we only ever use the count to determine if there are children (0 or 1+), + // hence setting a limit of 1 is sufficient here. + 1 + ) ); } diff --git a/src/lib/UI/Value/Content/Location.php b/src/lib/UI/Value/Content/Location.php index 9ff045c366..27ca4c8aef 100644 --- a/src/lib/UI/Value/Content/Location.php +++ b/src/lib/UI/Value/Content/Location.php @@ -17,7 +17,7 @@ */ class Location extends CoreLocation { - protected int $childCount; + protected ?int $childCount; protected bool $main; diff --git a/src/lib/UI/Value/ValueFactory.php b/src/lib/UI/Value/ValueFactory.php index 7dc529b799..7839e64e43 100644 --- a/src/lib/UI/Value/ValueFactory.php +++ b/src/lib/UI/Value/ValueFactory.php @@ -36,6 +36,7 @@ use Ibexa\Contracts\Core\Repository\Values\ObjectState\ObjectStateGroup; use Ibexa\Contracts\Core\Repository\Values\User\Policy; use Ibexa\Contracts\Core\Repository\Values\User\RoleAssignment; +use Ibexa\Contracts\Core\SiteAccess\ConfigResolverInterface; use Ibexa\Core\MVC\Symfony\Locale\UserLanguagePreferenceProviderInterface; use Ibexa\Core\Repository\LocationResolver\LocationResolver; use RuntimeException; @@ -53,7 +54,8 @@ public function __construct( protected PathService $pathService, protected DatasetFactory $datasetFactory, private UserLanguagePreferenceProviderInterface $userLanguagePreferenceProvider, - protected LocationResolver $locationResolver + protected LocationResolver $locationResolver, + protected ConfigResolverInterface $configResolver, ) { } @@ -137,9 +139,12 @@ public function createLocation(Location $location): UIValue\Content\Location { $translations = $location->getContent()->getVersionInfo()->getLanguageCodes(); $target = (new Target\Version())->deleteTranslations($translations); + $limit = $this->configResolver->getParameter('subtree_operations.query_subtree.limit'); + $useLimit = $limit > 0; + $count = $this->locationService->getLocationChildCount($location, $useLimit ? $limit : 1); return new UIValue\Content\Location($location, [ - 'childCount' => $this->locationService->getLocationChildCount($location), + 'childCount' => $useLimit && $count >= $limit ? null : $count, 'pathLocations' => $this->pathService->loadPathLocations($location), 'userCanManage' => $this->permissionResolver->canUser( 'content', From 91b42c078d609e30289ce8e50ffd500f88cee4e2 Mon Sep 17 00:00:00 2001 From: MateuszKolankowski Date: Tue, 27 Jan 2026 13:02:43 +0100 Subject: [PATCH 2/5] Added limits to subtree query configuration and updated tests --- .../Parser/SubtreeOperations.php | 1 - .../Parser/SubtreeOperationsTest.php | 85 +++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/src/bundle/DependencyInjection/Configuration/Parser/SubtreeOperations.php b/src/bundle/DependencyInjection/Configuration/Parser/SubtreeOperations.php index 07e86bfc32..1ead732d05 100644 --- a/src/bundle/DependencyInjection/Configuration/Parser/SubtreeOperations.php +++ b/src/bundle/DependencyInjection/Configuration/Parser/SubtreeOperations.php @@ -76,7 +76,6 @@ public function addSemanticConfig(NodeBuilder $nodeBuilder): void ->children() ->integerNode('limit') ->info('Limit the total count of items queried for when calculating the the number of direct children a node has. -1 for no limit.') - ->isRequired() ->end() ->end() ->end() diff --git a/tests/bundle/DependencyInjection/Configuration/Parser/SubtreeOperationsTest.php b/tests/bundle/DependencyInjection/Configuration/Parser/SubtreeOperationsTest.php index 8375ac625f..430c3215f4 100644 --- a/tests/bundle/DependencyInjection/Configuration/Parser/SubtreeOperationsTest.php +++ b/tests/bundle/DependencyInjection/Configuration/Parser/SubtreeOperationsTest.php @@ -32,6 +32,16 @@ public function getExpectedCopySubtreeLimit(): iterable yield 'disabled = 0' => [0]; } + /** + * @return iterable + */ + public function getExpectedQuerySubtreeLimit(): iterable + { + yield 'no limit = -1' => [-1]; + yield 'custom limit = 1000' => [1000]; + yield 'disabled = 0' => [0]; + } + protected function setUp(): void { $this->parser = new SubtreeOperations(); @@ -77,4 +87,79 @@ public function testCopySubtreeLimitNotSet(): void $this->parser->mapConfig($scopeSettings, $currentScope, $this->contextualizer); } + + /** + * @dataProvider getExpectedQuerySubtreeLimit + */ + public function testQuerySubtreeLimit(int $expectedQuerySubtreeLimit): void + { + $scopeSettings = [ + 'subtree_operations' => [ + 'query_subtree' => [ + 'limit' => $expectedQuerySubtreeLimit, + ], + ], + ]; + $currentScope = 'admin_group'; + + $this->contextualizer + ->expects(self::once()) + ->method('setContextualParameter') + ->with( + 'subtree_operations.query_subtree.limit', + $currentScope, + $expectedQuerySubtreeLimit + ); + + $this->parser->mapConfig($scopeSettings, $currentScope, $this->contextualizer); + } + + public function testQuerySubtreeLimitNotSet(): void + { + $scopeSettings = [ + 'subtree_operations' => [ + 'query_subtree' =>null, + ], + ]; + $currentScope = 'admin_group'; + + $this->contextualizer + ->expects(self::never()) + ->method('setContextualParameter'); + + $this->parser->mapConfig($scopeSettings, $currentScope, $this->contextualizer); + } + + public function testBothSubtreeOperationsSet(): void + { + $scopeSettings = [ + 'subtree_operations' => [ + 'copy_subtree' => [ + 'limit' => 200, + ], + 'query_subtree' => [ + 'limit' => 500, + ], + ], + ]; + $currentScope = 'admin_group'; + + $this->contextualizer + ->expects(self::exactly(2)) + ->method('setContextualParameter') + ->withConsecutive( + [ + 'subtree_operations.copy_subtree.limit', + $currentScope, + 200, + ], + [ + 'subtree_operations.query_subtree.limit', + $currentScope, + 500, + ] + ); + + $this->parser->mapConfig($scopeSettings, $currentScope, $this->contextualizer); + } } From 3ca8cf9a97f242abdf1ab0ef7845d8d6a17e64df Mon Sep 17 00:00:00 2001 From: MateuszKolankowski Date: Tue, 27 Jan 2026 13:23:40 +0100 Subject: [PATCH 3/5] Fix formatting in SubtreeOperationsTest.php --- .../Configuration/Parser/SubtreeOperationsTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/bundle/DependencyInjection/Configuration/Parser/SubtreeOperationsTest.php b/tests/bundle/DependencyInjection/Configuration/Parser/SubtreeOperationsTest.php index 430c3215f4..f5929379fe 100644 --- a/tests/bundle/DependencyInjection/Configuration/Parser/SubtreeOperationsTest.php +++ b/tests/bundle/DependencyInjection/Configuration/Parser/SubtreeOperationsTest.php @@ -118,7 +118,7 @@ public function testQuerySubtreeLimitNotSet(): void { $scopeSettings = [ 'subtree_operations' => [ - 'query_subtree' =>null, + 'query_subtree' => null, ], ]; $currentScope = 'admin_group'; From a2447e5a74b2d9aec2c4b8e53d41ad66cf9cd1d7 Mon Sep 17 00:00:00 2001 From: MateuszKolankowski Date: Thu, 29 Jan 2026 15:16:21 +0100 Subject: [PATCH 4/5] Updated limit description for query_subtree in SubtreeOperations --- .../Configuration/Parser/SubtreeOperations.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bundle/DependencyInjection/Configuration/Parser/SubtreeOperations.php b/src/bundle/DependencyInjection/Configuration/Parser/SubtreeOperations.php index 1ead732d05..550f53d7c6 100644 --- a/src/bundle/DependencyInjection/Configuration/Parser/SubtreeOperations.php +++ b/src/bundle/DependencyInjection/Configuration/Parser/SubtreeOperations.php @@ -75,7 +75,7 @@ public function addSemanticConfig(NodeBuilder $nodeBuilder): void ->arrayNode('query_subtree') ->children() ->integerNode('limit') - ->info('Limit the total count of items queried for when calculating the the number of direct children a node has. -1 for no limit.') + ->info('Limit the total count of items queried for when calculating the number of direct children a node has. -1 for no limit.') ->end() ->end() ->end() From 62929bcd51c454e40798e900accb55fbe2740d28 Mon Sep 17 00:00:00 2001 From: MateuszKolankowski Date: Fri, 6 Feb 2026 14:37:00 +0100 Subject: [PATCH 5/5] Added null limit handling for location child count retrieval --- src/lib/UI/Value/ValueFactory.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/UI/Value/ValueFactory.php b/src/lib/UI/Value/ValueFactory.php index 7839e64e43..c54be25dcc 100644 --- a/src/lib/UI/Value/ValueFactory.php +++ b/src/lib/UI/Value/ValueFactory.php @@ -141,7 +141,7 @@ public function createLocation(Location $location): UIValue\Content\Location $target = (new Target\Version())->deleteTranslations($translations); $limit = $this->configResolver->getParameter('subtree_operations.query_subtree.limit'); $useLimit = $limit > 0; - $count = $this->locationService->getLocationChildCount($location, $useLimit ? $limit : 1); + $count = $this->locationService->getLocationChildCount($location, $useLimit ? $limit : null); return new UIValue\Content\Location($location, [ 'childCount' => $useLimit && $count >= $limit ? null : $count,